added being able to assign sessions to projects via the sessions modal

This commit is contained in:
Storme-bit
2026-04-13 20:36:42 -07:00
parent e3f6b9a9db
commit 649ed2b350
8 changed files with 163 additions and 121 deletions

View File

@@ -143,7 +143,7 @@ export async function fetchModels() {
if(!res.ok) throw new Error(`Failted to fetch models: ${res.status}`); if(!res.ok) throw new Error(`Failted to fetch models: ${res.status}`);
return res.json(); return res.json();
} }
/*
export async function renameSession(sessionId, name) { export async function renameSession(sessionId, name) {
const res = await fetch(`${BASE_URL}/sessions/${sessionId}`, { const res = await fetch(`${BASE_URL}/sessions/${sessionId}`, {
method: 'PATCH', method: 'PATCH',
@@ -153,6 +153,20 @@ export async function renameSession(sessionId, name) {
if (!res.ok) throw new Error(`Failed to rename session: ${res.status}`); if (!res.ok) throw new Error(`Failed to rename session: ${res.status}`);
return res.json(); return res.json();
} }
*/
export async function updateSession(sessionId, { name, projectId } = {}) {
const res = await fetch(`${BASE_URL}/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, projectId }),
});
if (!res.ok) throw new Error(`Failed to update session: ${res.status}`);
return res.json();
}
export async function renameSession(sessionId, name) {
return updateSession(sessionId, {name})
}
export async function deleteSession(sessionId) { export async function deleteSession(sessionId) {
const res = await fetch(`${BASE_URL}/sessions/${sessionId}`, { const res = await fetch(`${BASE_URL}/sessions/${sessionId}`, {
@@ -191,3 +205,13 @@ export async function deleteProject(id) {
const res = await fetch(`${BASE_URL}/projects/${id}`, { method: 'DELETE' }); const res = await fetch(`${BASE_URL}/projects/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Failed to delete project: ${res.status}`); if (!res.ok) throw new Error(`Failed to delete project: ${res.status}`);
} }
export async function updateSessionProject(sessionId, projectId) {
const res = await fetch(`${BASE_URL}/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId }),
});
if (!res.ok) throw new Error(`Failed to update session project: ${res.status}`);
return res.json();
}

View File

