freezmo/components/icons.tsx
Martin Dvorak 6036238bf0 Teach it back: the AI becomes the curious student
A new mode where the user explains a topic in their own words and the AI
plays a curious classmate: short reactions, exactly one follow-up question
at a time, never "correct"/"wrong", never lecturing, and everything it knows
comes from the user's own decks and notes.

- Entry points: a sidebar row next to Exam mode and a "Teach it back" action
  in every deck's menu (which prefills the topic and scope).
- Start screen: deck or typed topic, friendly/skeptical student, the same
  context and model chips as Chat (/teach?deck=<id> pre-selects the deck).
- Session: the student prompt is a mode of the existing /api/chat route, so
  model resolution, allowlists, rate limits and the streaming client are the
  same code; the personality lives in lib/prompts.ts (TEACH_*). The deck's
  cards travel as a numbered checklist and the student is told to ask about
  the ideas that were skipped; it must say so rather than guess when the
  notes cover nothing. One constant (TEACH_MAX_EXCHANGES = 10) ends the
  session, or the Finish button does.
- Recap: a teach_summary call (JSON, one repair retry) yields well / gaps
  with card references / next steps; each gap links to the card it means.
  "Add missed cards to review" schedules those activities due now; "Turn
  gaps into cards" generates one flashcard per gap from the deck's notes and
  shows an editable review list before saving.
- Sessions are chat conversations with a mode:"teach" marker + session
  record: same IndexedDB history, backup and sync, filtered out of the Chat
  screens; the recent list reopens a recap or continues a session.
- Reused pieces: MessageRow, Composer (now optional attach/scope chips and a
  mic), Markdown/KaTeX, AiDisclaimer, ContextPicker/ModelPicker, plus a
  shared useChatModels hook and useKeyboardInset extracted from Chat.
- Dictation: Web Speech API recognition (lib/speech-input.ts); the mic only
  renders where the browser supports it, and dictated phrases land in the
  draft.
- All strings in en + cs. teach-test: 44 checks (API prompts and guards,
  both entry points, the session, recap actions, history, exchange limit,
  360/390px layouts, mic on/off). Version 0.8.1, SW v103.
2026-10-16 23:05:24 +00:00

474 lines
13 KiB
TypeScript

