Scrim UI

Model & Memory Preferences

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

model-preferences.tsx197 lines4 componentsReact + Tailwind, no dependencies

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 * 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", hint: "Balanced speed and quality", badges: ["Default"], icon: <ClaudeMark /> },
  { id: "opus", name: "Claude Opus", hint: "Best for hard problems", badges: ["Deep thinking"], icon: <ClaudeMark /> },
  { id: "haiku", name: "Claude Haiku", 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="2" 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="2" 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="2" 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="2" 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 (
    <div className="flex h-[560px] flex-col overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
      {/* Header */}
      <div className="flex items-center justify-between border-b border-zinc-200 px-5 py-3.5 dark:border-zinc-800">
        <div>
          <p className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
            Model &amp; Memory Preferences
          </p>
          <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
            How the assistant thinks, which tools it may use, and what it remembers.
          </p>
        </div>
        <span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] 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
        </span>
      </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-zinc-900 dark:text-zinc-100">Default model</p>
              <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
                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-zinc-200 p-5 dark:border-zinc-800 md:flex">
          <MemoryList
            items={memories}
            onAdd={addMemory}
            onForget={forgetMemory}
            description="Facts the assistant keeps across conversations"
          />
          <p className="mt-3 text-[11px] leading-5 text-zinc-500 dark:text-zinc-400">
            Memories are included with each message so the assistant stays consistent. Remove
            anything you do not want stored.
          </p>
        </div>
      </div>
    </div>
  );
}

Usage Guidelines

  • 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.

Common UX Mistakes

  • 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