AI 對話分支切換(React)
同一則 AI 回覆多版本切換的 React 元件:‹ 2/3 › 分頁器切換時內容依方向左右滑動、重新生成即開新分支並顯示打字骨架、氣泡上方可選迷你分支樹、焦點在氣泡內按 ←/→ 換版本,含複製與模型/時間小字。
把同一則回覆的多個版本收在一顆助理氣泡裡:底部工具列是「‹ 2/3 ›」分頁器、重新生成與複製按鈕,右側是模型與時間小字。切換時內容用 AnimatePresence 依方向滑動——往後翻新版本從右邊進、舊的往左退,往回翻則相反;按重新生成會呼叫 onRegenerate,你在串流期間把 isGenerating 設為 true,內容區就換成打字骨架、分頁器與分支樹末端預留一格,新版本推進 versions 後從右側滑進來。開啟 showTree 會在氣泡上方畫一條主線加每個版本一個小圓點,目前版本高亮並可點擊切換;焦點在氣泡內時 ←/→(與 Home/End)也能換版本。索引支援受控/非受控。
載入預覽⋯
npx shadcn@latest add https://webberui.com/r/ai-branch-switcher.jsonPlayground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<AiBranchSwitcher />
安裝
npx shadcn@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);
}}
/>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
versions | BranchVersion[] | — | 同一則回覆的所有版本(分支),依產生順序排列 |
index | number | — | 受控目前索引(0 起算);提供時由外部掌控 |
defaultIndex | number | 0 | 非受控初始索引(0 起算) |
onIndexChange | (index: number) => void | — | 索引變動時回呼(分頁器、分支樹、鍵盤,以及非受控自動跟到最新版本時都會觸發) |
onRegenerate | () => void | — | 點擊「重新生成」時回呼;未提供時不顯示重新生成按鈕 |
isGenerating | boolean | false | 是否正在產生尚未加入 versions 的新分支:內容區改顯示打字骨架、分頁器與分支樹末端預留一格、工具列停用 |
showTree | boolean | false | 是否在氣泡上方顯示迷你分支樹(一條主線+每個版本一個小圓點,可點擊切換) |
compact | boolean | false | 緊湊模式:縮小內距、字級與工具列 |
renderContent | (version: BranchVersion) => React.ReactNode | — | 自訂內容渲染(例如接 Markdown);未提供時把純文字依空行切成段落 |
regenerateLabel | string | — | 「重新生成」按鈕文字;等同 labels.regenerate 的捷徑,兩者同時提供時以此為準 |
labels | Partial<AiBranchSwitcherLabels> | — | 介面文案,可覆寫任一項 |
className | string | — | 透傳到最外層容器 |
BranchVersion
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
id | string | — | 唯一識別碼,作為 React key 與滑動切換動畫的邊界 |
content | string | — | 回覆內容;預設以段落渲染純文字(空行分段),可用 renderContent 換成自訂渲染 |
createdAt | string | — | 顯示用的產生時間文字(例如「14:32」);元件不做時區換算,原樣顯示 |
model | string | — | 產生此版本的模型名稱,顯示在工具列右側小字 |
AiBranchSwitcherLabels
| 欄位 | 型別 | 預設值 | 說明 |
|---|---|---|---|
regenerate | string | "重新生成" | 「重新生成」按鈕文字 |
previous | string | "上一個版本" | 「上一個版本」按鈕的無障礙標籤 |
next | string | "下一個版本" | 「下一個版本」按鈕的無障礙標籤 |
copy | string | "複製這個版本" | 複製按鈕的無障礙標籤 |
copied | string | "已複製" | 複製成功後短暫顯示的文字 |
generating | string | "正在產生新的版本" | 生成中的提示文字(骨架下方與 aria-live 宣告) |
tree | string | "回覆版本分支" | 分支樹區域的無障礙標籤 |
empty | string | "尚無回覆版本" | 沒有任何版本時顯示的文字 |
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-visiblering;焦點在氣泡內時 ←/→ 切換上一個/下一個版本,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 - 使用者系統開啟「減少動態效果」時,停用左右滑動、模糊、數字滾動、分支樹高亮環的位移與骨架閃爍,切換只保留淡入淡出