AI 應用元件

AI 對話分支切換

同一則 AI 回覆多版本切換的 React 元件:‹ 2/3 › 分頁器切換時內容依方向左右滑動、重新生成即開新分支並顯示打字骨架、氣泡上方可選迷你分支樹、焦點在氣泡內按 ←/→ 換版本,含複製與模型/時間小字。

把同一則回覆的多個版本收在一顆助理氣泡裡:底部工具列是「‹ 2/3 ›」分頁器、重新生成與複製按鈕,右側是模型與時間小字。切換時內容用 AnimatePresence 依方向滑動——往後翻新版本從右邊進、舊的往左退,往回翻則相反;按重新生成會呼叫 onRegenerate,你在串流期間把 isGenerating 設為 true,內容區就換成打字骨架、分頁器與分支樹末端預留一格,新版本推進 versions 後從右側滑進來。開啟 showTree 會在氣泡上方畫一條主線加每個版本一個小圓點,目前版本高亮並可點擊切換;焦點在氣泡內時 ←/→(與 Home/End)也能換版本。索引支援受控/非受控。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add ai-branch-switcher

還沒有專案,或安裝遇到問題?看快速開始 →

AI Branch Switcher 是用 React、Tailwind CSS 與 Motion 做的 AI 聊天回覆版本切換元件,也就是 AI 助理重新生成回答後出現的「‹ 2/3 ›」分頁器,圖示來自 lucide-react。元件只負責呈現與切換,資料與請求都由你的程式掌控。

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

編輯中的呼叫預設屬性
<AiBranchSwitcher/>

完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。

適用情境

  • AI 聊天產品中,讓使用者比較同一個問題重新生成的多個回答
  • 文案、標題、Email 草稿產生器,保留每次產出的版本供挑選
  • 客服輔助工具裡,客服人員在幾個建議回覆之間切換後再送出
  • 多模型比較時,把不同模型的回答收在同一顆氣泡(用 model 欄位標示來源)

安裝

npx webberui@latest add https://webberui.com/r/ai-branch-switcher.json

或在 components.json 設定 registries 後,改用 @webberui/ai-branch-switcher 安裝。

使用

import { AiBranchSwitcher, type BranchVersion } from "@/components/ui/ai-branch-switcher";

// 內容與模型名皆為虛構示範
const [versions, setVersions] = React.useState<BranchVersion[]>([
  { id: "v1", model: "Aurora-7", createdAt: "09:12", content: "【專業版】……" },
  { id: "v2", model: "Aurora-7", createdAt: "09:13", content: "【輕鬆版】……" },
]);
const [index, setIndex] = React.useState(1);
const [generating, setGenerating] = React.useState(false);

<AiBranchSwitcher
  versions={versions}
  index={index}
  onIndexChange={setIndex}
  isGenerating={generating}
  showTree
  onRegenerate={async () => {
    setGenerating(true);
    const content = await regenerate(); // 你的串流/請求
    // 同一批更新:推入新版本、索引跳到最後、結束產生中
    setVersions((prev) => [...prev, { id: crypto.randomUUID(), content, model: "Aurora-7" }]);
    setIndex(versions.length);
    setGenerating(false);
  }}
/>

範例

非受控+緊湊模式

不需要記錄目前看第幾版時,只傳 versions 與 defaultIndex;compact 適合側欄或窄版聊天視窗。

<AiBranchSwitcher
  compact
  defaultIndex={0}
  versions={[
    { id: "a1", model: "Aurora-7", createdAt: "14:32", content: "可以,退貨期限是收到商品後 7 天內。" },
    { id: "a2", model: "Aurora-7", createdAt: "14:33", content: "收到商品 7 天內都能申請退貨,請到「我的訂單」點選申請。" },
  ]}
/>

非受控模式下 versions 變多時,元件會自動切到最新版本並觸發 onIndexChange。

自訂內容渲染

renderContent 可以換成你自己的渲染方式;想沿用預設的段落樣式,可在裡面呼叫具名匯出的 renderPlainParagraphs。

import {
  AiBranchSwitcher,
  renderPlainParagraphs,
} from "@/components/ui/ai-branch-switcher";

<AiBranchSwitcher
  versions={versions}
  renderContent={(version) => (
    <div className="space-y-3 text-sm">
      {renderPlainParagraphs(version.content)}
      {version.model === "Aurora-7-mini" && (
        <p className="text-xs text-neutral-500">此版本由輕量模型產生</p>
      )}
    </div>
  )}
/>

改寫介面文案

labels 可以只覆寫部分欄位;position 是函式,會同時用在分頁器與螢幕閱讀器的宣告。

<AiBranchSwitcher
  versions={versions}
  onRegenerate={handleRegenerate}
  regenerateLabel="換個說法"
  labels={{
    copied: "已複製到剪貼簿",
    position: (current, total) => `回答 ${current}/${total}`,
  }}
/>

Props

Prop型別預設值說明
versionsBranchVersion[]—同一則回覆的所有版本(分支),依產生順序排列
indexnumber—受控目前索引(0 起算);提供時由外部掌控
defaultIndexnumber0非受控初始索引(0 起算)
onIndexChange(index: number) => void—索引變動時回呼(分頁器、分支樹、鍵盤,以及非受控自動跟到最新版本時都會觸發)
onRegenerate() => void—點擊「重新生成」時回呼;未提供時不顯示重新生成按鈕
isGeneratingbooleanfalse是否正在產生尚未加入 versions 的新分支:內容區改顯示打字骨架、分頁器與分支樹末端預留一格、工具列停用
showTreebooleanfalse是否在氣泡上方顯示迷你分支樹(一條主線+每個版本一個小圓點,可點擊切換)
compactbooleanfalse緊湊模式:縮小內距、字級與工具列
renderContent(version: BranchVersion) => React.ReactNode—自訂內容渲染(例如接 Markdown);未提供時把純文字依空行切成段落
regenerateLabelstring—「重新生成」按鈕文字;等同 labels.regenerate 的捷徑,兩者同時提供時以此為準
labelsPartial<AiBranchSwitcherLabels>—介面文案,可覆寫任一項
classNamestring—透傳到最外層容器

