memory isolation fix and session grouping in client
This commit is contained in:
@@ -166,6 +166,7 @@ export default function App() {
|
||||
<AllChatsView
|
||||
onBack={goBack}
|
||||
onSelectSession={session => { selectSession(session); navigate('chat'); }}
|
||||
projects={projects}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { fetchSessions, deleteSession } from '../api/orchestration';
|
||||
import { API_DEFAULTS, CLIENT_DEFAULTS } from '../config/constants';
|
||||
import { CLIENT_DEFAULTS } from '../config/constants';
|
||||
|
||||
const PAGE_SIZE = CLIENT_DEFAULTS.PAGE_SIZE;
|
||||
|
||||
export default function AllChatsView({ onSelectSession, onBack }) {
|
||||
export default function AllChatsView({ onSelectSession, onBack, projects }) {
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(0);
|
||||
@@ -23,8 +23,6 @@ export default function AllChatsView({ onSelectSession, onBack }) {
|
||||
try {
|
||||
const data = await fetchSessions(PAGE_SIZE, p * PAGE_SIZE);
|
||||
setSessions(data);
|
||||
// We don't have a total count from the API yet — infer pagination
|
||||
// from whether we got a full page back
|
||||
setTotal(data.length === PAGE_SIZE ? (p + 2) * PAGE_SIZE : p * PAGE_SIZE + data.length);
|
||||
} catch (err) {
|
||||
console.error('[AllChatsView] Failed to load sessions:', err.message);
|
||||
@@ -75,10 +73,14 @@ export default function AllChatsView({ onSelectSession, onBack }) {
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays === 1) return 'Yesterday';
|
||||
// Absolute date past 1 day
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
function getProject(projectId) {
|
||||
if (!projectId || !projects) return null;
|
||||
return projects.find(p => p.id === projectId) ?? null;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||
const allSelected = sessions.length > 0 && selected.size === sessions.length;
|
||||
|
||||
@@ -119,7 +121,6 @@ export default function AllChatsView({ onSelectSession, onBack }) {
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<th style={{ width: '36px', padding: '8px 0' }}>
|
||||
{/* Select all checkbox */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
@@ -128,12 +129,14 @@ export default function AllChatsView({ onSelectSession, onBack }) {
|
||||
/>
|
||||
</th>
|
||||
<th className="label-upper" style={{ textAlign: 'left', padding: '8px 12px' }}>Name</th>
|
||||
<th className="label-upper" style={{ textAlign: 'right', padding: '8px 0', width: '120px' }}>Last Active</th>
|
||||
<th className="label-upper" style={{ textAlign: 'left', padding: '8px 12px', width: '130px' }}>Project</th>
|
||||
<th className="label-upper" style={{ textAlign: 'right', padding: '8px 0', width: '110px' }}>Last Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessions.map(session => {
|
||||
const isSelected = selected.has(session.external_id);
|
||||
const project = getProject(session.project_id);
|
||||
return (
|
||||
<tr
|
||||
key={session.external_id}
|
||||
@@ -162,6 +165,21 @@ export default function AllChatsView({ onSelectSession, onBack }) {
|
||||
{session.name || session.external_id}
|
||||
</button>
|
||||
</td>
|
||||
<td style={{ padding: '10px 12px' }}>
|
||||
{project ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<div style={{
|
||||
width: '6px', height: '6px', borderRadius: '50%', flexShrink: 0,
|
||||
background: project.colour ?? 'var(--accent)',
|
||||
}} />
|
||||
<span className="text-xs text-muted truncate" style={{ maxWidth: '90px' }}>
|
||||
{project.name}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-xs text-muted" style={{ textAlign: 'right', padding: '10px 0' }}>
|
||||
{formatTimestamp(session.updated_at)}
|
||||
</td>
|
||||
@@ -171,7 +189,7 @@ export default function AllChatsView({ onSelectSession, onBack }) {
|
||||
|
||||
{sessions.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-base text-muted"
|
||||
<td colSpan={4} className="text-base text-muted"
|
||||
style={{ textAlign: 'center', padding: '40px' }}>
|
||||
No conversations yet
|
||||
</td>
|
||||
|
||||
@@ -45,11 +45,6 @@ export default function Sidebar({
|
||||
}
|
||||
}
|
||||
|
||||
function getPreview(session) {
|
||||
if (session.isNew) return 'New conversation';
|
||||
return session.name || session.external_id;
|
||||
}
|
||||
|
||||
// ── Collapsed rail ───────────────────────────────────────
|
||||
|
||||
if (!isOpen) {
|
||||
@@ -96,6 +91,30 @@ export default function Sidebar({
|
||||
|
||||
const recentSessions = sessions.slice(0, 10);
|
||||
|
||||
// Group recent sessions by project
|
||||
const grouped = {};
|
||||
const unassigned = [];
|
||||
for (const session of recentSessions) {
|
||||
if (session.project_id) {
|
||||
if (!grouped[session.project_id]) grouped[session.project_id] = [];
|
||||
grouped[session.project_id].push(session);
|
||||
} else {
|
||||
unassigned.push(session);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionRowProps = (session) => ({
|
||||
key: session.external_id,
|
||||
session,
|
||||
isActive: activeSession?.external_id === session.external_id,
|
||||
isHovered: hoveredId === session.external_id,
|
||||
onHover: setHoveredId,
|
||||
onSelect: () => { onSelectSession(session); onNavigate('chat'); },
|
||||
onRename: () => { setModalMode('settings'); setModalSession(session); },
|
||||
onDelete: () => { setModalMode('confirm-delete'); setModalSession(session); },
|
||||
onContextMenu: e => !session.isNew && openMenu(e, session),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-col" style={{
|
||||
@@ -141,45 +160,45 @@ export default function Sidebar({
|
||||
isOpen={projectsOpen}
|
||||
onToggle={() => setProjectsOpen(o => !o)}
|
||||
/>
|
||||
{projectsOpen && (
|
||||
<div style={{ padding: '4px 10px 8px' }}>
|
||||
{!projects?.length ? (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
border: '1px dashed var(--border)',
|
||||
color: 'var(--text-muted)',
|
||||
fontSize: '12px',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
No projects yet
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
{projects.slice(0, 6).map(project => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => {onSelectProject(project); onNavigate('project')}}
|
||||
className="btn-reset text-xs"
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: 'var(--bg-elevated)',
|
||||
border: `1px solid ${project.colour ?? 'var(--border)'}`,
|
||||
color: 'var(--text-secondary)',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
title={project.description ?? project.name}
|
||||
>
|
||||
<span className="truncate" style={{ display: 'block', maxWidth: '140px' }}>
|
||||
{project.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{projectsOpen && (
|
||||
<div style={{ padding: '4px 10px 8px' }}>
|
||||
{!projects?.length ? (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
border: '1px dashed var(--border)',
|
||||
color: 'var(--text-muted)',
|
||||
fontSize: '12px',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
No projects yet
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
{projects.slice(0, 6).map(project => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => { onSelectProject(project); onNavigate('project'); }}
|
||||
className="btn-reset text-xs"
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: 'var(--bg-elevated)',
|
||||
border: `1px solid ${project.colour ?? 'var(--border)'}`,
|
||||
color: 'var(--text-secondary)',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
title={project.description ?? project.name}
|
||||
>
|
||||
<span className="truncate" style={{ display: 'block', maxWidth: '140px' }}>
|
||||
{project.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ height: '1px', background: 'var(--border)', margin: '2px 0' }} />
|
||||
|
||||
@@ -189,28 +208,54 @@ export default function Sidebar({
|
||||
isOpen={chatsOpen}
|
||||
onToggle={() => setChatsOpen(o => !o)}
|
||||
/>
|
||||
|
||||
{chatsOpen && (
|
||||
<>
|
||||
{recentSessions.map(session => (
|
||||
<SessionRow
|
||||
key={session.external_id}
|
||||
session={session}
|
||||
isActive={activeSession?.external_id === session.external_id}
|
||||
isHovered={hoveredId === session.external_id}
|
||||
onHover={setHoveredId}
|
||||
onSelect={() => { onSelectSession(session); onNavigate('chat'); }}
|
||||
onRename={() => { setModalMode('settings'); setModalSession(session); }}
|
||||
onDelete={() => { setModalMode('confirm-delete'); setModalSession(session); }}
|
||||
onContextMenu={e => !session.isNew && openMenu(e, session)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{recentSessions.length === 0 && (
|
||||
<div className="text-xs text-muted" style={{ padding: '12px 16px', textAlign: 'center' }}>
|
||||
No conversations yet
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project groups */}
|
||||
{Object.entries(grouped).map(([projectId, projectSessions]) => {
|
||||
const project = projects?.find(p => p.id === Number(projectId));
|
||||
return (
|
||||
<div key={projectId}>
|
||||
{/* Project group label */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: '6px',
|
||||
padding: '6px 16px 2px',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '6px', height: '6px', borderRadius: '50%', flexShrink: 0,
|
||||
background: project?.colour ?? 'var(--accent)',
|
||||
}} />
|
||||
<span className="text-xs text-muted truncate">
|
||||
{project?.name ?? 'Project'}
|
||||
</span>
|
||||
</div>
|
||||
{projectSessions.map(session => (
|
||||
<SessionRow {...sessionRowProps(session)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Unassigned sessions */}
|
||||
{unassigned.length > 0 && (
|
||||
<>
|
||||
{Object.keys(grouped).length > 0 && (
|
||||
<div style={{ padding: '6px 16px 2px' }}>
|
||||
<span className="text-xs text-muted">Other</span>
|
||||
</div>
|
||||
)}
|
||||
{unassigned.map(session => (
|
||||
<SessionRow {...sessionRowProps(session)} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{sessions.length > 0 && (
|
||||
<button
|
||||
onClick={() => onNavigate('all-chats')}
|
||||
|
||||
@@ -14,10 +14,6 @@ async function searchEpisodes( vector, {limit = ORCHESTRATION.RECENT_EPISODE_LIM
|
||||
} else if (sessionId) {
|
||||
body.filter = { must: [{key: 'sessionId', match: {value: sessionId} }] };
|
||||
}
|
||||
|
||||
console.log('[qdrant] searchEpisodes filter:', JSON.stringify(body.filter));
|
||||
console.log('[qdrant] projectSessionIds:', projectSessionIds);
|
||||
|
||||
const res = await fetch (
|
||||
`${BASE_URL}/collections/${COLLECTIONS.EPISODES}/points/search`,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user