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:
2026-06-28 13:06:01 +02:00
co-authored by Claude Sonnet 4.6
parent 3ecfed5d3d
commit 544882f95b
9 changed files with 547 additions and 22 deletions
+212
View File
@@ -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>
);
}