BranchVersion

欄位型別預設值說明
idstring—唯一識別碼,作為 React key 與滑動切換動畫的邊界
contentstring—回覆內容;預設以段落渲染純文字(空行分段),可用 renderContent 換成自訂渲染
createdAtstring—顯示用的產生時間文字(例如「14:32」);元件不做時區換算,原樣顯示
modelstring—產生此版本的模型名稱,顯示在工具列右側小字

AiBranchSwitcherLabels

欄位型別預設值說明
regeneratestring"重新生成"「重新生成」按鈕文字
previousstring"上一個版本"「上一個版本」按鈕的無障礙標籤
nextstring"下一個版本"「下一個版本」按鈕的無障礙標籤
copystring"複製這個版本"複製按鈕的無障礙標籤
copiedstring"已複製"複製成功後短暫顯示的文字
generatingstring"正在產生新的版本"生成中的提示文字(骨架下方與 aria-live 宣告)
treestring"回覆版本分支"分支樹區域的無障礙標籤
emptystring"尚無回覆版本"沒有任何版本時顯示的文字
position(current: number, total: number) => string第 ${current} 個版本,共 ${total} 個分頁器與 aria-live 宣告的位置文字

另外具名匯出 clampBranchIndex(index, count)(把索引夾在合法範圍)與 renderPlainParagraphs(content)(預設的純文字分段渲染),想在自訂 renderContent 裡沿用預設段落樣式時可直接呼叫。

細節

  • 方向感:切換方向在 render 期間依「上一次的索引」推導,受控、非受控與外部直接改 index 都正確;新版本從右進、舊的往左出,往回翻則相反。開始產生新分支時一律視為往前,避免上一次往回翻時骨架與舊內容同側進出撞在一起。
  • 重新生成的資料流:元件本身不打 API——按鈕只呼叫 onRegenerate。你在請求期間把 isGenerating 設為 true(內容區換成骨架、分頁器顯示「4/4」預留格),拿到結果後同一批推入 versions、把 index 指到最後、isGenerating 設回 false,新版本就會從右側滑進來。
  • 非受控自動跟隨:沒有傳 index 時,versions 變多會自動切到最新版本並觸發 onIndexChange;受控模式交由外部決定要不要跳。
  • 內容高度:切換用 AnimatePresence 的 popLayout,新舊版本同時在場、退場的一份被 overflow-hidden 裁掉;氣泡高度隨新版本內容直接更新,不做高度動畫,長短差很多的版本也不會拖慢切換。
  • 時間文字:createdAt 是顯示用字串,元件不解析、不做時區換算——由你決定要顯示「14:32」還是「昨天 09:10」,SSR 與客戶端輸出一致。
  • 複製:使用 navigator.clipboard.writeText,非 HTTPS 或使用者拒絕權限時靜默失敗(內容仍可手動選取);成功後圖示切成勾勾 1.6 秒。

可及性

  • 氣泡為 role="group"、aria-roledescription="回覆版本"、aria-label 為目前位置(「第 2 個版本,共 3 個」),整顆可聚焦(tabIndex=0)並有 focus-visible ring;焦點在氣泡內時 ←/→ 切換上一個/下一個版本,Home/End 跳到最前/最後
  • 分頁器兩顆按鈕都是原生 button(type="button")並帶 aria-label(可用 labels.previous/labels.next 換語言),在邊界時 disabled;分頁器群組本身也帶位置文字,視覺上的「2/3」對輔助科技隱藏以免重複朗讀
  • 分支樹的每個小圓點是 button,aria-label 為該版本的位置文字,目前版本加 aria-current="true";產生中時整組停用
  • 另有 role="status"、aria-live="polite" 的隱藏區域,切換版本時朗讀新位置、產生新分支時朗讀 labels.generating;產生中氣泡加 aria-busy
  • 使用者系統開啟「減少動態效果」時,停用左右滑動、模糊、數字滾動、分支樹高亮環的位移與骨架閃爍,切換只保留淡入淡出

常見問題

元件會自己呼叫 AI API 嗎?

不會。按下重新生成只會呼叫 onRegenerate,請求、串流與把新版本加進 versions 都由你處理;請求期間把 isGenerating 設為 true 就會顯示打字骨架。

可以在 Next.js App Router 使用嗎?

可以。檔案開頭已經有 "use client",在 Server Component 頁面裡照一般方式 import 即可。減少動態效果的判斷會等掛載後才套用,首次渲染與 SSR 輸出一致。

能顯示 Markdown 嗎?

預設只把純文字依空行切成段落。要顯示 Markdown,在 renderContent 裡接你專案使用的 Markdown 渲染器即可。

複製按鈕在什麼情況下沒有作用?

複製使用 navigator.clipboard.writeText,在非 HTTPS 環境或使用者拒絕權限時會靜默失敗,內容仍可手動選取。

需要哪些依賴?

motion、lucide-react、clsx 與 tailwind-merge,另外需要 @/lib/utils 的 cn。用 CLI 安裝會自動處理;不用 CLI 的話,照「手動」分頁複製原始碼也可以。