unnamed-ui
折叠/步骤

Thinking Process

Composed thinking step with status, content blocks, and sub-steps

思考中生成的内容
明确研究目标与边界
明确研究目标与边界,我将调用知识和搜索工具。
调取知识:
我正在调取知识库资料
📄
AI发展趋势.pdf
📄
AI发展历史.doc
对比岗位与简历关键信息
正在抽取关键技能并计算匹配度...
生成结论与问题清单
已生成 10 个面试问题,并输出风险点说明。
已取消
正在汇总候选人的关键信息与风险点...
"use client";

import { BookOpen } from "lucide-react";
import { ThinkingLoadingDotsPrimitive } from "@/components/wuhan/blocks/thinking-process/thinking-process-01";
import { ThinkingStep } from "@/components/composed/thinking-process";
import type { ThinkingStepItemProps } from "@/components/composed/thinking-step-item/thinking-step-item";

export function ThinkingProcessDemo() {
  const subSteps = [
    {
      status: "success",
      title: "明确研究目标与边界",
      items: [
        {
          content: "明确研究目标与边界,我将调用知识和搜索工具。",
          toolCall: {
            icon: <BookOpen className="size-4" />,
            title: "调取知识",
            content: "我正在调取知识库资料",
          },
          files: [
            { icon: "📄", name: "AI发展趋势.pdf" },
            { icon: "📄", name: "AI发展历史.doc" },
          ],
        },
      ],
      defaultOpen: true,
    },
    {
      status: "loading",
      title: "对比岗位与简历关键信息",
      items: [{ content: "正在抽取关键技能并计算匹配度..." }],
      defaultOpen: true,
    },
    {
      status: "success",
      title: "生成结论与问题清单",
      items: [{ content: "已生成 10 个面试问题,并输出风险点说明。" }],
      defaultOpen: true,
    },
  ] satisfies ThinkingStepItemProps[];

  return (
    <div className="w-full h-full flex flex-col gap-4 max-w-2xl">
      {/* 思考中状态 - 不显示时间,标题闪烁 */}
      <ThinkingStep
        status="thinking"
        title="思考中..."
        content="思考中生成的内容"
      />

      {/* 已完成状态 - 显示时间 */}
      <ThinkingStep
        status="completed"
        title="思考完成"
        duration={14}
        content="用户想要了解AI发展的趋势。这是一个比较开放的问题,需要从多个维度来概括当前的主要方向。考虑到用户可能不是专业人士,应该用清晰的结构和易懂的语言来组织信息。"
      />

      {/* 已完成状态 - 使用 contentBlocks 穿插渲染(文字 + 子步骤 + 文字) */}
      <ThinkingStep
        status="completed"
        title="思考完成(contentBlocks)"
        duration={30}
        contentBlocks={[
          {
            type: "text",
            key: "intro",
            content: "下面是本次分析的关键子步骤(可与文字穿插渲染):",
          },
          { type: "subSteps", key: "steps", steps: subSteps },
          {
            type: "text",
            key: "outro",
            content: "以上步骤完成后,已生成最终结论与问题清单。",
          },
          { type: "node", key: "node", node: <h1>自定义组件</h1> },
        ]}
      />

      {/* 长耗时提示 - 默认收起,点击可展开看到提示 */}
      <ThinkingStep status="thinking" title="搜索中..." longRunning />

      {/* 已取消状态 - 默认展开且会自动追加一个“已取消”子步骤 */}
      <ThinkingStep
        status="cancelled"
        title="已取消"
        contentBlocks={[{ type: "subSteps", key: "steps", steps: subSteps }]}
      />

      {/* 思考中状态 - 自定义图标(loading dots) */}
      <ThinkingStep
        status="thinking"
        title="思考中(自定义图标)"
        icon={<ThinkingLoadingDotsPrimitive />}
        content="正在汇总候选人的关键信息与风险点..."
      />
    </div>
  );
}

Thinking Process 是一个单个思考步骤块的组合组件,适合展示 AI 思考/任务执行的状态与内容,并支持“文字 + 子步骤”混合渲染。

安装

pnpm dlx shadcn@latest add http://localhost:3000/r/wuhan/thinking-process.json

概述

  • 定位:展示单个思考步骤(状态、标题、可折叠内容)
  • 组合方式:通过 contentBlocks 可穿插文字与子步骤
  • 适用范围:开箱即用;若需深度布局/交互定制,使用 primitives

