feat(frontend): NewSessionDialog reduced to question-only
Removes workspace/model/provider/thinking fields. Dialog now posts only { question } to createSession and calls onCreated on success.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,52 +17,19 @@ function renderDialog() {
|
|||||||
return { onCreated };
|
return { onCreated };
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
test("the form has only a question field (no workspace/model/provider/thinking)", async () => {
|
||||||
server.use(
|
|
||||||
http.get("http://localhost:8787/workspaces", () =>
|
|
||||||
HttpResponse.json([{ name: "default", file: "default.db" }]),
|
|
||||||
),
|
|
||||||
http.post("http://localhost:8787/sessions", () =>
|
|
||||||
HttpResponse.json({ id: "s1" }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("§9 degradation: empty models -> model field is a FREE-TEXT input, not a select", async () => {
|
|
||||||
server.use(
|
|
||||||
http.get("http://localhost:8787/models", () => HttpResponse.json({ models: [] })),
|
|
||||||
);
|
|
||||||
renderDialog();
|
renderDialog();
|
||||||
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
|
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
|
||||||
|
expect(await screen.findByLabelText(/domanda/i)).toBeInTheDocument();
|
||||||
// Free-text model input present; no model dropdown.
|
expect(screen.queryByLabelText(/workspace/i)).not.toBeInTheDocument();
|
||||||
const freetext = await screen.findByTestId("model-freetext");
|
expect(screen.queryByLabelText(/modello/i)).not.toBeInTheDocument();
|
||||||
expect(freetext.tagName).toBe("INPUT");
|
expect(screen.queryByLabelText(/provider/i)).not.toBeInTheDocument();
|
||||||
// The only <select> in the dialog is the workspace one (id nsd-workspace).
|
expect(screen.queryByLabelText(/thinking/i)).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("combobox", { name: /modello/i })).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("§9: non-empty models -> model field is a dropdown (select), not free text", async () => {
|
test("submitting posts only { question } and calls onCreated", async () => {
|
||||||
server.use(
|
|
||||||
http.get("http://localhost:8787/models", () =>
|
|
||||||
HttpResponse.json({ models: [{ provider: "zai", id: "glm-5.2" }] }),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
renderDialog();
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
|
|
||||||
|
|
||||||
// Model dropdown present; no free-text input.
|
|
||||||
const modelSelect = await screen.findByRole("combobox", { name: /modello/i });
|
|
||||||
expect(modelSelect.tagName).toBe("SELECT");
|
|
||||||
expect(screen.queryByTestId("model-freetext")).not.toBeInTheDocument();
|
|
||||||
// Option label combines provider/id.
|
|
||||||
expect(screen.getByRole("option", { name: "zai/glm-5.2" })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("submitting calls createSession with the expected body keys", async () => {
|
|
||||||
let body: unknown = null;
|
let body: unknown = null;
|
||||||
server.use(
|
server.use(
|
||||||
http.get("http://localhost:8787/models", () => HttpResponse.json({ models: [] })),
|
|
||||||
http.post("http://localhost:8787/sessions", async ({ request }) => {
|
http.post("http://localhost:8787/sessions", async ({ request }) => {
|
||||||
body = await request.json();
|
body = await request.json();
|
||||||
return HttpResponse.json({ id: "s1" });
|
return HttpResponse.json({ id: "s1" });
|
||||||
@@ -70,13 +37,16 @@ test("submitting calls createSession with the expected body keys", async () => {
|
|||||||
);
|
);
|
||||||
const { onCreated } = renderDialog();
|
const { onCreated } = renderDialog();
|
||||||
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
|
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
|
||||||
|
await userEvent.type(await screen.findByLabelText(/domanda/i), "Quante vendite nel 2025?");
|
||||||
const textarea = await screen.findByLabelText(/domanda/i);
|
|
||||||
await userEvent.type(textarea, "Quante vendite nel 2025?");
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: /^crea$/i }));
|
await userEvent.click(screen.getByRole("button", { name: /^crea$/i }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => expect(body).toEqual({ question: "Quante vendite nel 2025?" }));
|
||||||
expect(body).toEqual({ workspace: "default", question: "Quante vendite nel 2025?" });
|
|
||||||
});
|
|
||||||
await waitFor(() => expect(onCreated).toHaveBeenCalledWith("s1"));
|
await waitFor(() => expect(onCreated).toHaveBeenCalledWith("s1"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("empty question shows a validation error and does not submit", async () => {
|
||||||
|
renderDialog();
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: /^crea$/i }));
|
||||||
|
expect(await screen.findByRole("alert")).toHaveTextContent(/vuota/i);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// frontend/src/shell/NewSessionDialog.tsx
|
// frontend/src/shell/NewSessionDialog.tsx
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -9,8 +8,6 @@ import {
|
|||||||
} from "../components/ui/dialog";
|
} from "../components/ui/dialog";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { createSession } from "../api/sessions";
|
import { createSession } from "../api/sessions";
|
||||||
import { listWorkspaces } from "../api/workspaces";
|
|
||||||
import { listModels } from "../api/models";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreated: (id: string) => void;
|
onCreated: (id: string) => void;
|
||||||
@@ -19,43 +16,9 @@ interface Props {
|
|||||||
export function NewSessionDialog({ onCreated }: Props) {
|
export function NewSessionDialog({ onCreated }: Props) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [question, setQuestion] = useState("");
|
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 [error, setError] = useState<string | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Model entries may be plain strings or objects ({ id, provider, ... });
|
|
||||||
// normalize to { value, label } so the dropdown renders either shape.
|
|
||||||
const modelList: { value: string; label: string }[] = Array.isArray(
|
|
||||||
modelsData?.models,
|
|
||||||
)
|
|
||||||
? modelsData.models.map((m) => {
|
|
||||||
if (typeof m === "string") return { value: m, label: m };
|
|
||||||
const o = m as { id?: string; provider?: string };
|
|
||||||
const value = o.id ?? "";
|
|
||||||
const label = o.provider ? `${o.provider}/${value}` : value;
|
|
||||||
return { value, label };
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// 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) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -65,18 +28,9 @@ export function NewSessionDialog({ onCreated }: Props) {
|
|||||||
}
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const { id } = await createSession({
|
const { id } = await createSession({ question: question.trim() });
|
||||||
workspace: effectiveWorkspace,
|
|
||||||
question: question.trim(),
|
|
||||||
...(model ? { model } : {}),
|
|
||||||
...(thinking ? { thinking } : {}),
|
|
||||||
...(provider ? { provider } : {}),
|
|
||||||
});
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setQuestion("");
|
setQuestion("");
|
||||||
setModel("");
|
|
||||||
setThinking("");
|
|
||||||
setProvider("");
|
|
||||||
onCreated(id);
|
onCreated(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Errore nella creazione della sessione.");
|
setError(err instanceof Error ? err.message : "Errore nella creazione della sessione.");
|
||||||
@@ -114,88 +68,6 @@ export function NewSessionDialog({ onCreated }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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} value={m.value}>
|
|
||||||
{m.label}
|
|
||||||
</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 && (
|
{error && (
|
||||||
<p className="text-xs text-destructive" role="alert">
|
<p className="text-xs text-destructive" role="alert">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
Reference in New Issue
Block a user