TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Braces } from "lucide-react";4import { ResponsiveDialog } from "@/components/ui/sheet";5import { Button } from "@/components/ui/button";6import { Textarea } from "@/components/ui/input";7import { Segmented } from "@/components/ui/segmented";8import { Switch } from "@/components/ui/switch";9import type { ChatSettings } from "./model-config";10import { cn } from "@/lib/utils";1112type FormatMode = "text" | "json" | "json_schema";1314const DEFAULT_SCHEMA = '{\n "type": "object",\n "properties": {\n "answer": { "type": "string" }\n },\n "required": ["answer"],\n "additionalProperties": false\n}';1516/** Structured output: JSON object / JSON schema (with an editor) — only offered when the model supports it. */17export function StructuredOutputSheet({ open, onOpenChange, settings, onChange }: { open: boolean; onOpenChange: (o: boolean) => void; settings: ChatSettings; onChange: (s: ChatSettings) => void }) {18 const [mode, setMode] = React.useState<FormatMode>(settings.responseFormat?.type ?? "text");19 const [schemaText, setSchemaText] = React.useState(() => (settings.responseFormat?.schema ? JSON.stringify(settings.responseFormat.schema, null, 2) : DEFAULT_SCHEMA));20 const [error, setError] = React.useState<string | null>(null);2122 React.useEffect(() => {23 if (open) {24 // eslint-disable-next-line react-hooks/set-state-in-effect25 setMode(settings.responseFormat?.type ?? "text");26 setSchemaText(settings.responseFormat?.schema ? JSON.stringify(settings.responseFormat.schema, null, 2) : DEFAULT_SCHEMA);27 setError(null);28 }29 }, [open, settings.responseFormat]);3031 const apply = () => {32 const next = { ...settings };33 if (mode === "text") {34 delete next.responseFormat;35 } else if (mode === "json") {36 next.responseFormat = { type: "json" };37 } else {38 try {39 const parsed = JSON.parse(schemaText);40 if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Schema must be a JSON object");41 next.responseFormat = { type: "json_schema", schema: parsed, strict: true };42 } catch (e) {43 setError((e as Error).message);44 return;45 }46 }47 onChange(next);48 onOpenChange(false);49 };5051 return (52 <ResponsiveDialog53 open={open}54 onOpenChange={onOpenChange}55 title={56 <span className="inline-flex items-center gap-2">57 <Braces className="size-4" /> Structured output58 </span>59 }60 description="Ask the model to answer with JSON. Strict schema mode validates the shape when the provider supports it."61 size="md"62 footer={63 <div className="flex gap-2 sm:justify-end">64 <Button variant="ghost" className="flex-1 sm:flex-none" onClick={() => onOpenChange(false)}>65 Cancel66 </Button>67 <Button className="flex-1 sm:flex-none" onClick={apply}>68 Apply69 </Button>70 </div>71 }72 >73 <div className="space-y-4 pt-1">74 <Segmented<FormatMode> value={mode} onChange={setMode} fill ariaLabel="Response format" options={[{ value: "text", label: "Text" }, { value: "json", label: "JSON object" }, { value: "json_schema", label: "JSON schema" }]} />75 {mode === "json_schema" ? (76 <div className="space-y-1.5">77 <Textarea78 value={schemaText}79 onChange={(e) => {80 setSchemaText(e.target.value);81 try {82 JSON.parse(e.target.value);83 setError(null);84 } catch (err) {85 setError((err as Error).message);86 }87 }}88 className={cn("min-h-[220px] font-mono text-[12.5px] leading-5", error && "border-danger focus:border-danger")}89 spellCheck={false}90 aria-label="JSON schema"91 />92 {error ? <p className="text-xs text-danger">{error}</p> : <p className="text-xs text-fg-subtle">Object root; keep `additionalProperties: false` for strict mode.</p>}93 </div>94 ) : mode === "json" ? (95 <p className="text-[13px] text-fg-muted">The model returns a single JSON object. Mention the keys you expect in your prompt.</p>96 ) : (97 <p className="text-[13px] text-fg-muted">Plain Markdown answers (default).</p>98 )}99 </div>100 </ResponsiveDialog>101 );102}103104/** System prompt editor (conversation-level instructions). */105export function SystemPromptSheet({ open, onOpenChange, value, onChange, projectInstructions }: { open: boolean; onOpenChange: (o: boolean) => void; value: string; onChange: (v: string) => void; projectInstructions?: string | null }) {106 const [draft, setDraft] = React.useState(value);107 React.useEffect(() => {108 // eslint-disable-next-line react-hooks/set-state-in-effect109 if (open) setDraft(value);110 }, [open, value]);111 return (112 <ResponsiveDialog113 open={open}114 onOpenChange={onOpenChange}115 title="System prompt"116 description="Instructions that apply to the whole conversation. They are sent before every message."117 size="md"118 snap="half"119 footer={120 <div className="flex items-center gap-2">121 <span className="text-[12px] tabular-nums text-fg-subtle">{draft.length.toLocaleString()} chars</span>122 <div className="flex-1" />123 <Button variant="ghost" onClick={() => onOpenChange(false)}>124 Cancel125 </Button>126 <Button127 onClick={() => {128 onChange(draft);129 onOpenChange(false);130 }}131 >132 Save133 </Button>134 </div>135 }136 >137 <div className="space-y-3 pt-1">138 {projectInstructions ? (139 <div className="rounded-lg bg-bg-subtle px-3 py-2 text-[12.5px] text-fg-muted">140 <span className="font-medium text-fg">Project instructions</span> are prepended automatically:141 <p className="mt-1 line-clamp-3 whitespace-pre-wrap">{projectInstructions}</p>142 </div>143 ) : null}144 <Textarea value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="You are a concise assistant that answers in French…" className="min-h-[180px]" autoFocus />145 </div>146 </ResponsiveDialog>147 );148}149150const BUILTIN_TOOLS: { id: string; label: string; description: string }[] = [151 { id: "calculator", label: "Calculator", description: "Exact arithmetic for any non-trivial math." },152 { id: "clock", label: "Clock", description: "Current date and time in any time zone." },153 { id: "random", label: "Random", description: "Cryptographically secure random integers." },154];155156/** Built-in PolyLLM tools (run server-side, side-effect free) — toggles mirror `settings.tools`. */157export function ToolsSheet({ open, onOpenChange, settings, onChange, supported }: { open: boolean; onOpenChange: (o: boolean) => void; settings: ChatSettings; onChange: (s: ChatSettings) => void; supported: boolean }) {158 const on = new Set(settings.tools ?? []);159 const toggle = (id: string) => {160 const next = new Set(on);161 if (next.has(id)) next.delete(id);162 else next.add(id);163 const tools = [...next];164 const out = { ...settings };165 if (tools.length) out.tools = tools;166 else {167 delete out.tools;168 delete out.toolChoice;169 }170 onChange(out);171 };172 return (173 <ResponsiveDialog open={open} onOpenChange={onOpenChange} title="Tools" description={supported ? "Built-in PolyLLM tools the model may call. They run server-side and have no side effects." : "This model does not support tool calling."} size="sm">174 <ul className="divide-y divide-hairline pt-1">175 {BUILTIN_TOOLS.map((t) => (176 <li key={t.id} className="flex min-h-[52px] items-center gap-3 py-2">177 <span className="min-w-0 flex-1">178 <span className="block text-[14px] font-medium">{t.label}</span>179 <span className="block text-[12px] text-fg-muted">{t.description}</span>180 </span>181 <Switch checked={on.has(t.id)} onCheckedChange={() => toggle(t.id)} disabled={!supported} aria-label={`Enable ${t.label}`} />182 </li>183 ))}184 </ul>185 </ResponsiveDialog>186 );187}188