Skip to content
Scrim UI

Model & Memory Preferences

A preferences screen that picks the model, reasoning level and tools, and manages persistent memory.

model-preferences.tsx199 lines4 componentsReact + Tailwind + shadcn/ui

npx shadcn@latest add @scrimui/model-preferences
Agent promptClaude Code · Cursor · any agent
Add the Model & Memory Preferences pattern from the Scrim UI registry to this project — a complete screen, not a single component.

Model & Memory Preferences — A preferences screen that picks the model, reasoning level and tools, and manages persistent memory.

## 1. Install

```bash
npx shadcn@latest add @scrimui/model-preferences
```

This writes the screen to `components/blocks/model-preferences.tsx` and pulls in the components it is built from, each landing at `components/ui/`. Everything is plain React + Tailwind with no runtime dependencies. The imports in the block already point at those paths, so it compiles as installed.

## 2. What it is made of

- `model-selector` — Model Selector
- `reasoning-level` — Reasoning Level
- `tool-toggle` — Tool Toggle
- `memory-list` — Memory List

Each is a separate file you can edit or replace on its own; the block is the arrangement, not a monolith.

## 3. Rules this layout depends on

Keep these when adapting the screen — they are the reasons it works, and they are easy to break while restyling:

- Group decisions that shape every answer — model, reasoning effort and tools — into one visible place, not buried in settings.
- Show the reasoning-level trade-off in words, not just a slider: tell the user what 'deep' will cost in latency.
- State what each tool is allowed to do next to its switch; a toggle without a permission boundary is a trap.
- Keep memory visible and removable next to the controls that depend on it — the user should see what the assistant holds.
- Persist these choices per conversation and per user; preferences that reset silently break trust.

## 4. Do not do these

- Spreading model, tools and memory across separate pages so the user cannot reason about them together.
- Letting the tool switches run without explaining the scope of each permission.
- Hiding saved memories behind a 'data' page — a user should not have to hunt to forget something.
- Adding a reasoning-level control that does not say how much slower 'deep' answers will be.

The demo content in the file — messages, file names, model names — is placeholder. Replace it with this project's real data and wire the handlers to real state rather than shipping the stubs.

Reference: https://scrimui.dev/patterns/model-preferences

Installs the screen and every component it is built from, and carries the layout rules from this page so an agent does not restyle them away.

Live Preview

Model & Memory Preferences

How the assistant thinks, which tools it may use, and what it remembers.

Saved

Default model

Used for new conversations; you can still switch per message.

Reasoning effort

Balanced

A good default for most tasks

Allowed tools

What the assistant may do on your behalf

  • Web search

    Find current information online

  • Code execution

    Run code to verify answers

  • File access

    Read and edit attached files

  • Browser

    Open pages from the conversation

Built from these components

Pattern code

This file composes the components above. Copy each component from its page, then this pattern file wires them together.

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 "../../model-selector/model-selector";
import {
  ReasoningLevel,
  type ReasoningLevel as ReasoningLevelValue,
} from "../../reasoning-level/reasoning-level";
import { ToolToggle, type ToolSetting } from "../../tool-toggle/tool-toggle";
import { MemoryList, type MemoryItem } from "../../memory-list/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 &amp; 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>
  );
}

When to use it

  • Group decisions that shape every answer — model, reasoning effort and tools — into one visible place, not buried in settings.
  • Show the reasoning-level trade-off in words, not just a slider: tell the user what 'deep' will cost in latency.
  • State what each tool is allowed to do next to its switch; a toggle without a permission boundary is a trap.
  • Keep memory visible and removable next to the controls that depend on it — the user should see what the assistant holds.
  • Persist these choices per conversation and per user; preferences that reset silently break trust.

What breaks in production

  • Spreading model, tools and memory across separate pages so the user cannot reason about them together.
  • Letting the tool switches run without explaining the scope of each permission.
  • Hiding saved memories behind a 'data' page — a user should not have to hunt to forget something.
  • Adding a reasoning-level control that does not say how much slower 'deep' answers will be.

More Patterns

AI Chat

The canonical chat interface — sidebar, streaming messages, prompt input with model selector, and sources.

AI Research Assistant

A research flow that shows search tool calls, reasoning, sources and a cited final answer.

AI Coding Agent

A coding run with agent status, tool calls, diffs and a human-in-the-loop approval gate.

AI Voice Assistant

A voice-first conversation — live waveform states, a recording input, a spoken transcript and a typed fallback.

Artifact Workspace

Chat on the left, generated output on the right — artifacts open from the answer, stream, version, and fail without breaking the conversation.

Document Q&A Workspace

Ask your own documents — upload and parse, cited answers with inspectable passages, an honest not-found state, and a visible context budget.

Structured Extraction & Review

Upload a document, watch fields fill in, then review the flagged ones — per-field confidence, corrections that keep the original, export earned.

Image Generation Studio

A one-screen generation studio — prompt composer and model picker beside a feed of queued, staged, blocked and ready image results with variants.

Multi-agent Ops Console

Watch a fleet of agents at once — parallel statuses, an inspectable handoff, a waiting approval, a failed child run, and per-run plus fleet cost.

Customer Support Copilot

Grounded reply drafts with citations, honest low-confidence answers, inline corrections, an approval gate on refunds, and a rating row on every draft.

Generative UI Dashboard

The model assembles a dashboard from a controlled widget registry — streamed props, an unsupported-request fallback, and widget clicks that re-enter the chat.