- beautifului.dev 组件直连:Sidebar Nav、Tool Chips、Approval Card(抽屉化)、Streaming/Thinking、Code Block、Selection Actions、Prompt Bar、Search
19 lines
12 KiB
JSON
19 lines
12 KiB
JSON
{
|
||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||
"name": "thinking-state",
|
||
"type": "registry:component",
|
||
"title": "Thinking",
|
||
"description": "Expandable traces — steps, reasoning, search, coding.",
|
||
"registryDependencies": [
|
||
"https://www.beautifului.dev/r/foundation.json"
|
||
],
|
||
"files": [
|
||
{
|
||
"path": "components/primitives/ThinkingState.tsx",
|
||
"content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\n/* ─────────────────────────────────────────────────────────\n * THINKING — expandable agent trace, four variants\n *\n * Steps step list with spinner → muted checks\n * Reasoning prose reasoning that expands, then settles\n * Search web-search trace: query + sources read\n * Coding tool trace: files read, edits, commands\n *\n * The trace runs once, settles, and remains expandable.\n * ───────────────────────────────────────────────────────── */\n\nconst STAGES = [800, 600, 1800, 2600, 1600];\n\nfunction useSequence(steps: number[]) {\n const [stage, setStage] = useState(0);\n useEffect(() => {\n if (stage >= steps.length - 1) return;\n const t = setTimeout(() => setStage((s) => s + 1), steps[stage]);\n return () => clearTimeout(t);\n }, [stage, steps]);\n return stage;\n}\n\ntype Row = {\n primary: string;\n secondary?: string;\n mono?: boolean;\n add?: number;\n del?: number;\n href?: string;\n};\n\nconst VARIANTS: Record<\n string,\n { active: string; done: string; rows: Row[]; query?: string }\n> = {\n Steps: {\n active: \"Thinking\",\n done: \"Thought for 4 seconds\",\n rows: [\n { primary: \"Reading flavor briefs\" },\n { primary: \"Scanning supplier lists\" },\n { primary: \"Comparing tasting notes\", secondary: \"6 flavors\" },\n { primary: \"Writing the scoop report\" },\n ],\n },\n Reasoning: {\n active: \"Thinking\",\n done: \"Thought for 4 seconds\",\n rows: [\n { primary: \"Summer demand spikes for stone-fruit flavors — peach and apricot lead.\" },\n { primary: \"I should check cone inventory before promoting a waffle-bowl special.\" },\n ],\n },\n Search: {\n active: \"Searching the web\",\n done: \"Searched the web\",\n query: \"best waffle cone supplier\",\n rows: [\n { primary: \"Joy Cone\", secondary: \"joycone.com\", href: \"https://joycone.com/fs_products/waffle-cones/\" },\n { primary: \"WebstaurantStore\", secondary: \"webstaurantstore.com\", href: \"https://www.webstaurantstore.com/ice-cream-shop-supplies.html\" },\n { primary: \"The Konery\", secondary: \"thekonery.com\", href: \"https://www.thekonery.com/\" },\n ],\n },\n Coding: {\n active: \"Running tools\",\n done: \"Ran 3 tools\",\n rows: [\n { primary: \"Read\", secondary: \"flavors.ts\", mono: true },\n { primary: \"Edit\", secondary: \"ChurnSchedule.tsx\", mono: true, add: 74, del: 41 },\n { primary: \"Run\", secondary: \"npm run freeze\", mono: true },\n ],\n },\n};\n\nfunction Dot({ tone }: { tone: string }) {\n return (\n <span className={`flex size-3.5 shrink-0 items-center justify-center rounded-full text-white ${tone}`}>\n <svg width=\"9\" height=\"9\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M3.5 12h17M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18\" />\n </svg>\n </span>\n );\n}\n\nconst TONES = [\"bg-accent\", \"bg-orange\", \"bg-green\"];\n\nexport default function ThinkingState({ variant = \"Steps\", onSettled }: { variant?: string; onSettled?: () => void }) {\n const stage = useSequence(STAGES);\n const [manualExpanded, setManualExpanded] = useState<boolean | null>(null);\n const [selectedTool, setSelectedTool] = useState<string | null>(null);\n const v = VARIANTS[variant] ?? VARIANTS.Steps;\n const autoExpanded = stage >= 1 && stage < 4;\n const expanded = manualExpanded ?? autoExpanded;\n const working = stage < 3;\n const visible = stage < 2 ? 0 : stage === 2 ? Math.min(2, v.rows.length) : v.rows.length;\n const traceRef = useRef<HTMLDivElement>(null);\n const [lineHeight, setLineHeight] = useState(0);\n useLayoutEffect(() => {\n if (traceRef.current) setLineHeight(traceRef.current.offsetHeight);\n }, [visible, expanded, variant, stage]);\n\n /* let embedders sequence content after the trace settles */\n const settledRef = useRef(false);\n useEffect(() => {\n if (working || settledRef.current) return;\n settledRef.current = true;\n onSettled?.();\n }, [working, onSettled]);\n\n return (\n <div\n key={variant}\n className=\"flex w-full max-w-95 flex-col\"\n style={{\n minHeight: working || expanded ? 176 : undefined,\n transition: \"min-height 400ms cubic-bezier(0.23,1,0.32,1)\",\n }}\n >\n {/* header — shared across variants */}\n <button\n type=\"button\"\n aria-expanded={expanded}\n onClick={() => setManualExpanded((current) => !(current ?? autoExpanded))}\n className=\"-mx-1.5 flex w-fit items-center gap-2 rounded-control px-1.5 py-1\n transition-colors duration-100 hover:bg-hover-2\"\n >\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill={working ? \"var(--ink-2)\" : \"var(--ink-3)\"}>\n <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />\n </svg>\n <span role=\"status\" className=\"contents\">\n {working ? (\n <span\n className=\"bg-clip-text text-[13px] font-medium whitespace-nowrap text-transparent\"\n style={{\n backgroundImage:\n \"linear-gradient(90deg, var(--ink-3) 35%, var(--ink) 50%, var(--ink-3) 65%)\",\n backgroundSize: \"200% 100%\",\n animation: \"shimmer-text 1.4s linear infinite\",\n }}\n >\n {v.active}\n </span>\n ) : (\n <span\n className=\"text-[13px] font-medium whitespace-nowrap text-ink-2\"\n style={{ animation: \"fade-in 350ms ease-out both\" }}\n >\n {v.done}\n </span>\n )}\n </span>\n <svg\n width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\"\n className=\"transition-transform duration-300\"\n style={{ transform: expanded ? \"rotate(180deg)\" : \"rotate(0)\" }}\n >\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n </button>\n\n {/* expandable trace */}\n <div\n className=\"grid transition-[grid-template-rows,opacity] duration-400\"\n style={{\n gridTemplateRows: expanded ? \"1fr\" : \"0fr\",\n opacity: expanded ? 1 : 0,\n transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n }}\n >\n <div className=\"overflow-hidden\">\n <div className=\"relative mt-1 ml-[5px] pl-4\">\n <span\n aria-hidden\n className=\"absolute left-[3px] w-px bg-line\"\n style={{ top: -8, height: lineHeight ? lineHeight - 2 : 0, transition: \"height 500ms cubic-bezier(0.23,1,0.32,1)\" }}\n />\n <div ref={traceRef} className=\"flex flex-col gap-1 py-1\">\n {v.query && (\n <div className=\"flex h-6 items-center gap-2 px-1.5\" style={{ animation: expanded ? \"fade-up 300ms cubic-bezier(0.23,1,0.32,1) both\" : undefined }}>\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2\" strokeLinecap=\"round\" className=\"shrink-0\">\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"M21 21l-4.3-4.3\" />\n </svg>\n <span className=\"text-[12.5px] text-ink-2\">{v.query}</span>\n </div>\n )}\n {v.rows.slice(0, visible).map((row, i) => {\n const content = (\n <>\n {variant === \"Search\" && <Dot tone={TONES[i % 3]} />}\n {variant === \"Steps\" && (\n i < visible - 1 || !working ? (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"shrink-0\">\n <path d=\"M20 6L9 17l-5-5\" />\n </svg>\n ) : (\n <span className=\"size-3 shrink-0 rounded-full border-[1.5px] border-line-strong border-t-ink-2\" style={{ animation: \"spin 700ms linear infinite\" }} />\n )\n )}\n <span className={`min-w-0 truncate text-[12.5px] ${variant === \"Reasoning\" ? \"whitespace-normal leading-relaxed text-ink-2\" : \"font-medium text-ink\"} ${variant === \"Search\" ? \"animated-underline\" : \"\"}`}>\n {row.primary}\n </span>\n {row.secondary && (\n <span className={`shrink-0 text-[11.5px] text-ink-3 ${row.mono ? \"font-mono\" : \"\"}`}>\n {row.secondary}\n </span>\n )}\n {row.add !== undefined && (\n <span className=\"shrink-0 font-mono text-[11px] tabular-nums\">\n <span className=\"text-green\">+{row.add}</span>{\" \"}\n <span className=\"text-red\">−{row.del}</span>\n </span>\n )}\n </>\n );\n const rowClass = \"flex min-h-7 w-full items-center gap-2 rounded-[6px] px-1.5 py-0.5 text-left\";\n const animation = { animation: `fade-up 320ms cubic-bezier(0.23,1,0.32,1) ${i * 120}ms both` };\n\n if (variant === \"Search\") {\n return (\n <a\n key={row.primary}\n href={row.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={`${rowClass} transition-colors duration-150 hover:bg-hover`}\n style={animation}\n >\n {content}\n </a>\n );\n }\n\n if (variant === \"Coding\") {\n const selected = selectedTool === row.primary;\n return (\n <button\n key={row.primary}\n type=\"button\"\n aria-pressed={selected}\n onClick={() => setSelectedTool(selected ? null : row.primary)}\n className={`${rowClass} transition-colors duration-150 ${selected ? \"bg-inset\" : \"hover:bg-hover\"}`}\n style={animation}\n >\n {content}\n </button>\n );\n }\n\n return (\n <div key={row.primary} className={rowClass} style={animation}>\n {content}\n </div>\n );\n })}\n {variant === \"Search\" && stage >= 3 && (\n <span className=\"text-[12px] text-ink-3\" style={{ animation: \"fade-in 300ms ease-out both\" }}>\n +7 more\n </span>\n )}\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n}\n",
|
||
"type": "registry:component",
|
||
"target": "components/primitives/ThinkingState.tsx"
|
||
}
|
||
]
|
||
}
|