feat(frontend): sessions list/create + steering + resume + selectors
- NavSessions: left nav listing sessions via listSessions (TanStack Query), clicking resumes via resumeSession and sets active; active item highlighted - NewSessionDialog: ShadCn dialog with question textarea, workspace native select (from listWorkspaces), graceful model field (dropdown when models list is non-empty, free-text input when empty per §9 degradation), optional thinking/provider fields; try/catch surfaces errors inline - SteerInput: text input + button POSTs to postSteer, clears on send, supports Enter key; only shown when a session is active - WorkflowBar: renders 8 phases (F1..F8) with data-active highlighting driven by useSessionStore.currentPhase (new store field + setPhase action) - AppShell: wires all four components; right artifact sidebar now collapsible via a toggle button (useState sidebarOpen); replaces old "Nuova domanda" button with NewSessionDialog trigger - f1-loop.test: updated to drive through the new dialog flow (open → fill question → submit → wait for SSE → respond) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
// frontend/src/shell/NewSessionDialog.tsx
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../components/ui/dialog";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { createSession } from "../api/sessions";
|
||||
import { listWorkspaces } from "../api/workspaces";
|
||||
import { listModels } from "../api/models";
|
||||
|
||||
interface Props {
|
||||
onCreated: (id: string) => void;
|
||||
}
|
||||
|
||||
export function NewSessionDialog({ onCreated }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [question, setQuestion] = useState("");
|
||||
const [workspace, setWorkspace] = useState("");
|
||||
const [model, setModel] = useState("");
|
||||
const [thinking, setThinking] = useState("");
|
||||
const [provider, setProvider] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const { data: workspaces = [] } = useQuery({
|
||||
queryKey: ["workspaces"],
|
||||
queryFn: listWorkspaces,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const { data: modelsData } = useQuery({
|
||||
queryKey: ["models"],
|
||||
queryFn: listModels,
|
||||
enabled: open,
|
||||
});
|
||||
|
||||
const modelList: string[] = Array.isArray(modelsData?.models)
|
||||
? (modelsData.models as string[])
|
||||
: [];
|
||||
|
||||
// When workspace list loads, pre-select the first one if nothing chosen yet
|
||||
const effectiveWorkspace =
|
||||
workspace || (workspaces.length > 0 ? workspaces[0].name : "default");
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
if (!question.trim()) {
|
||||
setError("La domanda non può essere vuota.");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const { id } = await createSession({
|
||||
workspace: effectiveWorkspace,
|
||||
question: question.trim(),
|
||||
...(model ? { model } : {}),
|
||||
...(thinking ? { thinking } : {}),
|
||||
...(provider ? { provider } : {}),
|
||||
});
|
||||
setOpen(false);
|
||||
setQuestion("");
|
||||
setModel("");
|
||||
setThinking("");
|
||||
setProvider("");
|
||||
onCreated(id);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore nella creazione della sessione.");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Nuova domanda
|
||||
</Button>
|
||||
<DialogContent showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nuova sessione</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1" htmlFor="nsd-question">
|
||||
Domanda
|
||||
</label>
|
||||
<textarea
|
||||
id="nsd-question"
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none focus:ring-1 resize-none"
|
||||
placeholder="Inserisci la tua domanda…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1" htmlFor="nsd-workspace">
|
||||
Workspace
|
||||
</label>
|
||||
<select
|
||||
id="nsd-workspace"
|
||||
value={workspace}
|
||||
onChange={(e) => setWorkspace(e.target.value)}
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
{workspaces.length === 0 ? (
|
||||
<option value="default">default</option>
|
||||
) : (
|
||||
workspaces.map((w) => (
|
||||
<option key={w.name} value={w.name}>
|
||||
{w.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Model field — graceful degradation (§9): free text when models list is empty */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1" htmlFor="nsd-model">
|
||||
Modello (opzionale)
|
||||
</label>
|
||||
{modelList.length > 0 ? (
|
||||
<select
|
||||
id="nsd-model"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
className="w-full border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="">— default —</option>
|
||||
{modelList.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
id="nsd-model"
|
||||
type="text"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder="es. claude-sonnet-4-5"
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none focus:ring-1"
|
||||
data-testid="model-freetext"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1" htmlFor="nsd-thinking">
|
||||
Thinking (opzionale)
|
||||
</label>
|
||||
<input
|
||||
id="nsd-thinking"
|
||||
type="text"
|
||||
value={thinking}
|
||||
onChange={(e) => setThinking(e.target.value)}
|
||||
placeholder="enabled / disabled"
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none focus:ring-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium mb-1" htmlFor="nsd-provider">
|
||||
Provider (opzionale)
|
||||
</label>
|
||||
<input
|
||||
id="nsd-provider"
|
||||
type="text"
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
placeholder="es. anthropic"
|
||||
className="w-full border rounded px-2 py-1 text-sm focus:outline-none focus:ring-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Annulla
|
||||
</Button>
|
||||
<Button type="submit" size="sm" disabled={busy}>
|
||||
{busy ? "Creazione…" : "Crea"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user