@@ -1,116 +1,128 @@
// SessionModal.jsx import React, { useState, useEffect, useRef } from 'react';
import React, {useState, useEffect, useRef} from 'react'; import { updateSession } from '../api/orchestration';
export default function SessionModal({ session, mode = 'settings', onRename, onDelete, onClose }) { export default function SessionModal({ session, mode = 'settings', onRename, onDelete, onClose, projects = [] }) {
const [name, setName] = useState(session?.name || ''); const [name, setName] = useState(session?.name || '');
const inputRef = useRef(null); const [projectId, setProjectId] = useState(session?.project_id ?? '');
const inputRef = useRef(null);
useEffect(() => { useEffect(() => {
if (mode === 'settings') { if (mode === 'settings') {
inputRef.current?.focus(); inputRef.current?.focus();
inputRef.current?.select(); inputRef.current?.select();
}
}, [mode]);
function handleSubmit() {
const trimmed = name.trim();
if (!trimmed) return;
onRename(session, trimmed);
onClose();
} }
}, [mode]);
function handleKeyDown(e) { function handleSubmit() {
if (e.key === 'Enter' && mode === 'settings') handleSubmit(); const trimmed = name.trim();
if (e.key === 'Escape') onClose(); if (!trimmed) return;
} onRename(session, trimmed, projectId || null);
onClose();
}
if (!session) return null; function handleKeyDown(e) {
if (e.key === 'Enter' && mode === 'settings') handleSubmit();
if (e.key === 'Escape') onClose();
}
return ( if (!session) return null;
<div onClick={onClose} style={{
position: 'fixed', return (
inset: 0, <div onClick={onClose} style={{
background: 'rgba(0,0,0,0.5)', position: 'fixed', inset: 0,
display: 'flex', background: 'rgba(0,0,0,0.5)',
alignItems: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
justifyContent: 'center', zIndex: 100,
zIndex: 100, }}>
}}> <div onClick={e => e.stopPropagation()} onKeyDown={handleKeyDown} style={{
<div onClick={e => e.stopPropagation()} onKeyDown={handleKeyDown} style={{ background: 'var(--bg-surface)',
background: 'var(--bg-surface)', border: '1px solid var(--border)',
border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)',
borderRadius: 'var(--radius-lg)', padding: '24px', width: '360px',
padding: '24px', display: 'flex', flexDirection: 'column', gap: '16px',
width: '360px', }}>
display: 'flex', {mode === 'settings' ? (
flexDirection: 'column', <>
gap: '16px', <h2 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)' }}>
}}> Session Settings
{mode === 'settings' ? ( </h2>
<>
<h2 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)' }}> {/* Name */}
Session Settings <div className="flex-col" style={{ gap: '6px' }}>
</h2> <label className="label-upper">Name</label>
<div className="flex-col" style={{ gap: '8px' }}> <input
<label className="label-upper">Name</label> ref={inputRef}
<input value={name}
ref={inputRef} onChange={e => setName(e.target.value)}
value={name} placeholder="Enter session name..."
onChange={e => setName(e.target.value)} style={{
placeholder="Enter session name..." background: 'var(--bg-elevated)', border: '1px solid var(--border)',
style={{ borderRadius: 'var(--radius-md)', padding: '8px 12px',
background: 'var(--bg-elevated)', color: 'var(--text-primary)', fontSize: '14px', outline: 'none', width: '100%',
border: '1px solid var(--border)', }}
borderRadius: 'var(--radius-md)', />
padding: '8px 12px',
color: 'var(--text-primary)',
fontSize: '14px',
outline: 'none',
width: '100%',
}}
/>
</div>
<div className="flex" style={{ gap: '8px', justifyContent: 'flex-end' }}>
<button className="btn-reset text-base text-muted"
onClick={onClose}
style={{ padding: '8px 14px', borderRadius: 'var(--radius-md)' }}
>Cancel</button>
<button className="btn-primary" onClick={handleSubmit}
disabled={!name.trim()}
style={{ padding: '8px 16px' }}
>Save</button>
</div>
</>
) : (
<>
<h2 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)' }}>
Delete Session
</h2>
<p className="text-sm text-secondary">
Are you sure you want to delete{' '}
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>
{session.name || session.external_id}
</span>
? This will permanently remove all messages in this conversation.
</p>
<div className="flex" style={{ gap: '8px', justifyContent: 'flex-end' }}>
<button className="btn-reset text-base text-muted"
onClick={onClose}
style={{ padding: '8px 14px', borderRadius: 'var(--radius-md)' }}
>Cancel</button>
<button className="btn-reset text-base"
onClick={() => { onDelete(session); onClose(); }}
style={{
padding: '8px 16px',
borderRadius: 'var(--radius-md)',
background: '#c0392b',
color: 'white',
}}
>Delete</button>
</div>
</>
)}
</div> </div>
</div>
); {/* Project assignment */}
<div className="flex-col" style={{ gap: '6px' }}>
<label className="label-upper">Project <span style={{ opacity: 0.5 }}>(optional)</span></label>
<select
value={projectId}
onChange={e => setProjectId(e.target.value)}
style={{
width: '100%', padding: '8px 10px',
background: 'var(--bg-elevated)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)', color: 'var(--text-primary)',
fontSize: '13px', cursor: 'pointer', outline: 'none',
}}
>
<option value=''>No project</option>
{projects.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
<div className="flex" style={{ gap: '8px', justifyContent: 'flex-end' }}>
<button className="btn-reset text-base text-muted"
onClick={onClose}
style={{ padding: '8px 14px', borderRadius: 'var(--radius-md)' }}>
Cancel
</button>
<button className="btn-primary" onClick={handleSubmit}
disabled={!name.trim()}
style={{ padding: '8px 16px' }}>
Save
</button>
</div>
</>
) : (
<>
<h2 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)' }}>
Delete Session
</h2>
<p className="text-sm text-secondary">
Are you sure you want to delete{' '}
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>
{session.name || session.external_id}
</span>
? This will permanently remove all messages in this conversation.
</p>
<div className="flex" style={{ gap: '8px', justifyContent: 'flex-end' }}>
<button className="btn-reset text-base text-muted"
onClick={onClose}
style={{ padding: '8px 14px', borderRadius: 'var(--radius-md)' }}>
Cancel
</button>
<button className="btn-reset text-base"
onClick={() => { onDelete(session); onClose(); }}
style={{ padding: '8px 16px', borderRadius: 'var(--radius-md)', background: '#c0392b', color: 'white' }}>
Delete
</button>
</div>
</>
)}
</div>
</div>
);
} }

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import SessionModal from './SessionModal'; import SessionModal from './SessionModal';
import { useContextMenu } from '../hooks/useContextMenu'; import { useContextMenu } from '../hooks/useContextMenu';
import { renameSession, deleteSession } from '../api/orchestration'; import { renameSession, deleteSession, updateSession } from '../api/orchestration';
export default function Sidebar({ export default function Sidebar({
@@ -26,9 +26,9 @@ export default function Sidebar({
// ── Handlers ──────────────────────────────────────────── // ── Handlers ────────────────────────────────────────────
async function handleRename(session, name) { async function handleRename(session, name, projectId) {
try { try {
await renameSession(session.external_id, name); await updateSession(session.external_id, { name, projectId });
onSessionsChange(); onSessionsChange();
} catch (err) { } catch (err) {
console.error('[Sidebar] Rename failed:', err.message); console.error('[Sidebar] Rename failed:', err.message);
@@ -273,6 +273,7 @@ export default function Sidebar({
onRename={handleRename} onRename={handleRename}
onDelete={handleDelete} onDelete={handleDelete}
onClose={() => setModalSession(null)} onClose={() => setModalSession(null)}
projects={projects}
/> />
)} )}
</> </>

View File

@@ -53,6 +53,9 @@ export function useChat({ activeSession, appendMessage, updateLastMessage, refre
// Refresh session list so new sessions appear in sidebar // Refresh session list so new sessions appear in sidebar
refreshSessions(); refreshSessions();
// Delayed refresh
setTimeout( () => refreshSessions(), 3000);
}, },
onError: (err) => { onError: (err) => {

View File

@@ -52,13 +52,15 @@ function deleteSession(id) {
db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id); db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
} }
function updateSession(id, { name } = {}){ function updateSession(id, { name, projectId } = {}) {
const db = getDB(); const db = getDB();
db.prepare(` db.prepare(`
UPDATE sessions UPDATE sessions
SET name = ?, updated_at = unixepoch() SET name = ?,
project_id = COALESCE(?, project_id),
updated_at = unixepoch()
WHERE id = ? WHERE id = ?
`).run(name ?? null, id); `).run(name ?? null, projectId ?? null, id);
return getSession(id); return getSession(id);
} }

View File

@@ -66,9 +66,9 @@ app.get('/sessions/:id', (req, res) => {
}); });
app.patch('/sessions/by-external/:externalId', (req, res) => { app.patch('/sessions/by-external/:externalId', (req, res) => {
const { name } = req.body; const { name, projectId } = req.body;
try { try {
const session = episodic.updateSessionByExternalId(req.params.externalId, {name }); const session = episodic.updateSessionByExternalId(req.params.externalId, {name, projectId });
res.json(session); res.json(session);
} catch (err) { } catch (err) {
res.status(500).json({error: err.message }); res.status(500).json({error: err.message });

View File

@@ -30,11 +30,11 @@ router.get('/', async (req, res) => {
}) })
router.patch('/:sessionId', async (req, res) => { router.patch('/:sessionId', async (req, res) => {
const { name } = req.body; const { name, projectId } = req.body;
if (!name?.trim()) return res.status(400).json({ error: 'name is required' }); if (!name?.trim()) return res.status(400).json({ error: 'name is required' });
try { try {
const session = await memory.updateSession(req.params.sessionId, { name: name.trim() }); const session = await memory.updateSession(req.params.sessionId, { name, projectId });
res.json(session); res.json(session);
} catch (err) { } catch (err) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });

View File

@@ -63,11 +63,11 @@ async function getSessions(limit = EPISODIC.DEFAULT_SESSIONS_LIMIT, offset = EPI
return res.json(); return res.json();
} }
async function updateSession(externalId, { name }) { async function updateSession(externalId, { name, projectId }) {
const res = await fetch(`${BASE_URL}/sessions/by-external/${externalId}`, { const res = await fetch(`${BASE_URL}/sessions/by-external/${externalId}`, {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }), body: JSON.stringify({ name, projectId }),
}); });
if (!res.ok) throw new Error(`Failed to update session: ${res.status}`); if (!res.ok) throw new Error(`Failed to update session: ${res.status}`);
return res.json(); return res.json();