added being able to assign sessions to projects via the sessions modal
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user