A preferences screen that picks the model, reasoning level and tools, and manages persistent memory.
@scrimui
src/showcase/patterns/model-preferences/model-preferences.tsx
"use client";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import * as React from "react";
import { ModelSelector, type ModelOption } from "@/components/ui/model-selector";
import {
ReasoningLevel,
type ReasoningLevel as ReasoningLevelValue,
} from "@/components/ui/reasoning-level";
import { ToolToggle, type ToolSetting } from "@/components/ui/tool-toggle";
import { MemoryList, type MemoryItem } from "@/components/ui/memory-list";
/* ------------------------------------------------------------------ */
/* Data */
/* ------------------------------------------------------------------ */
const MODELS: ModelOption[] = [
{ id: "sonnet", name: "Claude Sonnet 5", hint: "Balanced speed and quality", badges: ["Default"], icon: <ClaudeMark /> },
{ id: "opus", name: "Claude Opus 5", hint: "Best for hard problems", badges: ["Deep thinking"], icon: <ClaudeMark /> },
{ id: "haiku", name: "Claude Haiku 4.5", hint: "Fastest responses", badges: ["Fast"], icon: <ClaudeMark /> },
];
function initTools(): ToolSetting[] {
return [
{
id: "search",
name: "Web search",
description: "Find current information online",
enabled: true,
icon: <SearchIcon />,
},
{
id: "code",
name: "Code execution",
description: "Run code to verify answers",
enabled: true,
icon: <CodeIcon />,
},
{
id: "files",
name: "File access",
description: "Read and edit attached files",
enabled: false,
icon: <FileIcon />,
},
{
id: "browser",
name: "Browser",
description: "Open pages from the conversation",
enabled: false,
icon: <GlobeIcon />,
},
];
}
const INITIAL_MEMORIES: MemoryItem[] = [
{
id: "m1",
text: "Builds AI chat interfaces in TypeScript with Tailwind and zero UI dependencies.",
updatedAt: "2d ago",
},
{ id: "m2", text: "Prefers zinc-based palettes and compact, dependency-free components.", updatedAt: "5d ago" },
{ id: "m3", text: "Shipping a voice assistant pattern this week.", updatedAt: "Today" },
];
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
/**
* The provider's mark, inlined so this pattern stays dependency-free. Swap it
* for whichever providers your own model list uses.
*/
function ClaudeMark() {
return (
<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden className="shrink-0 fill-[#d97757]">
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
</svg>
);
}
function SearchIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="15" height="15">
<circle cx="11" cy="11" r="7" />
<path d="m21 21-4.3-4.3" />
</svg>
);
}
function CodeIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="15" height="15">
<path d="m8 7-5 5 5 5" />
<path d="m16 7 5 5-5 5" />
</svg>
);
}
function FileIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="15" height="15">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2Z" />
<path d="M14 2v6h6" />
</svg>
);
}
function GlobeIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="15" height="15">
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 0 20 15.3 15.3 0 0 1 0-20Z" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* ModelPreferencesPattern */
/* ------------------------------------------------------------------ */
export function ModelPreferencesPattern() {
const [model, setModel] = React.useState("sonnet");
const [level, setLevel] = React.useState<ReasoningLevelValue>("balanced");
const [tools, setTools] = React.useState<ToolSetting[]>(initTools);
const [memories, setMemories] = React.useState<MemoryItem[]>(INITIAL_MEMORIES);
const idRef = React.useRef(4);
function toggleTool(id: string, enabled: boolean) {
setTools((ts) => ts.map((t) => (t.id === id ? { ...t, enabled } : t)));
}
function addMemory(text: string) {
setMemories((m) => [...m, { id: `m${idRef.current++}`, text, updatedAt: "Just now" }]);
}
function forgetMemory(id: string) {
setMemories((m) => m.filter((item) => item.id !== id));
}
return (
<Card className="gap-0 py-0 flex h-[560px] flex-col overflow-hidden rounded-xl border border-border bg-card">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-5 py-3.5">
<div>
<p className="text-sm font-semibold text-foreground">
Model & Memory Preferences
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
How the assistant thinks, which tools it may use, and what it remembers.
</p>
</div>
<Badge variant="secondary" className="inline-flex items-center gap-1.5 rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-400">
<span className="h-1.5 w-1.5 rounded-full bg-current" />
Saved
</Badge>
</div>
<div className="flex min-h-0 flex-1">
{/* Settings */}
<div className="flex-1 space-y-6 overflow-y-auto p-5">
<section className="space-y-2">
<div>
<p className="text-sm font-medium text-foreground">Default model</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Used for new conversations; you can still switch per message.
</p>
</div>
<ModelSelector options={MODELS} value={model} onSelect={setModel} />
</section>
<ReasoningLevel value={level} onChange={setLevel} />
<ToolToggle
tools={tools}
onToggle={toggleTool}
title="Allowed tools"
description="What the assistant may do on your behalf"
/>
</div>
{/* Memory */}
<div className="hidden w-80 shrink-0 flex-col overflow-y-auto border-l border-border p-5 md:flex">
<MemoryList
items={memories}
onAdd={addMemory}
onForget={forgetMemory}
description="Facts the assistant keeps across conversations"
/>
<p className="mt-3 text-xs leading-5 text-muted-foreground">
Memories are included with each message so the assistant stays consistent. Remove
anything you do not want stored.
</p>
</div>
</div>
</Card>
);
}