Files
inkreach-agent/scripts/beautifului/thinking-state.json
T
Rick Ye a5951849c8 v0.6.0: beautifului 整合 + 会话/工作区/队列/权限/命令/附件全链路
- beautifului.dev 组件直连:Sidebar Nav、Tool Chips、Approval Card(抽屉化)、Streaming/Thinking、Code Block、Selection Actions、Prompt Bar、Search
2026-09-06 09:57:14 +08:00

19 lines
12 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"$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"
}
]
}