Colors (GSD palette) untouched. New visual vocabulary across the whole GUI: - warm-tinted layered shadow tokens (--shadow-xs/sm/md) replacing flat rings; cards/dialogs framed with hairline border + real elevation - strict radii scale: xl cards / lg inputs / md controls / 2xl dialogs; buttons move from pills to rounded-md - third type register: --font-mono + tabular-nums on SQL, timers, dates, ids; standardized .thot-label micro-label tier - prose: framed tables with uppercase headers, elevated code blocks - SqlViewer badges moved from hardcoded Tailwind greens/reds to success/ destructive token tints; ResultsPanel inline styles replaced with classes - artifact gate: first option rendered as filled primary action Verified offline via Playwright against a mock backend replaying a real psd session (no VPN needed). tsc clean, 115/115 vitest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
// frontend/src/shell/NewSessionDialog.tsx
|
|
import { useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "../components/ui/dialog";
|
|
import { Button } from "../components/ui/button";
|
|
import { createSession } from "../api/sessions";
|
|
|
|
interface Props {
|
|
onCreated: (id: string) => void;
|
|
}
|
|
|
|
export function NewSessionDialog({ onCreated }: Props) {
|
|
const [open, setOpen] = useState(false);
|
|
const [question, setQuestion] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
if (!question.trim()) {
|
|
setError("The question cannot be empty.");
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
const { id } = await createSession({ question: question.trim() });
|
|
setOpen(false);
|
|
setQuestion("");
|
|
onCreated(id);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to create the session.");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={() => setOpen(true)}
|
|
>
|
|
New session
|
|
</Button>
|
|
<DialogContent showCloseButton={false}>
|
|
<DialogHeader>
|
|
<DialogTitle>New session</DialogTitle>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
<div>
|
|
<label className="mb-1.5 block text-xs font-medium text-muted-foreground" htmlFor="nsd-question">
|
|
Question
|
|
</label>
|
|
<textarea
|
|
id="nsd-question"
|
|
value={question}
|
|
onChange={(e) => setQuestion(e.target.value)}
|
|
rows={3}
|
|
className="w-full resize-none rounded-lg border border-input bg-card px-3 py-2 text-sm shadow-xs outline-none transition-[border-color,box-shadow] placeholder:text-muted-foreground focus:border-primary/50 focus:ring-3 focus:ring-ring/15"
|
|
placeholder="Enter your question…"
|
|
/>
|
|
</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)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" size="sm" disabled={busy}>
|
|
{busy ? "Creating…" : "Create"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|