Usage

基本

import { ThinkingStep } from "@/registry/wuhan/composed/thinking-process/thinking-process";

export function Example() {
  return (
    <ThinkingStep
      status="completed"
      title="思考完成"
      duration={14}
      content="分析完成,已生成详细方案。"
      defaultOpen
    />
  );
}

内容块(推荐)

import { ThinkingStep } from "@/registry/wuhan/composed/thinking-process/thinking-process";

const steps = [
  { status: "success", title: "子步骤 1", items: [{ content: "已完成" }] },
  { status: "loading", title: "子步骤 2", items: [{}] },
];

<ThinkingStep
  status="completed"
  title="分析完成"
  contentBlocks={[
    { type: "text", key: "intro", content: "下面是关键子步骤:" },
    { type: "subSteps", key: "steps", steps },
    { type: "text", key: "outro", content: "步骤完成后将生成结论。" },
  ]}
  defaultOpen
/>;

长耗时提示

<ThinkingStep status="thinking" title="搜索中..." longRunning />

自定义文案

<ThinkingStep
  status="completed"
  title="思考完成"
  labels={{
    longRunningHint: "任务需要一段时间,可稍后查看",
    cancelledStepTitle: "已终止",
  }}
/>;

API (Composed)

属性类型默认值说明
status'pending' | 'thinking' | 'completed' | 'idle' | 'running' | 'success' | 'error' | 'cancelled''pending'步骤状态
titleReact.ReactNode-步骤标题
contentReact.ReactNode-步骤内容
contentBlocksThinkingStepContentBlock[]-文字/子步骤/自定义节点穿插
subStepsunknown[]-子步骤数据(需配合 renderSubSteps
renderSubSteps(subSteps) => ReactNode-子步骤渲染器
durationnumber-耗时(秒)
headerMetaReact.ReactNode-头部右侧自定义信息
iconReact.ReactNode-自定义图标
arrowIconReact.ReactNode-自定义箭头
hintReact.ReactNode-长耗时提示文案
longRunningbooleanfalse长耗时场景默认收起
labelsThinkingStepLabels-文案与统计配置
triggerIdstring-触发器 id(无障碍)
contentIdstring-内容区域 id(无障碍)
defaultOpenbooleanfalse默认展开
openboolean-受控展开
onOpenChange(open) => void-展开变化回调

Behavior Notes

  • contentBlocks 存在时忽略 content / subSteps / renderSubSteps,且不会回退到其他内容来源
  • status="cancelled" 会自动追加一个“已取消”子步骤
  • duration 仅在成功态(success/completed)显示,且有 headerMeta 时会优先显示 headerMeta
  • hint/longRunning 仅在没有正文内容时展示(避免出现“空内容容器”)

代码演示

默认状态

分析完成,已生成详细方案。
"use client";

import { ThinkingStep } from "@/components/composed/thinking-process/thinking-process";

export function ThinkingProcessDefault() {
  return (
    <div className="w-full h-full max-w-2xl">
      <ThinkingStep
        status="completed"
        title="思考完成"
        duration={14}
        content="分析完成,已生成详细方案。"
        defaultOpen
      />
    </div>
  );
}

内容块

下面是关键子步骤:
整理需求
明确目标与范围。
调取知识:
读取历史资料
分析方案
正在生成分析结构...
以上步骤已完成。
"use client";

import { BookOpen } from "lucide-react";
import { ThinkingStep } from "@/components/composed/thinking-process/thinking-process";
import type { ThinkingStepItemProps } from "@/components/composed/thinking-step-item/thinking-step-item";

export function ThinkingProcessCustom() {
  const steps: ThinkingStepItemProps[] = [
    {
      status: "success",
      title: "整理需求",
      items: [
        {
          content: "明确目标与范围。",
          toolCall: {
            icon: <BookOpen className="size-4" />,
            title: "调取知识",
            content: "读取历史资料",
          },
        },
      ],
    },
    {
      status: "loading",
      title: "分析方案",
      items: [{ content: "正在生成分析结构..." }],
    },
  ];

  return (
    <div className="w-full h-full max-w-2xl">
      <ThinkingStep
        status="completed"
        title="思考完成"
        duration={24}
        contentBlocks={[
          { type: "text", key: "intro", content: "下面是关键子步骤:" },
          { type: "subSteps", key: "steps", steps },
          { type: "text", key: "outro", content: "以上步骤已完成。" },
        ]}
        defaultOpen
      />
    </div>
  );
}

长时运行

"use client";

import { ThinkingStep } from "@/components/composed/thinking-process/thinking-process";

export function ThinkingProcessWithHint() {
  return (
    <div className="w-full h-full max-w-2xl space-y-4">
      <ThinkingStep status="thinking" title="搜索中..." longRunning />
      <ThinkingStep
        status="thinking"
        title="处理中..."
        hint="任务将持续数分钟,可稍后查看"
        longRunning
      />
    </div>
  );
}

状态与标签

配置项
基础信息
体验与提示
外观与可访问性
流式控制
状态:pending·阶段:intro·已暂停
AI 消息预览(ThinkingStep 内嵌)
"use client";

import * as React from "react";
import { BookOpen, ChevronRight } from "lucide-react";
import {
  ThinkingLoadingDotsPrimitive,
  type ThinkingStepStatus,
} from "@/components/wuhan/blocks/thinking-process/thinking-process-01";
import { ThinkingStep } from "@/components/composed/thinking-process/thinking-process";
import type { ThinkingStepItemProps } from "@/components/composed/thinking-step-item/thinking-step-item";
type StreamSpeed = "slow" | "medium" | "fast";
type Phase =
  | {
      type: "text";
      key: string;
      fullText: string;
    }
  | {
      type: "step";
      key: string;
      step: ThinkingStepItemProps;
    };

const INTRO_TEXT =
  "用户想要了解 AI 发展的趋势。这是一个比较开放的问题,需要从多个维度来概括当前的主要方向。考虑到用户可能不是专业人士,我会先明确范围,再提炼关键维度。";
const OUTRO_TEXT =
  "关键步骤完成后,我会基于维度给出结论,并附上可执行的建议与风险提示。";

const FULL_STEPS = [
  {
    status: "success",
    title: "明确研究目标与边界",
    items: [
      {
        content: "明确研究目标与边界,我将调用知识和搜索工具。",
        toolCall: {
          icon: <BookOpen className="size-4" />,
          title: "调取知识",
          content: "我正在调取知识库资料",
        },
        files: [
          { icon: "📄", name: "AI发展趋势.pdf" },
          { icon: "📄", name: "AI发展历史.doc" },
        ],
      },
    ],
  },
  {
    status: "loading",
    title: "对比岗位与简历关键信息",
    items: [
      { content: "正在抽取关键技能并计算匹配度..." },
      { content: "对照 JD 与历史项目,补齐软技能维度。" },
    ],
  },
  {
    status: "success",
    title: "生成结论与问题清单",
    items: [
      { content: "已生成 10 个面试问题,并输出风险点说明。" },
      { content: "补充候选人潜力评估与跟进建议。" },
    ],
  },
] satisfies ThinkingStepItemProps[];

const SPEED_CONFIG: Record<
  StreamSpeed,
  { interval: number; textStep: number }
> = {
  slow: { interval: 320, textStep: 2 },
  medium: { interval: 160, textStep: 4 },
  fast: { interval: 80, textStep: 8 },
};

export function ThinkingProcessDebugging() {
  const phases = React.useMemo<Phase[]>(
    () => [
      { type: "text", key: "intro", fullText: INTRO_TEXT },
      { type: "step", key: "step-1", step: FULL_STEPS[0] },
      { type: "step", key: "step-2", step: FULL_STEPS[1] },
      { type: "step", key: "step-3", step: FULL_STEPS[2] },
      { type: "text", key: "outro", fullText: OUTRO_TEXT },
    ],
    [],
  );

  const [title, setTitle] = React.useState("");
  const [headerMeta, setHeaderMeta] = React.useState("");
  const [useHeaderMeta, setUseHeaderMeta] = React.useState(false);
  const [duration, setDuration] = React.useState(0);
  const [longRunning, setLongRunning] = React.useState(false);
  const [hint, setHint] = React.useState("");
  const [longRunningHint, setLongRunningHint] = React.useState("");
  const [cancelledStepTitle, setCancelledStepTitle] = React.useState("");
  const [triggerId, setTriggerId] = React.useState("");
  const [contentId, setContentId] = React.useState("");
  const [useCustomIcon, setUseCustomIcon] = React.useState(false);
  const [useCustomArrow, setUseCustomArrow] = React.useState(false);
  const [speed, setSpeed] = React.useState<StreamSpeed>("medium");
  const [isStreaming, setIsStreaming] = React.useState(false);
  const [streamState, setStreamState] = React.useState({
    phaseIndex: 0,
    textProgress: 0,
    stepProgress: 0,
  });

  const isCompleted = streamState.phaseIndex >= phases.length;
  const hasStarted =
    streamState.phaseIndex > 0 ||
    streamState.textProgress > 0 ||
    streamState.stepProgress > 0;

  const resolvedStatus: ThinkingStepStatus = isCompleted
    ? "completed"
    : hasStarted
      ? "thinking"
      : "pending";

  const resolvedHeaderMeta =
    useHeaderMeta && headerMeta.trim().length > 0
      ? headerMeta.trim()
      : undefined;
  const resolvedHint = hint.trim().length > 0 ? hint.trim() : undefined;
  const resolvedLongRunningHint =
    longRunningHint.trim().length > 0 ? longRunningHint.trim() : undefined;
  const resolvedCancelledStepTitle =
    cancelledStepTitle.trim().length > 0
      ? cancelledStepTitle.trim()
      : undefined;
  const resolvedLabels =
    resolvedLongRunningHint || resolvedCancelledStepTitle
      ? {
          longRunningHint: resolvedLongRunningHint,
          cancelledStepTitle: resolvedCancelledStepTitle,
        }
      : undefined;
  const resolvedTitle =
    title.trim().length > 0
      ? title.trim()
      : resolvedStatus === "completed"
        ? "思考完成"
        : "思考中...";

  const contentBlocks = React.useMemo(() => {
    const blocks: Array<
      | { type: "text"; key: string; content: React.ReactNode }
      | {
          type: "subSteps";
          key: string;
          steps: ThinkingStepItemProps[];
        }
    > = [];
    const stepsBuffer: ThinkingStepItemProps[] = [];
    const currentPhase = phases[streamState.phaseIndex];
    const flushSteps = () => {
      if (stepsBuffer.length === 0) return;
      blocks.push({
        type: "subSteps",
        key: `steps-${blocks.length}`,
        steps: [...stepsBuffer],
      });
      stepsBuffer.length = 0;
    };

    phases.forEach((phase, index) => {
      if (phase.type === "text") {
        flushSteps();
        if (isCompleted || index < streamState.phaseIndex) {
          blocks.push({
            type: "text",
            key: phase.key,
            content: phase.fullText,
          });
          return;
        }
        if (currentPhase?.key === phase.key) {
          blocks.push({
            type: "text",
            key: phase.key,
            content: phase.fullText.slice(0, streamState.textProgress) || "",
          });
        }
        return;
      }

      if (phase.type === "step") {
        if (!isCompleted && index > streamState.phaseIndex) return;
        const totalItems = phase.step.items?.length ?? 0;
        const isCurrent = currentPhase?.key === phase.key && !isCompleted;
        const visibleCount = isCompleted
          ? totalItems
          : isCurrent
            ? Math.min(totalItems, streamState.stepProgress)
            : totalItems;
        const status: ThinkingStepItemProps["status"] = isCompleted
          ? "success"
          : isCurrent
            ? "loading"
            : "success";

        stepsBuffer.push({
          ...phase.step,
          status,
          items: phase.step.items?.slice(0, visibleCount),
          defaultOpen: true,
        });
      }
    });

    flushSteps();

    return blocks;
  }, [
    isCompleted,
    phases,
    streamState.phaseIndex,
    streamState.stepProgress,
    streamState.textProgress,
  ]);

  React.useEffect(() => {
    if (!isStreaming) return;
    const config = SPEED_CONFIG[speed];
    const timer = window.setInterval(() => {
      setStreamState((prev) => {
        const phase = phases[prev.phaseIndex];
        if (!phase) return prev;
        if (phase.type === "text") {
          const nextText = Math.min(
            prev.textProgress + config.textStep,
            phase.fullText.length,
          );
          if (nextText >= phase.fullText.length) {
            return {
              phaseIndex: prev.phaseIndex + 1,
              textProgress: 0,
              stepProgress: 0,
            };
          }
          return { ...prev, textProgress: nextText };
        }
        const totalItems = phase.step.items?.length ?? 0;
        const nextStep = Math.min(prev.stepProgress + 1, totalItems);
        if (nextStep >= totalItems) {
          return {
            phaseIndex: prev.phaseIndex + 1,
            textProgress: 0,
            stepProgress: 0,
          };
        }
        return { ...prev, stepProgress: nextStep };
      });
    }, config.interval);

    return () => window.clearInterval(timer);
  }, [isStreaming, phases, speed]);

  React.useEffect(() => {
    if (!isStreaming) return;
    const timer = window.setInterval(
      () => setDuration((prev) => prev + 1),
      1000,
    );
    return () => window.clearInterval(timer);
  }, [isStreaming]);

  React.useEffect(() => {
    if (!isStreaming) return;
    if (isCompleted) {
      setIsStreaming(false);
    }
  }, [isCompleted, isStreaming]);

  const handleStart = () => {
    setStreamState({ phaseIndex: 0, textProgress: 0, stepProgress: 0 });
    setDuration(0);
    setIsStreaming(true);
  };

  const handleStop = () => {
    setIsStreaming(false);
  };

  const handleComplete = () => {
    setIsStreaming(false);
    setStreamState({
      phaseIndex: phases.length,
      textProgress: 0,
      stepProgress: 0,
    });
  };

  const handleReset = () => {
    setIsStreaming(false);
    setStreamState({ phaseIndex: 0, textProgress: 0, stepProgress: 0 });
    setDuration(0);
  };

  return (
    <div className="w-full h-full max-w-5xl flex flex-col gap-6">
      <div className="flex flex-col gap-4 rounded-xl border border-border/60 bg-background/80 p-4">
        <div className="text-sm font-medium text-foreground">配置项</div>

        <details
          className="group rounded-lg border border-border/60 bg-background p-3"
          open
        >
          <summary className="flex cursor-pointer items-center justify-between text-sm font-medium text-foreground">
            基础信息
            <ChevronRight className="size-4 transition-transform group-open:rotate-90" />
          </summary>
          <div className="mt-3 grid gap-3 md:grid-cols-2">
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              标题
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                value={title}
                onChange={(event) => setTitle(event.target.value)}
              />
            </label>
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              右侧文案(headerMeta)
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                placeholder="如:耗时 12s / 已使用 3 个工具"
                value={headerMeta}
                onChange={(event) => setHeaderMeta(event.target.value)}
                disabled={!useHeaderMeta}
              />
            </label>
            <label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
              <input
                type="checkbox"
                checked={useHeaderMeta}
                onChange={(event) => setUseHeaderMeta(event.target.checked)}
              />
              启用 headerMeta(否则展示 duration)
            </label>
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              时长(秒)
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                type="number"
                min={0}
                value={duration}
                onChange={(event) =>
                  setDuration(Number(event.target.value) || 0)
                }
              />
            </label>
          </div>
        </details>

        <details className="group rounded-lg border border-border/60 bg-background p-3">
          <summary className="flex cursor-pointer items-center justify-between text-sm font-medium text-foreground">
            体验与提示
            <ChevronRight className="size-4 transition-transform group-open:rotate-90" />
          </summary>
          <div className="mt-3 grid gap-3 md:grid-cols-2">
            <label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
              <input
                type="checkbox"
                checked={longRunning}
                onChange={(event) => setLongRunning(event.target.checked)}
              />
              longRunning
            </label>
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              hint
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                placeholder="可选,留空则走默认提示"
                value={hint}
                onChange={(event) => setHint(event.target.value)}
              />
            </label>
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              labels.longRunningHint
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                placeholder="覆盖默认长耗时提示文案"
                value={longRunningHint}
                onChange={(event) => setLongRunningHint(event.target.value)}
              />
            </label>
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              labels.cancelledStepTitle
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                placeholder="取消时追加步骤标题"
                value={cancelledStepTitle}
                onChange={(event) => setCancelledStepTitle(event.target.value)}
              />
            </label>
          </div>
        </details>

        <details className="group rounded-lg border border-border/60 bg-background p-3">
          <summary className="flex cursor-pointer items-center justify-between text-sm font-medium text-foreground">
            外观与可访问性
            <ChevronRight className="size-4 transition-transform group-open:rotate-90" />
          </summary>
          <div className="mt-3 grid gap-3 md:grid-cols-2">
            <label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
              <input
                type="checkbox"
                checked={useCustomIcon}
                onChange={(event) => setUseCustomIcon(event.target.checked)}
              />
              icon(自定义 Loading 图标)
            </label>
            <label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
              <input
                type="checkbox"
                checked={useCustomArrow}
                onChange={(event) => setUseCustomArrow(event.target.checked)}
              />
              arrowIcon(自定义折叠箭头)
            </label>
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              triggerId
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                placeholder="可选,自定义 aria-controls"
                value={triggerId}
                onChange={(event) => setTriggerId(event.target.value)}
              />
            </label>
            <label className="flex flex-col gap-2 text-xs text-muted-foreground">
              contentId
              <input
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                placeholder="可选,自定义 aria-labelledby"
                value={contentId}
                onChange={(event) => setContentId(event.target.value)}
              />
            </label>
          </div>
        </details>

        <details
          className="group rounded-lg border border-border/60 bg-background p-3"
          open
        >
          <summary className="flex cursor-pointer items-center justify-between text-sm font-medium text-foreground">
            流式控制
            <ChevronRight className="size-4 transition-transform group-open:rotate-90" />
          </summary>
          <div className="mt-3 flex flex-col gap-3">
            <label className="flex flex-col gap-2 text-xs text-muted-foreground md:max-w-[240px]">
              速度
              <select
                className="h-9 rounded-md border border-border/60 bg-background px-3 text-sm text-foreground"
                value={speed}
                onChange={(event) =>
                  setSpeed(event.target.value as StreamSpeed)
                }
              >
                <option value="slow">慢</option>
                <option value="medium">中</option>
                <option value="fast">快</option>
              </select>
            </label>
            <div className="flex flex-wrap gap-2">
              <button
                type="button"
                className="h-9 rounded-md border border-border/60 bg-background px-4 text-sm text-foreground"
                onClick={handleStart}
              >
                开始流式
              </button>
              <button
                type="button"
                className="h-9 rounded-md border border-border/60 bg-background px-4 text-sm text-foreground"
                onClick={handleStop}
              >
                暂停
              </button>
              <button
                type="button"
                className="h-9 rounded-md border border-border/60 bg-background px-4 text-sm text-foreground"
                onClick={handleComplete}
              >
                一键完成
              </button>
              <button
                type="button"
                className="h-9 rounded-md border border-border/60 bg-background px-4 text-sm text-foreground"
                onClick={handleReset}
              >
                重置
              </button>
            </div>
            <div className="flex items-center gap-2 text-xs text-muted-foreground">
              状态:
              <span className="text-foreground">{resolvedStatus}</span>
              <span>·</span>
              <span>阶段:{phases[streamState.phaseIndex]?.key ?? "完成"}</span>
              <span>·</span>
              <span>{isStreaming ? "流式中" : "已暂停"}</span>
            </div>
          </div>
        </details>
      </div>

      <div className="rounded-xl border border-border/60 bg-muted/30 p-4">
        <div className="mb-3 text-xs text-muted-foreground">
          AI 消息预览(ThinkingStep 内嵌)
        </div>
        <div className="flex flex-col gap-3 rounded-lg border border-border/40 bg-background p-4">
          <ThinkingStep
            status={resolvedStatus}
            title={resolvedTitle}
            duration={duration}
            headerMeta={resolvedHeaderMeta}
            longRunning={longRunning}
            hint={resolvedHint}
            labels={resolvedLabels}
            triggerId={triggerId.trim() || undefined}
            contentId={contentId.trim() || undefined}
            icon={useCustomIcon ? <ThinkingLoadingDotsPrimitive /> : undefined}
            arrowIcon={
              useCustomArrow ? <ChevronRight className="size-4" /> : undefined
            }
            contentBlocks={contentBlocks}
          />
        </div>
      </div>
    </div>
  );
}

Primitives (Advanced)

需要高度定制时使用 primitives:

import {
  ThinkingStepPrimitive,
  ThinkingStepHeaderPrimitive,
  ThinkingStepContentPrimitive,
  ThinkingStatusLabelPrimitive,
  ThinkingIconContainerPrimitive,
  ThinkingTimeLabelPrimitive,
  ThinkingCollapseArrowPrimitive,
} from "@/registry/wuhan/blocks/thinking-process/thinking-process-01";
pnpm dlx shadcn@latest add http://localhost:3000/r/wuhan/thinking-process-01.json

Design Tokens

组件只消费全局 token(如 --text-* / --bg-* / --gap-* / --radius-*)。