{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"source-list","type":"registry:ui","title":"Source List","description":"The passages retrieval actually returned, with similarity scores and a visible relevance floor — including the case where nothing cleared it.","author":"Scrim UI (https://scrimui.dev)","categories":["sources"],"docs":"https://scrimui.dev/components/source-list","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/source-list/source-list.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * What retrieval actually returned — including what it returned and threw away.\n *\n * Most source panels show the top k passages and stop. The two things worth\n * building are the ones that get left out:\n *\n * **The floor has to be visible.** A retrieval system with no relevance floor\n * always returns something, and \"something\" for a question the corpus does\n * not answer is the closest match to a question nobody asked. Showing the\n * floor, and the candidates that fell under it, is what turns \"the model made\n * this up\" into \"nothing relevant was found and it answered anyway\" — two\n * different bugs, in two different files.\n *\n * **Nothing found is a state, not an empty list.** It is the state that makes\n * a RAG system trustworthy: no chunk cleared the floor, no model call, a\n * fixed \"it is not in these documents\". Rendering it as a blank panel throws\n * away the one moment the system was behaving well.\n *\n * The scores are shown because someone is always tuning the floor, and a\n * floor set without looking at the distribution it is cutting is a number\n * somebody guessed.\n */\n\nexport type RetrievedSource = {\n  id: string;\n  /** Document name, section, page — whatever locates it for a human. */\n  title: string;\n  passage: string;\n  /** Similarity, 0–1. */\n  score: number;\n};\n\nexport type SourceListProps = {\n  /** Every candidate considered, not only the ones that passed. */\n  sources: RetrievedSource[];\n  /** Below this, a passage was not sent to the model. */\n  floor?: number;\n  /** Jump to the passage in the document. */\n  onOpen?: (id: string) => void;\n  className?: string;\n};\n\nfunction ChevronIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"12\" height=\"12\" {...props}>\n      <path d=\"m6 9 6 6 6-6\" />\n    </svg>\n  );\n}\n\nfunction Row({\n  source,\n  n,\n  below,\n  onOpen,\n}: {\n  source: RetrievedSource;\n  n?: number;\n  below: boolean;\n  onOpen?: (id: string) => void;\n}) {\n  const body = (\n    <>\n      <div className=\"flex items-baseline gap-2\">\n        {n !== undefined && (\n          <span className=\"flex h-[1.35em] min-w-[1.35em] shrink-0 items-center justify-center rounded-[0.3em] bg-amber-100 px-[0.3em] text-[11px] font-medium text-amber-900 tabular-nums dark:bg-amber-400/20 dark:text-amber-200\">\n            {n}\n          </span>\n        )}\n        <span className=\"min-w-0 flex-1 truncate text-[13px] font-medium text-zinc-800 dark:text-zinc-100\">\n          {source.title}\n        </span>\n        <span\n          className={`shrink-0 tabular-nums text-[11px] ${\n            below ? \"text-zinc-400 dark:text-zinc-500\" : \"text-zinc-500 dark:text-zinc-400\"\n          }`}\n        >\n          {source.score.toFixed(3)}\n        </span>\n      </div>\n      <p\n        className={`mt-1 line-clamp-2 text-[12px] leading-5 ${\n          below ? \"text-zinc-400 dark:text-zinc-500\" : \"text-zinc-600 dark:text-zinc-300\"\n        }`}\n      >\n        {source.passage}\n      </p>\n    </>\n  );\n\n  if (!onOpen) {\n    return <div className=\"px-3.5 py-2.5\">{body}</div>;\n  }\n  return (\n    <button\n      type=\"button\"\n      onClick={() => onOpen(source.id)}\n      className=\"block w-full px-3.5 py-2.5 text-left transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50\"\n    >\n      {body}\n    </button>\n  );\n}\n\nexport function SourceList({ sources, floor = 0, onOpen, className = \"\" }: SourceListProps) {\n  const [showBelow, setShowBelow] = React.useState(false);\n\n  /* Sorted here rather than trusted from the caller: a panel whose order\n     disagrees with its own score column is the kind of bug nobody reports\n     and everybody stops trusting. */\n  const ranked = React.useMemo(() => [...sources].sort((a, b) => b.score - a.score), [sources]);\n  const passed = ranked.filter((s) => s.score >= floor);\n  const below = ranked.filter((s) => s.score < floor);\n\n  return (\n    <div className={`overflow-hidden rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900 ${className}`}>\n      <div className=\"flex items-baseline gap-2 border-b border-zinc-100 px-3.5 py-2.5 dark:border-zinc-800\">\n        <span className=\"text-[11px] font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500\">\n          Retrieved\n        </span>\n        <span className=\"text-[11px] text-zinc-500 dark:text-zinc-400\">\n          {passed.length} of {ranked.length} candidates\n        </span>\n        {floor > 0 && (\n          <span className=\"ml-auto tabular-nums text-[11px] text-zinc-400 dark:text-zinc-500\">\n            floor {floor.toFixed(2)}\n          </span>\n        )}\n      </div>\n\n      {passed.length === 0 ? (\n        /* The good state, rendered as such. Nothing cleared the floor, so no\n           model call was made and the answer is a fixed sentence — which is\n           the behaviour that makes the rest of the system worth trusting. */\n        <div className=\"px-3.5 py-4\">\n          <p className=\"text-[13px] font-medium text-zinc-800 dark:text-zinc-100\">\n            Nothing cleared the floor.\n          </p>\n          <p className=\"mt-1 text-[12px] leading-5 text-zinc-500 dark:text-zinc-400\">\n            The closest candidate scored {ranked[0]?.score.toFixed(3) ?? \"—\"}. No model call was\n            made — an answer built from these passages would have been invented.\n          </p>\n        </div>\n      ) : (\n        <div className=\"divide-y divide-zinc-100 dark:divide-zinc-800/80\">\n          {passed.map((s, i) => (\n            <Row key={s.id} source={s} n={i + 1} below={false} onOpen={onOpen} />\n          ))}\n        </div>\n      )}\n\n      {below.length > 0 && (\n        <div className=\"border-t border-zinc-100 dark:border-zinc-800\">\n          <button\n            type=\"button\"\n            onClick={() => setShowBelow((v) => !v)}\n            aria-expanded={showBelow}\n            className=\"flex w-full items-center gap-1.5 px-3.5 py-2 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-50 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800/50 dark:hover:text-zinc-200\"\n          >\n            <ChevronIcon className={showBelow ? \"rotate-180\" : \"\"} />\n            {below.length} below the floor — not sent to the model\n          </button>\n          {showBelow && (\n            <div className=\"divide-y divide-zinc-100 bg-zinc-50/60 dark:divide-zinc-800/80 dark:bg-zinc-800/20\">\n              {below.map((s) => (\n                <Row key={s.id} source={s} below onOpen={onOpen} />\n              ))}\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/source-list.tsx"}]}