"use client";
/**
* Small inline SVG icons used for the app chrome so that critical controls
* don't depend on a system emoji font being installed.
*/
type IconProps = { className?: string; strokeWidth?: number };
const base = "size-5";
function Svg({
children,
className,
filled,
}: {
children: React.ReactNode;
className?: string;
filled?: boolean;
}) {
return (
<svg
viewBox="0 0 24 24"
className={className ?? base}
fill={filled ? "currentColor" : "none"}
stroke={filled ? "none" : "currentColor"}
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{children}
</svg>
);
}
export function IceCubeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3.2 19 7v10l-7 3.8L5 17V7l7-3.8Z" />
<path d="M5 7l7 3.8L19 7M12 10.8V21" />
</Svg>
);
}
export function SnowflakeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 2v20M4.5 6.5l15 11M19.5 6.5l-15 11" />
<path d="M12 6l-2.2-2.2M12 6l2.2-2.2M12 18l-2.2 2.2M12 18l2.2 2.2M7 9.2 4.8 8.6M7 14.8l-2.2.6M17 9.2l2.2-.6M17 14.8l2.2.6" />
</Svg>
);
}
export function GearIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="3.2" />
<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.11A1.7 1.7 0 0 0 8.9 19.3a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.7 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.11A1.7 1.7 0 0 0 4.7 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.7a1.7 1.7 0 0 0 1.03-1.56V3a2 2 0 1 1 4 0v.11A1.7 1.7 0 0 0 15.1 4.7a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.03A1.7 1.7 0 0 0 21 10.06H21a2 2 0 1 1 0 4h-.11A1.7 1.7 0 0 0 19.4 15Z" />
</Svg>
);
}
export function SunIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</Svg>
);
}
export function MoonIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M21 12.8A8.5 8.5 0 1 1 11.2 3a6.6 6.6 0 0 0 9.8 9.8Z" />
</Svg>
);
}
export function BoltIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M13.5 2 4 13.2h5.4L9.5 22 19 10.8h-5.4L13.5 2Z" />
</Svg>
);
}
export function FlameIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M12 2s4.5 4.2 4.5 8.2c0 1.2-.5 2.2-1.2 2.9.1-1.6-.5-3.3-1.8-4.6.3 2.6-1 4-2.2 5.1-1 .9-1.8 1.8-1.8 3.2 0 1 .4 1.9 1.1 2.6-2.9-.3-5.1-2.6-5.1-5.5C5.5 9.7 12 8 12 2Z" />
<path d="M12 22c2.2 0 4-1.6 4-3.7 0-1.6-.8-2.4-1.7-3.3-.6-.6-1.3-1.3-1.6-2.3-.8.8-1.4 2-1.4 3.2 0 .9-.4 1.4-1 1.8-.5.4-1.1.9-1.1 1.9C9.2 21.1 10.4 22 12 22Z" />
</Svg>
);
}
export function SparklesIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6L12 3Z" />
<path d="M18.5 15.5l.8 2.2 2.2.8-2.2.8-.8 2.2-.8-2.2-2.2-.8 2.2-.8.8-2.2Z" />
</Svg>
);
}
export function LayersIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3.5 20 8l-8 4.5L4 8l8-4.5Z" />
<path d="m4.6 12.4 7.4 4.2 7.4-4.2M4.6 16.4l7.4 4.2 7.4-4.2" />
</Svg>
);
}
export function FolderIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M3 7.5A1.5 1.5 0 0 1 4.5 6h4l2 2.5h9A1.5 1.5 0 0 1 21 10v8a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18V7.5Z" />
</Svg>
);
}
export function CameraIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h1.9l1.1-1.8A1 1 0 0 1 9.4 5h5.2a1 1 0 0 1 .9.5L16.6 7h1.9A1.5 1.5 0 0 1 20 8.5v9A1.5 1.5 0 0 1 18.5 19h-13A1.5 1.5 0 0 1 4 17.5v-9Z" />
<circle cx="12" cy="12.8" r="3.2" />
</Svg>
);
}
export function PlayIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<path d="M8 5.5v13l11-6.5-11-6.5Z" />
</Svg>
);
}
export function DownloadIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3v12M7.5 10.5 12 15l4.5-4.5M4 20h16" />
</Svg>
);
}
export function UploadIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 16V4M7.5 8.5 12 4l4.5 4.5M4 20h16" />
</Svg>
);
}
export function HomeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 10.5 12 4l8 6.5V19a1 1 0 0 1-1 1h-4v-5H9v5H5a1 1 0 0 1-1-1v-8.5Z" />
</Svg>
);
}
export function CheckCircleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m8.2 12.4 2.6 2.6 5-5.4" />
</Svg>
);
}
export function AlertTriangleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 4.5 21 19.5H3L12 4.5Z" />
<path d="M12 10v4.2M12 17.2v.1" />
</Svg>
);
}
export function XCircleIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m9 9 6 6M15 9l-6 6" />
</Svg>
);
}
export function PlusIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 5v14M5 12h14" />
</Svg>
);
}
/** A chat bubble with a plus: "new chat" — distinct from the plain plus the
* rail uses for "new deck" (two identical + buttons read as the same thing). */
export function ChatPlusIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
<path d="M12 8.5v5M9.5 11h5" />
</Svg>
);
}
export function ShareIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 3v12M8.2 6.8 12 3l3.8 3.8" />
<path d="M6 12H5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1h-1" />
</Svg>
);
}
export function ChevronLeftIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M14.5 5.5 8 12l6.5 6.5" />
</Svg>
);
}
export function ChevronRightIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m9.5 5.5 6.5 6.5-6.5 6.5" />
</Svg>
);
}
export function RefreshIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M20 12a8 8 0 1 1-2.4-5.7M20 4v4h-4" />
</Svg>
);
}
export function EyeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
<circle cx="12" cy="12" r="3" />
</Svg>
);
}
export function EyeOffIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m3 3 18 18" />
<path d="M10.6 5.7c.5-.1.9-.2 1.4-.2 6 0 9.5 6.5 9.5 6.5a17.5 17.5 0 0 1-2.6 3.5" />
<path d="M6.6 6.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.4 3.8-1" />
<path d="M9.9 9.9a3 3 0 0 0 4.2 4.2" />
</Svg>
);
}
export function QuestionIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="M9.7 9.4a2.5 2.5 0 0 1 4.9.7c0 1.7-2.5 2-2.5 3.5" />
<path d="M12 17h.01" />
</Svg>
);
}
export function XIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m6 6 12 12M18 6 6 18" />
</Svg>
);
}
export function ExternalLinkIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M14 4h6v6" />
<path d="m20 4-8.5 8.5" />
<path d="M18 13.5V19a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5.5" />
</Svg>
);
}
/* --- Install-sheet chips ------------------------------------------------ */
export function CompassIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<path d="m15.6 8.4-2.3 5.3-5.3 2.3 2.3-5.3 5.3-2.3Z" />
</Svg>
);
}
export function EllipsisIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="5.5" cy="12" r="1.4" fill="currentColor" stroke="none" />
<circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none" />
<circle cx="18.5" cy="12" r="1.4" fill="currentColor" stroke="none" />
</Svg>
);
}
export function ChevronDownIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m6 9.5 6 6 6-6" />
</Svg>
);
}
export function AddToHomeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="4" y="4" width="16" height="16" rx="4.5" />
<path d="M12 8.8v6.4M8.8 12h6.4" />
</Svg>
);
}
/* --- Chat --------------------------------------------------------------- */
export function ChatIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
</Svg>
);
}
/* --- Teach it back ------------------------------------------------------ */
/** Graduation cap: the Teach it back mode in the sidebar and deck menus. */
export function TeacherIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M2.5 9.5 12 5l9.5 4.5L12 14 2.5 9.5Z" />
<path d="M6.5 11.5v4.2c0 .8 2.5 2.3 5.5 2.3s5.5-1.5 5.5-2.3v-4.2" />
<path d="M21.5 9.5v4" />
</Svg>
);
}
export function MicIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="9" y="3" width="6" height="11" rx="3" />
<path d="M5.5 11.5a6.5 6.5 0 0 0 13 0" />
<path d="M12 18v3" />
</Svg>
);
}
export function SendIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M12 19V5" />
<path d="m6 11 6-6 6 6" />
</Svg>
);
}
export function StopIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="7" y="7" width="10" height="10" rx="2" fill="currentColor" stroke="none" />
</Svg>
);
}
export function CopyIcon({ className }: IconProps) {
return (
<Svg className={className}>
<rect x="9" y="9" width="11" height="11" rx="2.5" />
<path d="M5.5 15H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v.5" />
</Svg>
);
}
export function PaperclipIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="m20 11.5-7.8 7.8a5 5 0 0 1-7-7l8.3-8.3a3.3 3.3 0 0 1 4.7 4.7l-8.3 8.3a1.7 1.7 0 0 1-2.4-2.4l7.4-7.4" />
</Svg>
);
}
export function PencilIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4 20h4.2L20 8.2a2.1 2.1 0 0 0-3-3L5.2 17 4 20Z" />
<path d="m14.5 6.7 2.8 2.8" />
</Svg>
);
}
export function HistoryIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4.5 12a7.5 7.5 0 1 1 2.2 5.3" />
<path d="M4.5 12H2m2.5 0 2.3-2.3M4.5 12l2.3 2.3" />
<path d="M12 8.5V12l2.4 1.6" />
</Svg>
);
}
export function ChromeIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="9" />
<circle cx="12" cy="12" r="3.4" />
<path d="M12 8.6V3M9.05 13.7 4.2 16.5M14.95 13.7l4.85 2.8" />
</Svg>
);
}
/** Read-aloud control (study cards); the waves light up while speaking. */
export function SpeakerIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M4.5 9.4h2.9l4-3.2v11.6l-4-3.2H4.5z" />
<path d="M15.2 9.2a3.9 3.9 0 0 1 0 5.6M17.7 6.7a7.3 7.3 0 0 1 0 10.6" />
</Svg>
);
}
/** Timed runs. */
export function ClockIcon({ className }: IconProps) {
return (
<Svg className={className}>
<circle cx="12" cy="12" r="8.2" />
<path d="M12 7.4V12l3.1 1.9" />
</Svg>
);
}
/** Frutiger Aero's soundtrack player. */
export function MusicIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M9.4 17.2V6.6l8-1.6v10.4" />
<circle cx="6.9" cy="17.6" r="2.5" />
<circle cx="14.9" cy="15.8" r="2.5" />
</Svg>
);
}
export function PauseIcon({ className }: IconProps) {
return (
<Svg className={className} filled>
<rect x="7.2" y="5.6" width="3.4" height="12.8" rx="1.4" />
<rect x="13.4" y="5.6" width="3.4" height="12.8" rx="1.4" />
</Svg>
);
}
/** "From your notes" — a page with a folded corner and text lines. */
export function NotesIcon({ className }: IconProps) {
return (
<Svg className={className}>
<path d="M6.4 3.6h7.4l4.8 4.8v12H6.4z" />
<path d="M13.6 3.9v4.7h4.7M9.4 12.4h5.2M9.4 15.8h5.2" />
</Svg>
);
}