Skip to content

refactor(recordings): align transcription with shadcn UI - #30

Open
MapleEve wants to merge 1 commit into
mini/sot-shadcn-framework-20260720from
mini/sot-recording-transcription-integration-20260730
Open

refactor(recordings): align transcription with shadcn UI#30
MapleEve wants to merge 1 commit into
mini/sot-shadcn-framework-20260720from
mini/sot-recording-transcription-integration-20260730

Conversation

@MapleEve

Copy link
Copy Markdown
Owner

Summary

  • align recording transcription states and skeletons with shared shadcn primitives
  • replace stale transcription source-shape guards with rendered semantic coverage
  • preserve latest shared Speaker, Route, Library, Onboarding, and DataSource coverage

Validation

  • production Biome error-on-warnings: pass
  • shared-test Biome: baseline 113 warnings / 6 infos; candidate 110 / 5; no formatter errors
  • clean and candidate canonical: 776 passed / 14 failed / 3 skipped; failure-name sets identical
  • focused Speaker/Route/Library/Onboarding/DataSource: 51/51 passed
  • focused/runtime transcription: 56/56 passed
  • System Chrome real Next/SQLite E2E: 7/7 passed
  • type-check and diff check: pass

No merge performed.

Copilot AI review requested due to automatic review settings July 30, 2026 16:40
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

❌ 16 Tests Failed:

Tests completed Failed Passed Skipped
790 16 774 3
View the top 3 failed test(s) by shortest run time
src/tests/recording-detail-copy-ui-regression.test.ts > recording detail copy and title action UI regressions > keeps standalone recording route fallback states in the new shell
Stack Traces | 0.00208s run time
AssertionError: expected -1 to be greater than or equal to 0
 ❯ extractOpeningElement src/tests/recording-detail-copy-ui-regression.test.ts:797:25
 ❯ src/tests/recording-detail-copy-ui-regression.test.ts:3167:53
src/tests/full-ui-replacement-regression.test.ts > full UI replacement regression coverage > keeps recording route loading skeleton sizing route-local and off the Skeleton primitive
Stack Traces | 0.0026s run time
AssertionError: expected -1 to be greater than or equal to 0
 ❯ extractBoundedSlice src/tests/full-ui-replacement-regression.test.ts:880:19
 ❯ src/tests/full-ui-replacement-regression.test.ts:5129:52
src/tests/full-ui-replacement-regression.test.ts > full UI replacement regression coverage > keeps system banner shared primitives free of feature business tokens
Stack Traces | 0.00557s run time
AssertionError: expected '"use client";\n\nimport {\n    Downlo…' to contain 'const systemBannerAlertClassNames'

- Expected
+ Received

- const systemBannerAlertClassNames
+ "use client";
+
+ import {
+     Download,
+     LockKeyhole,
+     Package,
+     Search,
+     ShieldX,
+     Upload,
+     WifiOff,
+     X,
+ } from "lucide-react";
+ import {
+     type ComponentProps,
+     type ReactNode,
+     useEffect,
+     useState,
+ } from "react";
+ import { useLanguage } from "@/components/language-provider";
+ import { Alert, AlertDescription, AlertTitle } from "@.../components/ui/alert";
+ import { Button, type ButtonProps } from "@.../components/ui/button";
+ import { Progress } from "@.../components/ui/progress";
+ import { hasBrowserWindow } from "@.../lib/platform/runtime";
+
+ type SystemBannerState =
+     | "offline"
+     | "permission-denied"
+     | "db-locked"
+     | "runtime-unavailable"
+     | "update-available"
+     | "import-progress"
+     | "export-progress";
+
+ interface SystemBannerEventDetail {
+     actionLabel?: string;
+     dismissLabel?: string;
+     id?: string;
+     indeterminate?: boolean;
+     message?: string;
+     progress?: number;
+     secondaryActionLabel?: string;
+     state: SystemBannerState | null;
+     title?: string;
+ }
+
+ type VisibleSystemBanner = SystemBannerEventDetail & {
+     state: SystemBannerState;
+ };
+
+ interface SystemBannerProps {
+     className?: string;
+ }
+
+ interface SystemBannerDefaultActions {
+     actionLabel?: string;
+     dismissLabel?: string;
+     secondaryActionLabel?: string;
+ }
+
+ type SystemBannerActionRole = "primary" | "secondary";
+ type SystemBannerButtonTone = "action" | "primary" | "dismiss";
+
+ interface SystemBannerItemProps {
+     banner: VisibleSystemBanner;
+     className?: string;
+     isStacked: boolean;
+     isZh: boolean;
+     onDismiss: (banner: VisibleSystemBanner) => void;
+ }
+
+ interface SystemBannerAlertProps {
+     a11y: ReturnType<typeof getBannerA11y>;
+     banner: VisibleSystemBanner;
+     children: ReactNode;
+     className?: string;
+ }
+
+ type SystemBannerButtonProps = Omit<ButtonProps, "size" | "variant"> & {
+     tone?: SystemBannerButtonTone;
+ };
+
+ type SystemBannerAlertVariant = NonNullable<
+     Parameters<typeof Alert>[0]["variant"]
+ >;
+ type SystemBannerIconProps = Omit<ComponentProps<typeof WifiOff>, "children">;
+
+ interface SystemBannerProgressProps {
+     indeterminate: boolean | undefined;
+     value: number;
+ }
+
+ const systemBannerAlertVariantByState: Record<
+     SystemBannerState,
+     SystemBannerAlertVariant
+ > = {
+     "db-locked": "destructiveSoftNeutral",
+     "runtime-unavailable": "destructiveSoftNeutral",
+     "export-progress": "default",
+     "import-progress": "default",
+     offline: "default",
+     "permission-denied": "destructiveSoftNeutral",
+     "update-available": "default",
+ } as const;
+
+ function getDefaultCopy(state: SystemBannerState, isZh: boolean) {
+     switch (state) {
+         case "offline":
+             return {
+                 title: isZh ? "当前无网络连接" : "Offline",
+                 message: isZh
+                     ? "所有已下载的录音与逐字稿可继续阅览 · 来源同步与新转写已暂停。"
+                     : "Downloaded recordings and transcripts remain readable. Source sync and new transcription are paused.",
+             };
+         case "permission-denied":
+             return {
+                 title: isZh
+                     ? "未授权访问录音文件夹"
+                     : "Recording folder permission denied",
+                 message: isZh
+                     ? "无法读取来源缓存目录 · 前往「系统设置 · 隐私与安全性 · 完全磁盘访问」打开开关。"
+                     : "The source cache folder cannot be read. Open System Settings > Privacy & Security > Full Disk Access.",
+             };
+         case "db-locked":
+             return {
+                 title: isZh
+                     ? "本地数据库被另一个 BetterAINote 实例占用"
+                     : "Local database is locked by another BetterAINote instance",
+                 message: isZh
+                     ? "同时只允许一个实例写入 · 当前实例已切到只读模式 · 关闭其它窗口后点「重新连接」。"
+                     : "Only one instance can write at a time. This instance is read-only until other windows close.",
+             };
+         case "runtime-unavailable":
+             return {
+                 title: isZh
+                     ? "同步运行时暂时不可用"
+                     : "Sync runtime is temporarily unavailable",
+                 message: isZh
+                     ? "自动同步暂时无法运行。已下载的录音仍可阅览,稍后可重新尝试同步。"
+                     : "Automatic sync is temporarily unavailable. Downloaded recordings remain readable and you can retry sync shortly.",
+             };
+         case "update-available":
+             return {
+                 title: isZh
+                     ? "BetterAINote 有可用更新"
+                     : "BetterAINote update available",
+                 message: isZh
+                     ? "重启后将应用最新版本。"
+                     : "Restart to apply the latest version.",
+             };
+         case "import-progress":
+             return {
+                 title: isZh
+                     ? "正在导入 BetterAINote 备份包"
+                     : "Importing BetterAINote backup",
+                 message: isZh
+                     ? "录音和来源内容正在写入本地。"
+                     : "Recordings and source content are being saved locally.",
+             };
+         case "export-progress":
+             return {
+                 title: isZh ? "正在导出录音" : "Exporting recordings",
+                 message: isZh
+                     ? "导出文件准备中,请保持当前页面打开。"
+                     : "Export files are being prepared. Keep this page open.",
+             };
+     }
+ }
+
+ function getStackedCopy(state: SystemBannerState, isZh: boolean) {
+     switch (state) {
+         case "offline":
+             return {
+                 title: isZh ? "当前无网络连接" : "Offline",
+                 message: isZh
+                     ? "来源同步已暂停 · 已下载的录音仍可阅览。"
+                     : "Source sync is paused. Downloaded recordings remain readable.",
+             };
+         case "update-available":
+             return {
+                 title: isZh ? "有可用更新" : "Update available",
+                 message: isZh
+                     ? "重启后将应用。"
+                     : "It will be applied after restart.",
+             };
+         default:
+             return getDefaultCopy(state, isZh);
+     }
+ }
+
+ function getPriority(state: SystemBannerState) {
+     switch (state) {
+         case "permission-denied":
+         case "db-locked":
+         case "runtime-unavailable":
+             return 0;
+         case "offline":
+             return 1;
+         case "import-progress":
+         case "export-progress":
+             return 2;
+         case "update-available":
+             return 3;
+     }
+ }
+
+ function getBannerA11y(state: SystemBannerState): {
+     "aria-live"?: "polite";
+     role?: "alert" | "status";
+ } {
+     if (state === "offline") {
+         return { "aria-live": "polite" as const, role: "status" as const };
+     }
+     if (
+         state === "permission-denied" ||
+         state === "db-locked" ||
+         state === "runtime-unavailable"
+     ) {
+         return { role: "alert" as const };
+     }
+     return {};
+ }
+
+ function normalizeProgress(progress: number | undefined) {
+     if (typeof progress !== "number" || Number.isNaN(progress)) return null;
+     const clamped = Math.min(100, Math.max(0, progress));
+     return Math.round(clamped / 10) * 10;
+ }
+
+ function getDefaultActions(
+     state: SystemBannerState,
+     isZh: boolean,
+ ): SystemBannerDefaultActions {
+     switch (state) {
+         case "offline":
+             return {
+                 actionLabel: isZh ? "重试" : "Retry",
+                 dismissLabel: isZh ? "收起" : "Dismiss",
+             };
+         case "permission-denied":
+             return {
+                 actionLabel: isZh ? "打开系统设置" : "Open System Settings",
+                 secondaryActionLabel: isZh ? "稍后" : "Later",
+             };
+         case "db-locked":
+             return {
+                 actionLabel: isZh ? "重新连接" : "Reconnect",
+                 secondaryActionLabel: isZh ? "只读继续" : "Continue read-only",
+             };
+         case "runtime-unavailable":
+             return {
+                 actionLabel: isZh ? "重试同步" : "Retry sync",
+                 dismissLabel: isZh ? "收起" : "Dismiss",
+             };
+         case "update-available":
+             return {
+                 actionLabel: isZh ? "重启并更新" : "Restart and update",
+                 secondaryActionLabel: isZh ? "查看更新内容" : "View changes",
+                 dismissLabel: isZh ? "稍后再说" : "Later",
+             };
+         case "import-progress":
+             return {
+                 actionLabel: isZh ? "暂停" : "Pause",
+                 secondaryActionLabel: isZh ? "取消" : "Cancel",
+             };
+         case "export-progress":
+             return {
+                 actionLabel: isZh ? "在 Finder 中显示" : "Show in Finder",
+                 secondaryActionLabel: isZh ? "取消" : "Cancel",
+             };
+     }
+ }
+
+ function SystemBannerIcon({
+     indeterminate,
+     state,
+     ...props
+ }: {
+     indeterminate: boolean | undefined;
+     state: SystemBannerState;
+ } & SystemBannerIconProps) {
+     if (state === "import-progress" && indeterminate) {
+         return <Search {...props} />;
+     }
+
+     switch (state) {
+         case "offline":
+             return <WifiOff {...props} />;
+         case "permission-denied":
+             return <ShieldX {...props} />;
+         case "db-locked":
+         case "runtime-unavailable":
+             return <LockKeyhole {...props} />;
+         case "update-available":
+             return <Package {...props} />;
+         case "import-progress":
+             return <Upload {...props} />;
+         case "export-progress":
+             return <Download {...props} />;
+     }
+ }
+
+ function SystemBannerAlert({
+     a11y,
+     banner,
+     children,
+     className,
+ }: SystemBannerAlertProps) {
+     return (
+         <Alert
+             aria-live={a11y["aria-live"]}
+             data-control="system-banner"
+             data-state={banner.state}
+             density="comfortable"
+             layout="inline"
+             role={a11y.role}
+             variant={systemBannerAlertVariantByState[banner.state]}
+             className={className}
+         >
+             {children}
+         </Alert>
+     );
+ }
+
+ function SystemBannerButton({
+     className,
+     tone = "action",
+     ...props
+ }: SystemBannerButtonProps) {
+     return (
+         <Button
+             size="sm"
+             variant={tone === "primary" ? "outline" : "ghost"}
+             className={className}
+             {...props}
+         />
+     );
+ }
+
+ function SystemBannerProgress({
+     indeterminate,
+     value,
+ }: SystemBannerProgressProps) {
+     return (
+         <Progress
+             aria-hidden="true"
+             className="min-w-[120px] flex-1"
+             indicatorClassName={
+                 indeterminate
+                     ? "w-[32%] animate-[sbn-sweep_1.4s_linear_infinite]"
+                     : undefined
+             }
+             value={value}
+         />
+     );
+ }
+
+ function getRenderedActions(
+     banner: VisibleSystemBanner,
+     defaultActions: SystemBannerDefaultActions,
+     isStacked: boolean,
+     isZh: boolean,
+ ) {
+     if (banner.indeterminate && banner.state === "import-progress") {
+         return {
+             primaryLabel:
+                 banner.actionLabel ??
+                 banner.secondaryActionLabel ??
+                 defaultActions.secondaryActionLabel,
+             primaryRole: "primary" as const,
+             secondaryLabel: undefined,
+             dismissLabel: undefined,
+         };
+     }
+
+     if (isStacked) {
+         return {
+             primaryLabel:
+                 banner.state === "update-available"
+                     ? (banner.secondaryActionLabel ??
+                       banner.actionLabel ??
+                       (isZh ? "查看" : "View"))
+                     : (banner.actionLabel ?? defaultActions.actionLabel),
+             primaryRole:
+                 banner.state === "update-available" && !banner.actionLabel
+                     ? ("secondary" as const)
+                     : ("primary" as const),
+             secondaryLabel: undefined,
+             dismissLabel:
+                 banner.state === "runtime-unavailable"
+                     ? (banner.dismissLabel ?? defaultActions.dismissLabel)
+                     : undefined,
+         };
+     }
+
+     return {
+         primaryLabel: banner.actionLabel ?? defaultActions.actionLabel,
+         primaryRole: "primary" as const,
+         secondaryLabel:
+             banner.secondaryActionLabel ?? defaultActions.secondaryActionLabel,
+         dismissLabel: banner.dismissLabel ?? defaultActions.dismissLabel,
+     };
+ }
+
+ function getSystemBannerActionName(
+     state: SystemBannerState,
+     role: SystemBannerActionRole,
+ ) {
+     if (role === "secondary") {
+         switch (state) {
+             case "permission-denied":
+                 return "later";
+             case "db-locked":
+                 return "continue-read-only";
+             case "runtime-unavailable":
+                 return "dismiss";
+             case "update-available":
+                 return "view-update-changes";
+             case "import-progress":
+                 return "cancel-import";
+             case "export-progress":
+                 return "cancel-export";
+             case "offline":
+                 return "dismiss";
+         }
+     }
+
+     switch (state) {
+         case "offline":
+             return "retry";
+         case "permission-denied":
+             return "open-system-settings";
+         case "db-locked":
+             return "reconnect";
+         case "runtime-unavailable":
+             return "retry-sync";
+         case "update-available":
+             return "restart-and-update";
+         case "import-progress":
+             return "pause-import";
+         case "export-progress":
+             return "show-export";
+     }
+ }
+
+ function dispatchSystemBannerAction(
+     banner: VisibleSystemBanner,
+     role: SystemBannerActionRole,
+ ) {
+     if (!hasBrowserWindow()) {
+         return;
+     }
+
+     window.dispatchEvent(
+         new CustomEvent("betterainote:system-banner-action", {
+             detail: {
+                 action: getSystemBannerActionName(banner.state, role),
+                 id: banner.id ?? banner.state,
+                 role,
+                 state: banner.state,
+             },
+         }),
+     );
+ }
+
+ function SystemBannerItem({
+     banner,
+     className,
+     isStacked,
+     isZh,
+     onDismiss,
+ }: SystemBannerItemProps) {
+     const defaultCopy = isStacked
+         ? getStackedCopy(banner.state, isZh)
+         : getDefaultCopy(banner.state, isZh);
+     const defaultActions = getDefaultActions(banner.state, isZh);
+     const progress = normalizeProgress(banner.progress);
+     const hasProgress =
+         banner.state === "import-progress" ||
+         banner.state === "export-progress";
+     const { dismissLabel, primaryLabel, primaryRole, secondaryLabel } =
+         getRenderedActions(banner, defaultActions, isStacked, isZh);
+     const bannerA11y = getBannerA11y(banner.state);
+     const primaryActionTone =
+         banner.state === "update-available" && !isStacked
+             ? "primary"
+             : "action";
+     const handleAction = (role: SystemBannerActionRole) => {
+         dispatchSystemBannerAction(banner, role);
+
+         if (role === "primary" && banner.state === "update-available") {
+             if (hasBrowserWindow()) {
+                 window.location.reload();
+             }
+             return;
+         }
+
+         if (role === "secondary") {
+             onDismiss(banner);
+         }
+     };
+
+     return (
+         <SystemBannerAlert
+             a11y={bannerA11y}
+             banner={banner}
+             className={className}
+         >
+             <SystemBannerIcon
+                 indeterminate={banner.indeterminate}
+                 state={banner.state}
+                 aria-hidden="true"
+             />
+             <div className="flex min-w-0 flex-1 flex-col gap-0.5">
+                 <AlertTitle>{banner.title ?? defaultCopy.title}</AlertTitle>
+                 <AlertDescription>
+                     {banner.message ?? defaultCopy.message}
+                 </AlertDescription>
+                 {hasProgress ? (
+                     <SystemBannerProgress
+                         indeterminate={banner.indeterminate}
+                         value={progress ?? 0}
+                     />
+                 ) : null}
+             </div>
+             <div className="flex flex-none gap-1.5">
+                 {primaryLabel ? (
+                     <SystemBannerButton
+                         aria-busy={
+                             banner.indeterminate &&
+                             banner.state === "import-progress"
+                                 ? true
+                                 : undefined
+                         }
+                         disabled={
+                             banner.indeterminate &&
+                             banner.state === "import-progress"
+                         }
+                         onClick={() => handleAction(primaryRole)}
+                         tone={primaryActionTone}
+                         type="button"
+                     >
+                         {primaryLabel}
+                     </SystemBannerButton>
+                 ) : null}
+                 {secondaryLabel ? (
+                     <SystemBannerButton
+                         onClick={() => handleAction("secondary")}
+                         type="button"
+                     >
+                         {secondaryLabel}
+                     </SystemBannerButton>
+                 ) : null}
+                 {dismissLabel ? (
+                     <SystemBannerButton
+                         aria-label={dismissLabel}
+                         onClick={() => onDismiss(banner)}
+                         tone="dismiss"
+                         type="button"
+                     >
+                         <X data-icon="inline-start" aria-hidden="true" />
+                     </SystemBannerButton>
+                 ) : null}
+             </div>
+         </SystemBannerAlert>
+     );
+ }
+
+ export function SystemBanner({ className }: SystemBannerProps) {
+     const { language } = useLanguage();
+     const isZh = language === "zh-CN";
+     const [eventDetails, setEventDetails] = useState<SystemBannerEventDetail[]>(
+         [],
+     );
+     const [online, setOnline] = useState(() =>
+         hasBrowserWindow() ? navigator.onLine : true,
+     );
+     const [offlineDismissed, setOfflineDismissed] = useState(false);
+
+     useEffect(() => {
+         if (!hasBrowserWindow()) {
+             return;
+         }
+
+         const handleOnline = () => {
+             setOnline(true);
+             setOfflineDismissed(false);
+         };
+         const handleOffline = () => {
+             setOnline(false);
+             setOfflineDismissed(false);
+         };
+         const handleSystemBanner = (event: Event) => {
+             const detail = (event as CustomEvent<SystemBannerEventDetail>)
+                 .detail;
+             setEventDetails((current) => {
+                 if (!detail?.state) {
+                     if (detail?.id) {
+                         return current.filter((item) => item.id !== detail.id);
+                     }
+                     return [];
+                 }
+
+                 const key = detail.id ?? detail.state;
+                 const next = current.filter(
+                     (item) => (item.id ?? item.state) !== key,
+                 );
+                 next.push({ ...detail, id: key });
+                 return next;
+             });
+         };
+
+         window.addEventListener("online", handleOnline);
+         window.addEventListener("offline", handleOffline);
+         window.addEventListener(
+             "betterainote:system-banner",
+             handleSystemBanner,
+         );
+
+         return () => {
+             window.removeEventListener("online", handleOnline);
+             window.removeEventListener("offline", handleOffline);
+             window.removeEventListener(
+                 "betterainote:system-banner",
+                 handleSystemBanner,
+             );
+         };
+     }, []);
+
+     const visibleBanners = [
+         ...(online
+             ? []
+             : [
+                   ...(offlineDismissed
+                       ? []
+                       : [
+                             {
+                                 id: "offline",
+                                 state: "offline" as const,
+                             },
+                         ]),
+               ]),
+         ...eventDetails.filter((detail): detail is VisibleSystemBanner =>
+             Boolean(detail.state),
+         ),
+     ]
+         .filter(
+             (banner, index, banners) =>
+                 banners.findIndex(
+                     (item) => (item.id ?? item.state) === banner.id,
+                 ) === index,
+         )
+         .sort((a, b) => getPriority(a.state) - getPriority(b.state))
+         .slice(0, 2);
+
+     if (visibleBanners.length === 0) {
+         return null;
+     }
+
+     const dismissBanner = (banner: VisibleSystemBanner) => {
+         if (banner.state === "offline") {
+             setOfflineDismissed(true);
+             return;
+         }
+         setEventDetails((current) =>
+             current.filter(
+                 (item) =>
+                     (item.id ?? item.state) !== (banner.id ?? banner.state),
+             ),
+         );
+     };
+
+     return (
+         <>
+             {visibleBanners.map((banner) => (
+                 <SystemBannerItem
+                     banner={banner}
+                     className={className}
+                     isStacked={visibleBanners.length > 1}
+                     isZh={isZh}
+                     key={banner.id ?? banner.state}
+                     onDismiss={dismissBanner}
+                 />
+             ))}
+         </>
+     );
+ }
+

 ❯ src/tests/full-ui-replacement-regression.test.ts:5840:28
src/tests/full-ui-replacement-regression.test.ts > full UI replacement regression coverage > composes system banners with shadcn Alert, Button, and Progress primitives
Stack Traces | 0.00694s run time
AssertionError: expected '"use client";\n\nimport {\n    Downlo…' to contain 'systemBannerAlertClassNames.body'

- Expected
+ Received

- systemBannerAlertClassNames.body
+ "use client";
+
+ import {
+     Download,
+     LockKeyhole,
+     Package,
+     Search,
+     ShieldX,
+     Upload,
+     WifiOff,
+     X,
+ } from "lucide-react";
+ import {
+     type ComponentProps,
+     type ReactNode,
+     useEffect,
+     useState,
+ } from "react";
+ import { useLanguage } from "@/components/language-provider";
+ import { Alert, AlertDescription, AlertTitle } from "@.../components/ui/alert";
+ import { Button, type ButtonProps } from "@.../components/ui/button";
+ import { Progress } from "@.../components/ui/progress";
+ import { hasBrowserWindow } from "@.../lib/platform/runtime";
+
+ type SystemBannerState =
+     | "offline"
+     | "permission-denied"
+     | "db-locked"
+     | "runtime-unavailable"
+     | "update-available"
+     | "import-progress"
+     | "export-progress";
+
+ interface SystemBannerEventDetail {
+     actionLabel?: string;
+     dismissLabel?: string;
+     id?: string;
+     indeterminate?: boolean;
+     message?: string;
+     progress?: number;
+     secondaryActionLabel?: string;
+     state: SystemBannerState | null;
+     title?: string;
+ }
+
+ type VisibleSystemBanner = SystemBannerEventDetail & {
+     state: SystemBannerState;
+ };
+
+ interface SystemBannerProps {
+     className?: string;
+ }
+
+ interface SystemBannerDefaultActions {
+     actionLabel?: string;
+     dismissLabel?: string;
+     secondaryActionLabel?: string;
+ }
+
+ type SystemBannerActionRole = "primary" | "secondary";
+ type SystemBannerButtonTone = "action" | "primary" | "dismiss";
+
+ interface SystemBannerItemProps {
+     banner: VisibleSystemBanner;
+     className?: string;
+     isStacked: boolean;
+     isZh: boolean;
+     onDismiss: (banner: VisibleSystemBanner) => void;
+ }
+
+ interface SystemBannerAlertProps {
+     a11y: ReturnType<typeof getBannerA11y>;
+     banner: VisibleSystemBanner;
+     children: ReactNode;
+     className?: string;
+ }
+
+ type SystemBannerButtonProps = Omit<ButtonProps, "size" | "variant"> & {
+     tone?: SystemBannerButtonTone;
+ };
+
+ type SystemBannerAlertVariant = NonNullable<
+     Parameters<typeof Alert>[0]["variant"]
+ >;
+ type SystemBannerIconProps = Omit<ComponentProps<typeof WifiOff>, "children">;
+
+ interface SystemBannerProgressProps {
+     indeterminate: boolean | undefined;
+     value: number;
+ }
+
+ const systemBannerAlertVariantByState: Record<
+     SystemBannerState,
+     SystemBannerAlertVariant
+ > = {
+     "db-locked": "destructiveSoftNeutral",
+     "runtime-unavailable": "destructiveSoftNeutral",
+     "export-progress": "default",
+     "import-progress": "default",
+     offline: "default",
+     "permission-denied": "destructiveSoftNeutral",
+     "update-available": "default",
+ } as const;
+
+ function getDefaultCopy(state: SystemBannerState, isZh: boolean) {
+     switch (state) {
+         case "offline":
+             return {
+                 title: isZh ? "当前无网络连接" : "Offline",
+                 message: isZh
+                     ? "所有已下载的录音与逐字稿可继续阅览 · 来源同步与新转写已暂停。"
+                     : "Downloaded recordings and transcripts remain readable. Source sync and new transcription are paused.",
+             };
+         case "permission-denied":
+             return {
+                 title: isZh
+                     ? "未授权访问录音文件夹"
+                     : "Recording folder permission denied",
+                 message: isZh
+                     ? "无法读取来源缓存目录 · 前往「系统设置 · 隐私与安全性 · 完全磁盘访问」打开开关。"
+                     : "The source cache folder cannot be read. Open System Settings > Privacy & Security > Full Disk Access.",
+             };
+         case "db-locked":
+             return {
+                 title: isZh
+                     ? "本地数据库被另一个 BetterAINote 实例占用"
+                     : "Local database is locked by another BetterAINote instance",
+                 message: isZh
+                     ? "同时只允许一个实例写入 · 当前实例已切到只读模式 · 关闭其它窗口后点「重新连接」。"
+                     : "Only one instance can write at a time. This instance is read-only until other windows close.",
+             };
+         case "runtime-unavailable":
+             return {
+                 title: isZh
+                     ? "同步运行时暂时不可用"
+                     : "Sync runtime is temporarily unavailable",
+                 message: isZh
+                     ? "自动同步暂时无法运行。已下载的录音仍可阅览,稍后可重新尝试同步。"
+                     : "Automatic sync is temporarily unavailable. Downloaded recordings remain readable and you can retry sync shortly.",
+             };
+         case "update-available":
+             return {
+                 title: isZh
+                     ? "BetterAINote 有可用更新"
+                     : "BetterAINote update available",
+                 message: isZh
+                     ? "重启后将应用最新版本。"
+                     : "Restart to apply the latest version.",
+             };
+         case "import-progress":
+             return {
+                 title: isZh
+                     ? "正在导入 BetterAINote 备份包"
+                     : "Importing BetterAINote backup",
+                 message: isZh
+                     ? "录音和来源内容正在写入本地。"
+                     : "Recordings and source content are being saved locally.",
+             };
+         case "export-progress":
+             return {
+                 title: isZh ? "正在导出录音" : "Exporting recordings",
+                 message: isZh
+                     ? "导出文件准备中,请保持当前页面打开。"
+                     : "Export files are being prepared. Keep this page open.",
+             };
+     }
+ }
+
+ function getStackedCopy(state: SystemBannerState, isZh: boolean) {
+     switch (state) {
+         case "offline":
+             return {
+                 title: isZh ? "当前无网络连接" : "Offline",
+                 message: isZh
+                     ? "来源同步已暂停 · 已下载的录音仍可阅览。"
+                     : "Source sync is paused. Downloaded recordings remain readable.",
+             };
+         case "update-available":
+             return {
+                 title: isZh ? "有可用更新" : "Update available",
+                 message: isZh
+                     ? "重启后将应用。"
+                     : "It will be applied after restart.",
+             };
+         default:
+             return getDefaultCopy(state, isZh);
+     }
+ }
+
+ function getPriority(state: SystemBannerState) {
+     switch (state) {
+         case "permission-denied":
+         case "db-locked":
+         case "runtime-unavailable":
+             return 0;
+         case "offline":
+             return 1;
+         case "import-progress":
+         case "export-progress":
+             return 2;
+         case "update-available":
+             return 3;
+     }
+ }
+
+ function getBannerA11y(state: SystemBannerState): {
+     "aria-live"?: "polite";
+     role?: "alert" | "status";
+ } {
+     if (state === "offline") {
+         return { "aria-live": "polite" as const, role: "status" as const };
+     }
+     if (
+         state === "permission-denied" ||
+         state === "db-locked" ||
+         state === "runtime-unavailable"
+     ) {
+         return { role: "alert" as const };
+     }
+     return {};
+ }
+
+ function normalizeProgress(progress: number | undefined) {
+     if (typeof progress !== "number" || Number.isNaN(progress)) return null;
+     const clamped = Math.min(100, Math.max(0, progress));
+     return Math.round(clamped / 10) * 10;
+ }
+
+ function getDefaultActions(
+     state: SystemBannerState,
+     isZh: boolean,
+ ): SystemBannerDefaultActions {
+     switch (state) {
+         case "offline":
+             return {
+                 actionLabel: isZh ? "重试" : "Retry",
+                 dismissLabel: isZh ? "收起" : "Dismiss",
+             };
+         case "permission-denied":
+             return {
+                 actionLabel: isZh ? "打开系统设置" : "Open System Settings",
+                 secondaryActionLabel: isZh ? "稍后" : "Later",
+             };
+         case "db-locked":
+             return {
+                 actionLabel: isZh ? "重新连接" : "Reconnect",
+                 secondaryActionLabel: isZh ? "只读继续" : "Continue read-only",
+             };
+         case "runtime-unavailable":
+             return {
+                 actionLabel: isZh ? "重试同步" : "Retry sync",
+                 dismissLabel: isZh ? "收起" : "Dismiss",
+             };
+         case "update-available":
+             return {
+                 actionLabel: isZh ? "重启并更新" : "Restart and update",
+                 secondaryActionLabel: isZh ? "查看更新内容" : "View changes",
+                 dismissLabel: isZh ? "稍后再说" : "Later",
+             };
+         case "import-progress":
+             return {
+                 actionLabel: isZh ? "暂停" : "Pause",
+                 secondaryActionLabel: isZh ? "取消" : "Cancel",
+             };
+         case "export-progress":
+             return {
+                 actionLabel: isZh ? "在 Finder 中显示" : "Show in Finder",
+                 secondaryActionLabel: isZh ? "取消" : "Cancel",
+             };
+     }
+ }
+
+ function SystemBannerIcon({
+     indeterminate,
+     state,
+     ...props
+ }: {
+     indeterminate: boolean | undefined;
+     state: SystemBannerState;
+ } & SystemBannerIconProps) {
+     if (state === "import-progress" && indeterminate) {
+         return <Search {...props} />;
+     }
+
+     switch (state) {
+         case "offline":
+             return <WifiOff {...props} />;
+         case "permission-denied":
+             return <ShieldX {...props} />;
+         case "db-locked":
+         case "runtime-unavailable":
+             return <LockKeyhole {...props} />;
+         case "update-available":
+             return <Package {...props} />;
+         case "import-progress":
+             return <Upload {...props} />;
+         case "export-progress":
+             return <Download {...props} />;
+     }
+ }
+
+ function SystemBannerAlert({
+     a11y,
+     banner,
+     children,
+     className,
+ }: SystemBannerAlertProps) {
+     return (
+         <Alert
+             aria-live={a11y["aria-live"]}
+             data-control="system-banner"
+             data-state={banner.state}
+             density="comfortable"
+             layout="inline"
+             role={a11y.role}
+             variant={systemBannerAlertVariantByState[banner.state]}
+             className={className}
+         >
+             {children}
+         </Alert>
+     );
+ }
+
+ function SystemBannerButton({
+     className,
+     tone = "action",
+     ...props
+ }: SystemBannerButtonProps) {
+     return (
+         <Button
+             size="sm"
+             variant={tone === "primary" ? "outline" : "ghost"}
+             className={className}
+             {...props}
+         />
+     );
+ }
+
+ function SystemBannerProgress({
+     indeterminate,
+     value,
+ }: SystemBannerProgressProps) {
+     return (
+         <Progress
+             aria-hidden="true"
+             className="min-w-[120px] flex-1"
+             indicatorClassName={
+                 indeterminate
+                     ? "w-[32%] animate-[sbn-sweep_1.4s_linear_infinite]"
+                     : undefined
+             }
+             value={value}
+         />
+     );
+ }
+
+ function getRenderedActions(
+     banner: VisibleSystemBanner,
+     defaultActions: SystemBannerDefaultActions,
+     isStacked: boolean,
+     isZh: boolean,
+ ) {
+     if (banner.indeterminate && banner.state === "import-progress") {
+         return {
+             primaryLabel:
+                 banner.actionLabel ??
+                 banner.secondaryActionLabel ??
+                 defaultActions.secondaryActionLabel,
+             primaryRole: "primary" as const,
+             secondaryLabel: undefined,
+             dismissLabel: undefined,
+         };
+     }
+
+     if (isStacked) {
+         return {
+             primaryLabel:
+                 banner.state === "update-available"
+                     ? (banner.secondaryActionLabel ??
+                       banner.actionLabel ??
+                       (isZh ? "查看" : "View"))
+                     : (banner.actionLabel ?? defaultActions.actionLabel),
+             primaryRole:
+                 banner.state === "update-available" && !banner.actionLabel
+                     ? ("secondary" as const)
+                     : ("primary" as const),
+             secondaryLabel: undefined,
+             dismissLabel:
+                 banner.state === "runtime-unavailable"
+                     ? (banner.dismissLabel ?? defaultActions.dismissLabel)
+                     : undefined,
+         };
+     }
+
+     return {
+         primaryLabel: banner.actionLabel ?? defaultActions.actionLabel,
+         primaryRole: "primary" as const,
+         secondaryLabel:
+             banner.secondaryActionLabel ?? defaultActions.secondaryActionLabel,
+         dismissLabel: banner.dismissLabel ?? defaultActions.dismissLabel,
+     };
+ }
+
+ function getSystemBannerActionName(
+     state: SystemBannerState,
+     role: SystemBannerActionRole,
+ ) {
+     if (role === "secondary") {
+         switch (state) {
+             case "permission-denied":
+                 return "later";
+             case "db-locked":
+                 return "continue-read-only";
+             case "runtime-unavailable":
+                 return "dismiss";
+             case "update-available":
+                 return "view-update-changes";
+             case "import-progress":
+                 return "cancel-import";
+             case "export-progress":
+                 return "cancel-export";
+             case "offline":
+                 return "dismiss";
+         }
+     }
+
+     switch (state) {
+         case "offline":
+             return "retry";
+         case "permission-denied":
+             return "open-system-settings";
+         case "db-locked":
+             return "reconnect";
+         case "runtime-unavailable":
+             return "retry-sync";
+         case "update-available":
+             return "restart-and-update";
+         case "import-progress":
+             return "pause-import";
+         case "export-progress":
+             return "show-export";
+     }
+ }
+
+ function dispatchSystemBannerAction(
+     banner: VisibleSystemBanner,
+     role: SystemBannerActionRole,
+ ) {
+     if (!hasBrowserWindow()) {
+         return;
+     }
+
+     window.dispatchEvent(
+         new CustomEvent("betterainote:system-banner-action", {
+             detail: {
+                 action: getSystemBannerActionName(banner.state, role),
+                 id: banner.id ?? banner.state,
+                 role,
+                 state: banner.state,
+             },
+         }),
+     );
+ }
+
+ function SystemBannerItem({
+     banner,
+     className,
+     isStacked,
+     isZh,
+     onDismiss,
+ }: SystemBannerItemProps) {
+     const defaultCopy = isStacked
+         ? getStackedCopy(banner.state, isZh)
+         : getDefaultCopy(banner.state, isZh);
+     const defaultActions = getDefaultActions(banner.state, isZh);
+     const progress = normalizeProgress(banner.progress);
+     const hasProgress =
+         banner.state === "import-progress" ||
+         banner.state === "export-progress";
+     const { dismissLabel, primaryLabel, primaryRole, secondaryLabel } =
+         getRenderedActions(banner, defaultActions, isStacked, isZh);
+     const bannerA11y = getBannerA11y(banner.state);
+     const primaryActionTone =
+         banner.state === "update-available" && !isStacked
+             ? "primary"
+             : "action";
+     const handleAction = (role: SystemBannerActionRole) => {
+         dispatchSystemBannerAction(banner, role);
+
+         if (role === "primary" && banner.state === "update-available") {
+             if (hasBrowserWindow()) {
+                 window.location.reload();
+             }
+             return;
+         }
+
+         if (role === "secondary") {
+             onDismiss(banner);
+         }
+     };
+
+     return (
+         <SystemBannerAlert
+             a11y={bannerA11y}
+             banner={banner}
+             className={className}
+         >
+             <SystemBannerIcon
+                 indeterminate={banner.indeterminate}
+                 state={banner.state}
+                 aria-hidden="true"
+             />
+             <div className="flex min-w-0 flex-1 flex-col gap-0.5">
+                 <AlertTitle>{banner.title ?? defaultCopy.title}</AlertTitle>
+                 <AlertDescription>
+                     {banner.message ?? defaultCopy.message}
+                 </AlertDescription>
+                 {hasProgress ? (
+                     <SystemBannerProgress
+                         indeterminate={banner.indeterminate}
+                         value={progress ?? 0}
+                     />
+                 ) : null}
+             </div>
+             <div className="flex flex-none gap-1.5">
+                 {primaryLabel ? (
+                     <SystemBannerButton
+                         aria-busy={
+                             banner.indeterminate &&
+                             banner.state === "import-progress"
+                                 ? true
+                                 : undefined
+                         }
+                         disabled={
+                             banner.indeterminate &&
+                             banner.state === "import-progress"
+                         }
+                         onClick={() => handleAction(primaryRole)}
+                         tone={primaryActionTone}
+                         type="button"
+                     >
+                         {primaryLabel}
+                     </SystemBannerButton>
+                 ) : null}
+                 {secondaryLabel ? (
+                     <SystemBannerButton
+                         onClick={() => handleAction("secondary")}
+                         type="button"
+                     >
+                         {secondaryLabel}
+                     </SystemBannerButton>
+                 ) : null}
+                 {dismissLabel ? (
+                     <SystemBannerButton
+                         aria-label={dismissLabel}
+                         onClick={() => onDismiss(banner)}
+                         tone="dismiss"
+                         type="button"
+                     >
+                         <X data-icon="inline-start" aria-hidden="true" />
+                     </SystemBannerButton>
+                 ) : null}
+             </div>
+         </SystemBannerAlert>
+     );
+ }
+
+ export function SystemBanner({ className }: SystemBannerProps) {
+     const { language } = useLanguage();
+     const isZh = language === "zh-CN";
+     const [eventDetails, setEventDetails] = useState<SystemBannerEventDetail[]>(
+         [],
+     );
+     const [online, setOnline] = useState(() =>
+         hasBrowserWindow() ? navigator.onLine : true,
+     );
+     const [offlineDismissed, setOfflineDismissed] = useState(false);
+
+     useEffect(() => {
+         if (!hasBrowserWindow()) {
+             return;
+         }
+
+         const handleOnline = () => {
+             setOnline(true);
+             setOfflineDismissed(false);
+         };
+         const handleOffline = () => {
+             setOnline(false);
+             setOfflineDismissed(false);
+         };
+         const handleSystemBanner = (event: Event) => {
+             const detail = (event as CustomEvent<SystemBannerEventDetail>)
+                 .detail;
+             setEventDetails((current) => {
+                 if (!detail?.state) {
+                     if (detail?.id) {
+                         return current.filter((item) => item.id !== detail.id);
+                     }
+                     return [];
+                 }
+
+                 const key = detail.id ?? detail.state;
+                 const next = current.filter(
+                     (item) => (item.id ?? item.state) !== key,
+                 );
+                 next.push({ ...detail, id: key });
+                 return next;
+             });
+         };
+
+         window.addEventListener("online", handleOnline);
+         window.addEventListener("offline", handleOffline);
+         window.addEventListener(
+             "betterainote:system-banner",
+             handleSystemBanner,
+         );
+
+         return () => {
+             window.removeEventListener("online", handleOnline);
+             window.removeEventListener("offline", handleOffline);
+             window.removeEventListener(
+                 "betterainote:system-banner",
+                 handleSystemBanner,
+             );
+         };
+     }, []);
+
+     const visibleBanners = [
+         ...(online
+             ? []
+             : [
+                   ...(offlineDismissed
+                       ? []
+                       : [
+                             {
+                                 id: "offline",
+                                 state: "offline" as const,
+                             },
+                         ]),
+               ]),
+         ...eventDetails.filter((detail): detail is VisibleSystemBanner =>
+             Boolean(detail.state),
+         ),
+     ]
+         .filter(
+             (banner, index, banners) =>
+                 banners.findIndex(
+                     (item) => (item.id ?? item.state) === banner.id,
+                 ) === index,
+         )
+         .sort((a, b) => getPriority(a.state) - getPriority(b.state))
+         .slice(0, 2);
+
+     if (visibleBanners.length === 0) {
+         return null;
+     }
+
+     const dismissBanner = (banner: VisibleSystemBanner) => {
+         if (banner.state === "offline") {
+             setOfflineDismissed(true);
+             return;
+         }
+         setEventDetails((current) =>
+             current.filter(
+                 (item) =>
+                     (item.id ?? item.state) !== (banner.id ?? banner.state),
+             ),
+         );
+     };
+
+     return (
+         <>
+             {visibleBanners.map((banner) => (
+                 <SystemBannerItem
+                     banner={banner}
+                     className={className}
+                     isStacked={visibleBanners.length > 1}
+                     isZh={isZh}
+                     key={banner.id ?? banner.state}
+                     onDismiss={dismissBanner}
+                 />
+             ))}
+         </>
+     );
+ }
+

 ❯ src/tests/full-ui-replacement-regression.test.ts:5912:24
src/tests/recording-detail-copy-ui-regression.test.ts > recording detail copy and title action UI regressions > keeps recording tag creation controls on shadcn buttons
Stack Traces | 0.0155s run time
AssertionError: expected '"use client";\n\nimport { LoaderCircl…' to contain '<InputGroupButton'

- Expected
+ Received

- <InputGroupButton
+ "use client";
+
+ import { LoaderCircle, Pencil, Plus, Trash2, X } from "lucide-react";
+ import { useMemo, useState } from "react";
+ import { Alert, AlertDescription, AlertTitle } from "@.../components/ui/alert";
+ import { Button } from "@.../components/ui/button";
+ import {
+     Card,
+     CardContent,
+     CardFooter,
+     CardHeader,
+     CardTitle,
+ } from "@.../components/ui/card";
+ import {
+     Dialog,
+     DialogContent,
+     DialogDescription,
+     DialogFooter,
+     DialogHeader,
+     DialogTitle,
+     DialogTrigger,
+ } from "@.../components/ui/dialog";
+ import { Input } from "@.../components/ui/input";
+ import { Label } from "@.../components/ui/label";
+ import { Separator } from "@.../components/ui/separator";
+ import { ToggleGroup, ToggleGroupItem } from "@.../components/ui/toggle-group";
+ import {
+     MAX_RECORDING_TAG_NAME_LENGTH,
+     RECORDING_TAG_COLORS,
+     RECORDING_TAG_ICONS,
+     type RecordingTag,
+     type RecordingTagColor,
+     type RecordingTagIcon,
+ } from "@/lib/recording-tags";
+ import type { Recording } from "@/types/recording";
+ import {
+     RecordingTagIconGlyph,
+     recordingTagColorLabel,
+ } from "./recording-tag-visuals";
+
+ interface RecordingTagManagerProps {
+     recording: Recording;
+     availableTags: RecordingTag[];
+     onAvailableTagsChange: (tags: RecordingTag[]) => void;
+     onRecordingTagsChange: (recordingId: string, tags: RecordingTag[]) => void;
+     loadError?: string | null;
+     onClose?: () => void;
+ }
+
+ type TagPayload = Pick<RecordingTag, "color" | "icon" | "name">;
+ type AssignmentPayload = { tagIds: string[]; tags: RecordingTag[] };
+ type RetryAction =
+     | { payload: AssignmentPayload; type: "assignment" }
+     | { payload: TagPayload; type: "create" }
+     | { payload: TagPayload; tag: RecordingTag; type: "update" }
+     | { tag: RecordingTag; type: "delete" };
+
+ async function readJsonResponse(response: Response) {
+     const data = await response.json().catch(() => ({}));
+     if (!response.ok) {
+         throw new Error(
+             typeof data?.error === "string" ? data.error : "Request failed",
+         );
+     }
+     return data;
+ }
+
+ function toErrorMessage(error: unknown, fallback: string) {
+     return error instanceof Error && error.message ? error.message : fallback;
+ }
+
+ export function RecordingTagManager({
+     recording,
+     availableTags,
+     loadError,
+     onAvailableTagsChange,
+     onRecordingTagsChange,
+     onClose,
+ }: RecordingTagManagerProps) {
+     const [createName, setCreateName] = useState("");
+     const [createColor, setCreateColor] = useState<RecordingTagColor>("purple");
+     const [createIcon, setCreateIcon] = useState<RecordingTagIcon>("tag");
+     const [editingTagId, setEditingTagId] = useState<string | null>(null);
+     const [editName, setEditName] = useState("");
+     const [editColor, setEditColor] = useState<RecordingTagColor>("purple");
+     const [editIcon, setEditIcon] = useState<RecordingTagIcon>("tag");
+     const [pending, setPending] = useState<string | null>(null);
+     const [operationError, setOperationError] = useState<string | null>(null);
+     const [retryAction, setRetryAction] = useState<RetryAction | null>(null);
+     const [createDialogOpen, setCreateDialogOpen] = useState(false);
+     const [deleteTarget, setDeleteTarget] = useState<RecordingTag | null>(null);
+
+     const selectedTagIds = useMemo(
+         () => new Set(recording.tags.map((tag) => tag.id)),
+         [recording.tags],
+     );
+     const busy = pending !== null;
+     const visibleError = operationError ?? loadError ?? null;
+
+     const updateCatalog = (tag: RecordingTag) => {
+         onAvailableTagsChange([
+             tag,
+             ...availableTags.filter((item) => item.id !== tag.id),
+         ]);
+     };
+
+     const updateAssignments = async (payload: AssignmentPayload) => {
+         const response = await fetch(`/api/recordings/${recording.id}/tags`, {
+             method: "PUT",
+             headers: { "Content-Type": "application/json" },
+             body: JSON.stringify({ tagIds: payload.tagIds }),
+         });
+         const data = await readJsonResponse(response);
+         const tags = Array.isArray(data.tags) ? data.tags : payload.tags;
+         onRecordingTagsChange(recording.id, tags);
+     };
+
+     const runAssignment = async (payload: AssignmentPayload) => {
+         if (busy) return;
+         setPending("assignment");
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             await updateAssignments(payload);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签保存失败"));
+             setRetryAction({ payload, type: "assignment" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const toggleTag = (tag: RecordingTag) => {
+         const nextTags = selectedTagIds.has(tag.id)
+             ? recording.tags.filter((item) => item.id !== tag.id)
+             : [...recording.tags, tag];
+         void runAssignment({
+             tagIds: nextTags.map((item) => item.id),
+             tags: nextTags,
+         });
+     };
+
+     const createTag = async (payload: TagPayload) => {
+         if (busy || !payload.name.trim()) return;
+         setPending("create");
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch("/api/recording-tags", {
+                 method: "POST",
+                 headers: { "Content-Type": "application/json" },
+                 body: JSON.stringify(payload),
+             });
+             const data = await readJsonResponse(response);
+             const tag = data.tag as RecordingTag;
+             updateCatalog(tag);
+             const nextTags = [...recording.tags, tag];
+             await updateAssignments({
+                 tagIds: nextTags.map((item) => item.id),
+                 tags: nextTags,
+             });
+             setCreateName("");
+             setCreateDialogOpen(false);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签创建失败"));
+             setRetryAction({ payload, type: "create" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const updateTag = async (tag: RecordingTag, payload: TagPayload) => {
+         if (busy || !payload.name.trim()) return;
+         setPending(`update-${tag.id}`);
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch(`/api/recording-tags/${tag.id}`, {
+                 method: "PATCH",
+                 headers: { "Content-Type": "application/json" },
+                 body: JSON.stringify(payload),
+             });
+             const data = await readJsonResponse(response);
+             const updatedTag = data.tag as RecordingTag;
+             updateCatalog(updatedTag);
+             onRecordingTagsChange(
+                 recording.id,
+                 recording.tags.map((item) =>
+                     item.id === updatedTag.id ? updatedTag : item,
+                 ),
+             );
+             setEditingTagId(null);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签更新失败"));
+             setRetryAction({ payload, tag, type: "update" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const deleteTag = async (tag: RecordingTag) => {
+         if (busy) return;
+         setPending(`delete-${tag.id}`);
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch(`/api/recording-tags/${tag.id}`, {
+                 method: "DELETE",
+             });
+             await readJsonResponse(response);
+             onAvailableTagsChange(
+                 availableTags.filter((item) => item.id !== tag.id),
+             );
+             onRecordingTagsChange(
+                 recording.id,
+                 recording.tags.filter((item) => item.id !== tag.id),
+             );
+             setEditingTagId(null);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签删除失败"));
+             setRetryAction({ tag, type: "delete" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const confirmDelete = () => {
+         if (!deleteTarget || busy) return;
+         const tag = deleteTarget;
+         setDeleteTarget(null);
+         void deleteTag(tag);
+     };
+
+     const retry = () => {
+         if (!retryAction) return;
+         if (retryAction.type === "assignment") {
+             void runAssignment(retryAction.payload);
+         } else if (retryAction.type === "create") {
+             void createTag(retryAction.payload);
+         } else if (retryAction.type === "update") {
+             void updateTag(retryAction.tag, retryAction.payload);
+         } else {
+             void deleteTag(retryAction.tag);
+         }
+     };
+
+     const startEdit = (tag: RecordingTag) => {
+         setEditingTagId(tag.id);
+         setEditName(tag.name);
+         setEditColor(tag.color);
+         setEditIcon(tag.icon);
+         setOperationError(null);
+         setRetryAction(null);
+     };
+
+     const closeCreateDialog = () => {
+         if (busy) return;
+         setCreateDialogOpen(false);
+         setCreateName("");
+         setOperationError(null);
+         setRetryAction(null);
+     };
+
+     const renderError = () =>
+         visibleError ? (
+             <Alert role="alert" variant="destructive">
+                 <AlertTitle>标签操作失败</AlertTitle>
+                 <AlertDescription className="flex flex-wrap items-center gap-3">
+                     <span>{visibleError}</span>
+                     {retryAction ? (
+                         <Button
+                             disabled={busy}
+                             onClick={retry}
+                             size="sm"
+                             type="button"
+                             variant="outline"
+                         >
+                             重试
+                         </Button>
+                     ) : null}
+                 </AlertDescription>
+             </Alert>
+         ) : null;
+
+     const renderPicker = ({
+         color,
+         icon,
+         onColorChange,
+         onIconChange,
+         prefix,
+     }: {
+         color: RecordingTagColor;
+         icon: RecordingTagIcon;
+         onColorChange: (value: RecordingTagColor) => void;
+         onIconChange: (value: RecordingTagIcon) => void;
+         prefix: string;
+     }) => (
+         <div className="grid gap-3 sm:grid-cols-2">
+             <div className="grid gap-2">
+                 <Label id={`${prefix}-color-label`}>颜色</Label>
+                 <ToggleGroup
+                     aria-labelledby={`${prefix}-color-label`}
+                     disabled={busy}
+                     onValueChange={(value) => {
+                         if (
+                             RECORDING_TAG_COLORS.includes(
+                                 value as RecordingTagColor,
+                             )
+                         ) {
+                             onColorChange(value as RecordingTagColor);
+                         }
+                     }}
+                     size="sm"
+                     type="single"
+                     value={color}
+                     variant="outline"
+                 >
+                     {RECORDING_TAG_COLORS.map((item) => (
+                         <ToggleGroupItem
+                             aria-label={recordingTagColorLabel[item]}
+                             key={item}
+                             value={item}
+                         >
+                             {recordingTagColorLabel[item]}
+                         </ToggleGroupItem>
+                     ))}
+                 </ToggleGroup>
+             </div>
+             <div className="grid gap-2">
+                 <Label id={`${prefix}-icon-label`}>图标</Label>
+                 <ToggleGroup
+                     aria-labelledby={`${prefix}-icon-label`}
+                     disabled={busy}
+                     layout="iconGrid"
+                     onValueChange={(value) => {
+                         if (
+                             RECORDING_TAG_ICONS.includes(
+                                 value as RecordingTagIcon,
+                             )
+                         ) {
+                             onIconChange(value as RecordingTagIcon);
+                         }
+                     }}
+                     size="sm"
+                     spacing={1}
+                     type="single"
+                     value={icon}
+                     variant="outline"
+                 >
+                     {RECORDING_TAG_ICONS.map((item) => (
+                         <ToggleGroupItem
+                             aria-label={item}
+                             key={item}
+                             value={item}
+                         >
+                             <RecordingTagIconGlyph icon={item} />
+                         </ToggleGroupItem>
+                     ))}
+                 </ToggleGroup>
+             </div>
+         </div>
+     );
+
+     return (
+         <>
+             <Card
+                 aria-busy={busy || undefined}
+                 className="w-full max-w-xl"
+                 data-control="recording-tag-manager"
+                 data-state={visibleError ? "error" : busy ? "saving" : "ready"}
+             >
+                 <CardHeader className="flex flex-row items-center justify-between gap-4">
+                     <CardTitle className="text-base">管理标签</CardTitle>
+                     {onClose ? (
+                         <Button
+                             aria-label="关闭标签管理"
+                             disabled={busy}
+                             onClick={onClose}
+                             size="icon-sm"
+                             type="button"
+                             variant="ghost"
+                         >
+                             <X />
+                         </Button>
+                     ) : null}
+                 </CardHeader>
+                 <CardContent className="grid gap-5">
+                     {!createDialogOpen ? renderError() : null}
+
+                     <section
+                         aria-labelledby="recording-tags-title"
+                         className="grid gap-3"
+                     >
+                         <div className="flex items-center justify-between gap-3">
+                             <Label id="recording-tags-title">
+                                 这条录音的标签
+                             </Label>
+                             <span className="text-sm text-muted-foreground">
+                                 {recording.tags.length} 个
+                             </span>
+                         </div>
+                         <div className="flex flex-wrap gap-2">
+                             {availableTags.length === 0 ? (
+                                 <p className="text-sm text-muted-foreground">
+                                     尚未创建标签
+                                 </p>
+                             ) : (
+                                 availableTags.map((tag) => {
+                                     const selected = selectedTagIds.has(tag.id);
+                                     return (
+                                         <Button
+                                             aria-pressed={selected}
+                                             data-control="recording-tag-toggle"
+                                             data-tag-id={tag.id}
+                                             disabled={busy}
+                                             key={tag.id}
+                                             onClick={() => toggleTag(tag)}
+                                             size="sm"
+                                             type="button"
+                                             variant={
+                                                 selected
+                                                     ? "secondary"
+                                                     : "outline"
+                                             }
+                                         >
+                                             {pending === "assignment" ? (
+                                                 <LoaderCircle className="animate-spin" />
+                                             ) : (
+                                                 <RecordingTagIconGlyph
+                                                     icon={tag.icon}
+                                                 />
+                                             )}
+                                             {tag.name}
+                                         </Button>
+                                     );
+                                 })
+                             )}
+                         </div>
+                     </section>
+
+                     <Separator />
+
+                     <section
+                         aria-labelledby="recording-tag-catalog-title"
+                         className="grid gap-3"
+                     >
+                         <Label id="recording-tag-catalog-title">标签目录</Label>
+                         <div className="grid gap-2">
+                             {availableTags.map((tag) =>
+                                 editingTagId === tag.id ? (
+                                     <div
+                                         className="grid gap-3 rounded-md border p-3"
+                                         key={tag.id}
+                                     >
+                                         <Input
+                                             aria-label="重命名标签"
+                                             disabled={busy}
+                                             maxLength={
+                                                 MAX_RECORDING_TAG_NAME_LENGTH
+                                             }
+                                             onChange={(event) =>
+                                                 setEditName(event.target.value)
+                                             }
+                                             value={editName}
+                                         />
+                                         {renderPicker({
+                                             color: editColor,
+                                             icon: editIcon,
+                                             onColorChange: setEditColor,
+                                             onIconChange: setEditIcon,
+                                             prefix: `edit-${tag.id}`,
+                                         })}
+                                         <div className="flex flex-wrap justify-end gap-2">
+                                             <Button
+                                                 disabled={busy}
+                                                 onClick={() =>
+                                                     setEditingTagId(null)
+                                                 }
+                                                 size="sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 取消
+                                             </Button>
+                                             <Button
+                                                 disabled={
+                                                     !editName.trim() || busy
+                                                 }
+                                                 onClick={() =>
+                                                     void updateTag(tag, {
+                                                         color: editColor,
+                                                         icon: editIcon,
+                                                         name: editName.trim(),
+                                                     })
+                                                 }
+                                                 size="sm"
+                                                 type="button"
+                                             >
+                                                 {pending ===
+                                                 `update-${tag.id}` ? (
+                                                     <LoaderCircle className="animate-spin" />
+                                                 ) : null}
+                                                 保存
+                                             </Button>
+                                         </div>
+                                     </div>
+                                 ) : (
+                                     <div
+                                         className="flex items-center justify-between gap-3 rounded-md border p-3"
+                                         key={tag.id}
+                                     >
+                                         <div className="flex min-w-0 items-center gap-2">
+                                             <RecordingTagIconGlyph
+                                                 icon={tag.icon}
+                                             />
+                                             <span className="truncate font-medium">
+                                                 {tag.name}
+                                             </span>
+                                             <span className="text-sm text-muted-foreground">
+                                                 {tag.recordingCount ?? 0} 条录音
+                                             </span>
+                                         </div>
+                                         <div className="flex shrink-0 gap-1">
+                                             <Button
+                                                 aria-label={`编辑 ${tag.name}`}
+                                                 disabled={busy}
+                                                 onClick={() => startEdit(tag)}
+                                                 size="icon-sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 <Pencil />
+                                             </Button>
+                                             <Button
+                                                 aria-label={`删除 ${tag.name}`}
+                                                 disabled={busy}
+                                                 onClick={() =>
+                                                     setDeleteTarget(tag)
+                                                 }
+                                                 size="icon-sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 <Trash2 />
+                                             </Button>
+                                         </div>
+                                     </div>
+                                 ),
+                             )}
+                         </div>
+                     </section>
+                 </CardContent>
+                 <CardFooter className="border-t pt-5">
+                     <Dialog
+                         onOpenChange={(open) => {
+                             if (open) {
+                                 setDeleteTarget(null);
+                                 setOperationError(null);
+                                 setRetryAction(null);
+                                 setCreateDialogOpen(true);
+                                 return;
+                             }
+                             closeCreateDialog();
+                         }}
+                         open={createDialogOpen}
+                     >
+                         <DialogTrigger asChild>
+                             <Button
+                                 data-control="recording-tag-create"
+                                 type="button"
+                             >
+                                 <Plus />
+                                 新建标签
+                             </Button>
+                         </DialogTrigger>
+                         <DialogContent aria-describedby="recording-tag-create-description">
+                             <DialogHeader>
+                                 <DialogTitle>新建标签</DialogTitle>
+                                 <DialogDescription id="recording-tag-create-description">
+                                     创建后会自动添加到这条录音。
+                                 </DialogDescription>
+                             </DialogHeader>
+                             <form
+                                 className="grid gap-5"
+                                 onSubmit={(event) => {
+                                     event.preventDefault();
+                                     void createTag({
+                                         color: createColor,
+                                         icon: createIcon,
+                                         name: createName.trim(),
+                                     });
+                                 }}
+                             >
+                                 {renderError()}
+                                 <div className="grid gap-2">
+                                     <Label htmlFor="recording-tag-create-name">
+                                         标签名称
+                                     </Label>
+                                     <Input
+                                         autoFocus
+                                         disabled={busy}
+                                         id="recording-tag-create-name"
+                                         maxLength={
+                                             MAX_RECORDING_TAG_NAME_LENGTH
+                                         }
+                                         onChange={(event) =>
+                                             setCreateName(event.target.value)
+                                         }
+                                         placeholder="例如:待跟进"
+                                         value={createName}
+                                     />
+                                 </div>
+                                 {renderPicker({
+                                     color: createColor,
+                                     icon: createIcon,
+                                     onColorChange: setCreateColor,
+                                     onIconChange: setCreateIcon,
+                                     prefix: "create",
+                                 })}
+                                 <DialogFooter>
+                                     <Button
+                                         disabled={busy}
+                                         onClick={closeCreateDialog}
+                                         type="button"
+                                         variant="outline"
+                                     >
+                                         取消
+                                     </Button>
+                                     <Button
+                                         disabled={!createName.trim() || busy}
+                                         type="submit"
+                                     >
+                                         {pending === "create" ? (
+                                             <LoaderCircle className="animate-spin" />
+                                         ) : (
+                                             <Plus />
+                                         )}
+                                         创建标签
+                                     </Button>
+                                 </DialogFooter>
+                             </form>
+                         </DialogContent>
+                     </Dialog>
+                 </CardFooter>
+             </Card>
+             <Dialog
+                 open={Boolean(deleteTarget)}
+                 onOpenChange={(open) => {
+                     if (!open) {
+                         setDeleteTarget(null);
+                     }
+                 }}
+             >
+                 <DialogContent>
+                     <DialogHeader>
+                         <DialogTitle>删除标签</DialogTitle>
+                         <DialogDescription>
+                             {deleteTarget
+                                 ? `“${deleteTarget.name}”会从所有录音中移除。`
+                                 : ""}
+                         </DialogDescription>
+                     </DialogHeader>
+                     <DialogFooter>
+                         <Button
+                             onClick={() => setDeleteTarget(null)}
+                             type="button"
+                             variant="outline"
+                         >
+                             取消
+                         </Button>
+                         <Button
+                             disabled={busy}
+                             onClick={confirmDelete}
+                             type="button"
+                             variant="destructive"
+                         >
+                             删除标签
+                         </Button>
+                     </DialogFooter>
+                 </DialogContent>
+             </Dialog>
+         </>
+     );
+ }
+

 ❯ src/tests/recording-detail-copy-ui-regression.test.ts:3622:28
src/tests/recording-detail-copy-ui-regression.test.ts > recording detail copy and title action UI regressions > keeps source report copy states explicit without dumping raw detail payloads
Stack Traces | 0.0252s run time
AssertionError: expected -1 to be greater than or equal to 0
 ❯ extractOpeningElement src/tests/recording-detail-copy-ui-regression.test.ts:797:25
 ❯ src/tests/recording-detail-copy-ui-regression.test.ts:1464:38
src/tests/full-ui-replacement-regression.test.ts > full UI replacement regression coverage > keeps auth on the SOT card structure
Stack Traces | 0.0269s run time
AssertionError: expected '"use client";\n\nimport Image from "n…' to contain '<main className={authLoginClassNames.…'

- Expected
+ Received

- <main className={authLoginClassNames.layout}>
+ "use client";
+
+ import Image from "next/image";
+ import { useEffect, useState } from "react";
+ import { toast } from "sonner";
+ import { Alert, AlertDescription } from "@.../components/ui/alert";
+ import { Button } from "@.../components/ui/button";
+ import {
+     Card,
+     CardContent,
+     CardDescription,
+     CardHeader,
+     CardTitle,
+ } from "@.../components/ui/card";
+ import {
+     Field,
+     FieldDescription,
+     FieldGroup,
+     FieldLabel,
+ } from "@.../components/ui/field";
+ import { Input } from "@.../components/ui/input";
+ import { Spinner } from "@.../components/ui/spinner";
+ import { signIn } from "@/lib/auth-client";
+ import {
+     navigateAndRefreshBrowserRoute,
+     useBrowserRouteController,
+ } from "@.../lib/platform/browser-router";
+
+ export function LoginForm({
+     intent = "login",
+     registrationOpen = false,
+ }: {
+     intent?: "login" | "setup";
+     registrationOpen?: boolean;
+ }) {
+     const router = useBrowserRouteController();
+     const [isLoading, setIsLoading] = useState(false);
+     const [isLocalLoading, setIsLocalLoading] = useState(false);
+     const [isMounted, setIsMounted] = useState(false);
+     const [formState, setFormState] = useState<{
+         kind: "error" | "success";
+         message: string;
+     } | null>(null);
+
+     useEffect(() => {
+         setIsMounted(true);
+     }, []);
+
+     async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
+         event.preventDefault();
+         const formData = new FormData(event.currentTarget);
+         const emailValue = String(formData.get("email") ?? "").trim();
+         setIsLoading(true);
+         setFormState(null);
+
+         try {
+             const result = await signIn.magicLink({
+                 email: emailValue,
+                 callbackURL: "/dashboard",
+                 newUserCallbackURL: "/onboarding",
+                 errorCallbackURL: "/login",
+             });
+             if (result.error) {
+                 const message = result.error.message || "登录链接发送失败";
+                 setFormState({ kind: "error", message });
+                 toast.error(message);
+                 return;
+             }
+             const message = "登录链接已发送";
+             setFormState({ kind: "success", message });
+             toast.success(message);
+         } catch (error) {
+             const message =
+                 error instanceof Error ? error.message : "登录链接发送失败";
+             setFormState({ kind: "error", message });
+             toast.error(message);
+         } finally {
+             setIsLoading(false);
+         }
+     }
+
+     async function handleLocalUse() {
+         setIsLocalLoading(true);
+         setFormState(null);
+
+         try {
+             const result = await signIn.anonymous();
+             if (result.error) {
+                 const message = result.error.message || "本地工作空间启动失败";
+                 setFormState({ kind: "error", message });
+                 toast.error(message);
+                 return;
+             }
+             toast.success("已进入本地工作空间");
+             navigateAndRefreshBrowserRoute(router, "/dashboard");
+         } catch (error) {
+             const message =
+                 error instanceof Error ? error.message : "本地工作空间启动失败";
+             setFormState({ kind: "error", message });
+             toast.error(message);
+         } finally {
+             setIsLocalLoading(false);
+         }
+     }
+
+     const invalid = formState?.kind === "error";
+     const title = intent === "setup" ? "设置同步身份" : "登录 BetterAINote";
+     const cardHeading =
+         intent === "setup" ? "上手 / Sign in" : "登录 / Sign in";
+     const subtitle =
+         intent === "setup" && registrationOpen
+             ? "首次使用可发送邮箱链接创建同步身份,也可以只在本地工作空间继续。"
+             : "登录是可选的,仅用于多端同步";
+     return (
+         <main className="grid min-h-svh place-items-center bg-background px-6 py-10 text-foreground">
+             <Card className="w-full max-w-sm">
+                 <form onSubmit={handleSubmit}>
+                     <CardHeader>
+                         <CardTitle>{cardHeading}</CardTitle>
+                         <CardDescription>
+                             邮箱 + 链接 · 不要密码
+                         </CardDescription>
+                     </CardHeader>
+                     <CardContent className="flex flex-col items-center text-center">
+                         <Image
+                             className="mb-4 size-9"
+                             src="/assets/logo-mark-steel.svg"
+                             alt=""
+                             width={36}
+                             height={36}
+                             unoptimized
+                         />{" "}
+                         <CardTitle>{title}</CardTitle>
+                         <CardDescription>{subtitle}</CardDescription>
+                         <FieldGroup className="mx-auto w-full max-w-xs">
+                             <Field>
+                                 <FieldLabel htmlFor="email" className="sr-only">
+                                     邮箱
+                                 </FieldLabel>
+                                 <Input
+                                     id="email"
+                                     name="email"
+                                     type="email"
+                                     defaultValue=""
+                                     required
+                                     disabled={!isMounted || isLoading}
+                                     autoComplete="email"
+                                     aria-invalid={invalid}
+                                     aria-describedby={
+                                         formState
+                                             ? "auth-form-message"
+                                             : undefined
+                                     }
+                                     placeholder="mei@example.com"
+                                 />
+                                 {formState ? (
+                                     <Alert
+                                         id="auth-form-message"
+                                         className="text-left"
+                                         role={
+                                             formState.kind === "success"
+                                                 ? "status"
+                                                 : "alert"
+                                         }
+                                         aria-live={
+                                             formState.kind === "success"
+                                                 ? "polite"
+                                                 : "assertive"
+                                         }
+                                         variant={
+                                             formState.kind === "error"
+                                                 ? "statusError"
+                                                 : "default"
+                                         }
+                                     >
+                                         <AlertDescription>
+                                             {formState.message}
+                                         </AlertDescription>
+                                     </Alert>
+                                 ) : null}
+                             </Field>
+                             <Field className="gap-3">
+                                 <Button
+                                     type="submit"
+                                     disabled={!isMounted || isLoading}
+                                     aria-busy={isLoading}
+                                     variant="default"
+                                     className="w-full"
+                                 >
+                                     {isLoading ? (
+                                         <>
+                                             <Spinner aria-hidden="true" />
+                                             发送中...
+                                         </>
+                                     ) : (
+                                         "发送登录链接"
+                                     )}
+                                 </Button>
+                                 <FieldDescription className="text-center">
+                                     或{" "}
+                                     <Button
+                                         type="button"
+                                         disabled={!isMounted || isLocalLoading}
+                                         aria-busy={isLocalLoading}
+                                         variant="link"
+                                         onClick={() => void handleLocalUse()}
+                                     >
+                                         {isLocalLoading ? (
+                                             <>
+                                                 <Spinner aria-hidden="true" />
+                                                 启动中...
+                                             </>
+                                         ) : (
+                                             "仅本地使用"
+                                         )}
+                                     </Button>
+                                 </FieldDescription>
+                             </Field>
+                         </FieldGroup>
+                     </CardContent>
+                 </form>
+             </Card>
+         </main>
+     );
+ }
+

 ❯ src/tests/full-ui-replacement-regression.test.ts:6425:27
src/tests/dashboard-ui-foundation.test.ts > dashboard SOT foundation > keeps dashboard route loading skeleton on the shadcn primitive contract
Stack Traces | 0.032s run time
AssertionError: expected -1 to be greater than or equal to 0
 ❯ extractBoundedSlice src/tests/dashboard-ui-foundation.test.ts:345:19
 ❯ src/tests/dashboard-ui-foundation.test.ts:1815:52
src/tests/full-ui-replacement-regression.test.ts > full UI replacement regression coverage > keeps semantic global primitives and a Radix settings shell without global SOT visual overrides
Stack Traces | 0.0428s run time
AssertionError: expected '"use client";\n\nimport { LoaderCircl…' to contain 'RECORDING_TAG_SWATCH_ITEM_CLASS_NAME'

- Expected
+ Received

- RECORDING_TAG_SWATCH_ITEM_CLASS_NAME
+ "use client";
+
+ import { LoaderCircle, Pencil, Plus, Trash2, X } from "lucide-react";
+ import { useMemo, useState } from "react";
+ import { Alert, AlertDescription, AlertTitle } from "@.../components/ui/alert";
+ import { Button } from "@.../components/ui/button";
+ import {
+     Card,
+     CardContent,
+     CardFooter,
+     CardHeader,
+     CardTitle,
+ } from "@.../components/ui/card";
+ import {
+     Dialog,
+     DialogContent,
+     DialogDescription,
+     DialogFooter,
+     DialogHeader,
+     DialogTitle,
+     DialogTrigger,
+ } from "@.../components/ui/dialog";
+ import { Input } from "@.../components/ui/input";
+ import { Label } from "@.../components/ui/label";
+ import { Separator } from "@.../components/ui/separator";
+ import { ToggleGroup, ToggleGroupItem } from "@.../components/ui/toggle-group";
+ import {
+     MAX_RECORDING_TAG_NAME_LENGTH,
+     RECORDING_TAG_COLORS,
+     RECORDING_TAG_ICONS,
+     type RecordingTag,
+     type RecordingTagColor,
+     type RecordingTagIcon,
+ } from "@/lib/recording-tags";
+ import type { Recording } from "@/types/recording";
+ import {
+     RecordingTagIconGlyph,
+     recordingTagColorLabel,
+ } from "./recording-tag-visuals";
+
+ interface RecordingTagManagerProps {
+     recording: Recording;
+     availableTags: RecordingTag[];
+     onAvailableTagsChange: (tags: RecordingTag[]) => void;
+     onRecordingTagsChange: (recordingId: string, tags: RecordingTag[]) => void;
+     loadError?: string | null;
+     onClose?: () => void;
+ }
+
+ type TagPayload = Pick<RecordingTag, "color" | "icon" | "name">;
+ type AssignmentPayload = { tagIds: string[]; tags: RecordingTag[] };
+ type RetryAction =
+     | { payload: AssignmentPayload; type: "assignment" }
+     | { payload: TagPayload; type: "create" }
+     | { payload: TagPayload; tag: RecordingTag; type: "update" }
+     | { tag: RecordingTag; type: "delete" };
+
+ async function readJsonResponse(response: Response) {
+     const data = await response.json().catch(() => ({}));
+     if (!response.ok) {
+         throw new Error(
+             typeof data?.error === "string" ? data.error : "Request failed",
+         );
+     }
+     return data;
+ }
+
+ function toErrorMessage(error: unknown, fallback: string) {
+     return error instanceof Error && error.message ? error.message : fallback;
+ }
+
+ export function RecordingTagManager({
+     recording,
+     availableTags,
+     loadError,
+     onAvailableTagsChange,
+     onRecordingTagsChange,
+     onClose,
+ }: RecordingTagManagerProps) {
+     const [createName, setCreateName] = useState("");
+     const [createColor, setCreateColor] = useState<RecordingTagColor>("purple");
+     const [createIcon, setCreateIcon] = useState<RecordingTagIcon>("tag");
+     const [editingTagId, setEditingTagId] = useState<string | null>(null);
+     const [editName, setEditName] = useState("");
+     const [editColor, setEditColor] = useState<RecordingTagColor>("purple");
+     const [editIcon, setEditIcon] = useState<RecordingTagIcon>("tag");
+     const [pending, setPending] = useState<string | null>(null);
+     const [operationError, setOperationError] = useState<string | null>(null);
+     const [retryAction, setRetryAction] = useState<RetryAction | null>(null);
+     const [createDialogOpen, setCreateDialogOpen] = useState(false);
+     const [deleteTarget, setDeleteTarget] = useState<RecordingTag | null>(null);
+
+     const selectedTagIds = useMemo(
+         () => new Set(recording.tags.map((tag) => tag.id)),
+         [recording.tags],
+     );
+     const busy = pending !== null;
+     const visibleError = operationError ?? loadError ?? null;
+
+     const updateCatalog = (tag: RecordingTag) => {
+         onAvailableTagsChange([
+             tag,
+             ...availableTags.filter((item) => item.id !== tag.id),
+         ]);
+     };
+
+     const updateAssignments = async (payload: AssignmentPayload) => {
+         const response = await fetch(`/api/recordings/${recording.id}/tags`, {
+             method: "PUT",
+             headers: { "Content-Type": "application/json" },
+             body: JSON.stringify({ tagIds: payload.tagIds }),
+         });
+         const data = await readJsonResponse(response);
+         const tags = Array.isArray(data.tags) ? data.tags : payload.tags;
+         onRecordingTagsChange(recording.id, tags);
+     };
+
+     const runAssignment = async (payload: AssignmentPayload) => {
+         if (busy) return;
+         setPending("assignment");
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             await updateAssignments(payload);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签保存失败"));
+             setRetryAction({ payload, type: "assignment" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const toggleTag = (tag: RecordingTag) => {
+         const nextTags = selectedTagIds.has(tag.id)
+             ? recording.tags.filter((item) => item.id !== tag.id)
+             : [...recording.tags, tag];
+         void runAssignment({
+             tagIds: nextTags.map((item) => item.id),
+             tags: nextTags,
+         });
+     };
+
+     const createTag = async (payload: TagPayload) => {
+         if (busy || !payload.name.trim()) return;
+         setPending("create");
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch("/api/recording-tags", {
+                 method: "POST",
+                 headers: { "Content-Type": "application/json" },
+                 body: JSON.stringify(payload),
+             });
+             const data = await readJsonResponse(response);
+             const tag = data.tag as RecordingTag;
+             updateCatalog(tag);
+             const nextTags = [...recording.tags, tag];
+             await updateAssignments({
+                 tagIds: nextTags.map((item) => item.id),
+                 tags: nextTags,
+             });
+             setCreateName("");
+             setCreateDialogOpen(false);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签创建失败"));
+             setRetryAction({ payload, type: "create" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const updateTag = async (tag: RecordingTag, payload: TagPayload) => {
+         if (busy || !payload.name.trim()) return;
+         setPending(`update-${tag.id}`);
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch(`/api/recording-tags/${tag.id}`, {
+                 method: "PATCH",
+                 headers: { "Content-Type": "application/json" },
+                 body: JSON.stringify(payload),
+             });
+             const data = await readJsonResponse(response);
+             const updatedTag = data.tag as RecordingTag;
+             updateCatalog(updatedTag);
+             onRecordingTagsChange(
+                 recording.id,
+                 recording.tags.map((item) =>
+                     item.id === updatedTag.id ? updatedTag : item,
+                 ),
+             );
+             setEditingTagId(null);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签更新失败"));
+             setRetryAction({ payload, tag, type: "update" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const deleteTag = async (tag: RecordingTag) => {
+         if (busy) return;
+         setPending(`delete-${tag.id}`);
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch(`/api/recording-tags/${tag.id}`, {
+                 method: "DELETE",
+             });
+             await readJsonResponse(response);
+             onAvailableTagsChange(
+                 availableTags.filter((item) => item.id !== tag.id),
+             );
+             onRecordingTagsChange(
+                 recording.id,
+                 recording.tags.filter((item) => item.id !== tag.id),
+             );
+             setEditingTagId(null);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签删除失败"));
+             setRetryAction({ tag, type: "delete" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const confirmDelete = () => {
+         if (!deleteTarget || busy) return;
+         const tag = deleteTarget;
+         setDeleteTarget(null);
+         void deleteTag(tag);
+     };
+
+     const retry = () => {
+         if (!retryAction) return;
+         if (retryAction.type === "assignment") {
+             void runAssignment(retryAction.payload);
+         } else if (retryAction.type === "create") {
+             void createTag(retryAction.payload);
+         } else if (retryAction.type === "update") {
+             void updateTag(retryAction.tag, retryAction.payload);
+         } else {
+             void deleteTag(retryAction.tag);
+         }
+     };
+
+     const startEdit = (tag: RecordingTag) => {
+         setEditingTagId(tag.id);
+         setEditName(tag.name);
+         setEditColor(tag.color);
+         setEditIcon(tag.icon);
+         setOperationError(null);
+         setRetryAction(null);
+     };
+
+     const closeCreateDialog = () => {
+         if (busy) return;
+         setCreateDialogOpen(false);
+         setCreateName("");
+         setOperationError(null);
+         setRetryAction(null);
+     };
+
+     const renderError = () =>
+         visibleError ? (
+             <Alert role="alert" variant="destructive">
+                 <AlertTitle>标签操作失败</AlertTitle>
+                 <AlertDescription className="flex flex-wrap items-center gap-3">
+                     <span>{visibleError}</span>
+                     {retryAction ? (
+                         <Button
+                             disabled={busy}
+                             onClick={retry}
+                             size="sm"
+                             type="button"
+                             variant="outline"
+                         >
+                             重试
+                         </Button>
+                     ) : null}
+                 </AlertDescription>
+             </Alert>
+         ) : null;
+
+     const renderPicker = ({
+         color,
+         icon,
+         onColorChange,
+         onIconChange,
+         prefix,
+     }: {
+         color: RecordingTagColor;
+         icon: RecordingTagIcon;
+         onColorChange: (value: RecordingTagColor) => void;
+         onIconChange: (value: RecordingTagIcon) => void;
+         prefix: string;
+     }) => (
+         <div className="grid gap-3 sm:grid-cols-2">
+             <div className="grid gap-2">
+                 <Label id={`${prefix}-color-label`}>颜色</Label>
+                 <ToggleGroup
+                     aria-labelledby={`${prefix}-color-label`}
+                     disabled={busy}
+                     onValueChange={(value) => {
+                         if (
+                             RECORDING_TAG_COLORS.includes(
+                                 value as RecordingTagColor,
+                             )
+                         ) {
+                             onColorChange(value as RecordingTagColor);
+                         }
+                     }}
+                     size="sm"
+                     type="single"
+                     value={color}
+                     variant="outline"
+                 >
+                     {RECORDING_TAG_COLORS.map((item) => (
+                         <ToggleGroupItem
+                             aria-label={recordingTagColorLabel[item]}
+                             key={item}
+                             value={item}
+                         >
+                             {recordingTagColorLabel[item]}
+                         </ToggleGroupItem>
+                     ))}
+                 </ToggleGroup>
+             </div>
+             <div className="grid gap-2">
+                 <Label id={`${prefix}-icon-label`}>图标</Label>
+                 <ToggleGroup
+                     aria-labelledby={`${prefix}-icon-label`}
+                     disabled={busy}
+                     layout="iconGrid"
+                     onValueChange={(value) => {
+                         if (
+                             RECORDING_TAG_ICONS.includes(
+                                 value as RecordingTagIcon,
+                             )
+                         ) {
+                             onIconChange(value as RecordingTagIcon);
+                         }
+                     }}
+                     size="sm"
+                     spacing={1}
+                     type="single"
+                     value={icon}
+                     variant="outline"
+                 >
+                     {RECORDING_TAG_ICONS.map((item) => (
+                         <ToggleGroupItem
+                             aria-label={item}
+                             key={item}
+                             value={item}
+                         >
+                             <RecordingTagIconGlyph icon={item} />
+                         </ToggleGroupItem>
+                     ))}
+                 </ToggleGroup>
+             </div>
+         </div>
+     );
+
+     return (
+         <>
+             <Card
+                 aria-busy={busy || undefined}
+                 className="w-full max-w-xl"
+                 data-control="recording-tag-manager"
+                 data-state={visibleError ? "error" : busy ? "saving" : "ready"}
+             >
+                 <CardHeader className="flex flex-row items-center justify-between gap-4">
+                     <CardTitle className="text-base">管理标签</CardTitle>
+                     {onClose ? (
+                         <Button
+                             aria-label="关闭标签管理"
+                             disabled={busy}
+                             onClick={onClose}
+                             size="icon-sm"
+                             type="button"
+                             variant="ghost"
+                         >
+                             <X />
+                         </Button>
+                     ) : null}
+                 </CardHeader>
+                 <CardContent className="grid gap-5">
+                     {!createDialogOpen ? renderError() : null}
+
+                     <section
+                         aria-labelledby="recording-tags-title"
+                         className="grid gap-3"
+                     >
+                         <div className="flex items-center justify-between gap-3">
+                             <Label id="recording-tags-title">
+                                 这条录音的标签
+                             </Label>
+                             <span className="text-sm text-muted-foreground">
+                                 {recording.tags.length} 个
+                             </span>
+                         </div>
+                         <div className="flex flex-wrap gap-2">
+                             {availableTags.length === 0 ? (
+                                 <p className="text-sm text-muted-foreground">
+                                     尚未创建标签
+                                 </p>
+                             ) : (
+                                 availableTags.map((tag) => {
+                                     const selected = selectedTagIds.has(tag.id);
+                                     return (
+                                         <Button
+                                             aria-pressed={selected}
+                                             data-control="recording-tag-toggle"
+                                             data-tag-id={tag.id}
+                                             disabled={busy}
+                                             key={tag.id}
+                                             onClick={() => toggleTag(tag)}
+                                             size="sm"
+                                             type="button"
+                                             variant={
+                                                 selected
+                                                     ? "secondary"
+                                                     : "outline"
+                                             }
+                                         >
+                                             {pending === "assignment" ? (
+                                                 <LoaderCircle className="animate-spin" />
+                                             ) : (
+                                                 <RecordingTagIconGlyph
+                                                     icon={tag.icon}
+                                                 />
+                                             )}
+                                             {tag.name}
+                                         </Button>
+                                     );
+                                 })
+                             )}
+                         </div>
+                     </section>
+
+                     <Separator />
+
+                     <section
+                         aria-labelledby="recording-tag-catalog-title"
+                         className="grid gap-3"
+                     >
+                         <Label id="recording-tag-catalog-title">标签目录</Label>
+                         <div className="grid gap-2">
+                             {availableTags.map((tag) =>
+                                 editingTagId === tag.id ? (
+                                     <div
+                                         className="grid gap-3 rounded-md border p-3"
+                                         key={tag.id}
+                                     >
+                                         <Input
+                                             aria-label="重命名标签"
+                                             disabled={busy}
+                                             maxLength={
+                                                 MAX_RECORDING_TAG_NAME_LENGTH
+                                             }
+                                             onChange={(event) =>
+                                                 setEditName(event.target.value)
+                                             }
+                                             value={editName}
+                                         />
+                                         {renderPicker({
+                                             color: editColor,
+                                             icon: editIcon,
+                                             onColorChange: setEditColor,
+                                             onIconChange: setEditIcon,
+                                             prefix: `edit-${tag.id}`,
+                                         })}
+                                         <div className="flex flex-wrap justify-end gap-2">
+                                             <Button
+                                                 disabled={busy}
+                                                 onClick={() =>
+                                                     setEditingTagId(null)
+                                                 }
+                                                 size="sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 取消
+                                             </Button>
+                                             <Button
+                                                 disabled={
+                                                     !editName.trim() || busy
+                                                 }
+                                                 onClick={() =>
+                                                     void updateTag(tag, {
+                                                         color: editColor,
+                                                         icon: editIcon,
+                                                         name: editName.trim(),
+                                                     })
+                                                 }
+                                                 size="sm"
+                                                 type="button"
+                                             >
+                                                 {pending ===
+                                                 `update-${tag.id}` ? (
+                                                     <LoaderCircle className="animate-spin" />
+                                                 ) : null}
+                                                 保存
+                                             </Button>
+                                         </div>
+                                     </div>
+                                 ) : (
+                                     <div
+                                         className="flex items-center justify-between gap-3 rounded-md border p-3"
+                                         key={tag.id}
+                                     >
+                                         <div className="flex min-w-0 items-center gap-2">
+                                             <RecordingTagIconGlyph
+                                                 icon={tag.icon}
+                                             />
+                                             <span className="truncate font-medium">
+                                                 {tag.name}
+                                             </span>
+                                             <span className="text-sm text-muted-foreground">
+                                                 {tag.recordingCount ?? 0} 条录音
+                                             </span>
+                                         </div>
+                                         <div className="flex shrink-0 gap-1">
+                                             <Button
+                                                 aria-label={`编辑 ${tag.name}`}
+                                                 disabled={busy}
+                                                 onClick={() => startEdit(tag)}
+                                                 size="icon-sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 <Pencil />
+                                             </Button>
+                                             <Button
+                                                 aria-label={`删除 ${tag.name}`}
+                                                 disabled={busy}
+                                                 onClick={() =>
+                                                     setDeleteTarget(tag)
+                                                 }
+                                                 size="icon-sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 <Trash2 />
+                                             </Button>
+                                         </div>
+                                     </div>
+                                 ),
+                             )}
+                         </div>
+                     </section>
+                 </CardContent>
+                 <CardFooter className="border-t pt-5">
+                     <Dialog
+                         onOpenChange={(open) => {
+                             if (open) {
+                                 setDeleteTarget(null);
+                                 setOperationError(null);
+                                 setRetryAction(null);
+                                 setCreateDialogOpen(true);
+                                 return;
+                             }
+                             closeCreateDialog();
+                         }}
+                         open={createDialogOpen}
+                     >
+                         <DialogTrigger asChild>
+                             <Button
+                                 data-control="recording-tag-create"
+                                 type="button"
+                             >
+                                 <Plus />
+                                 新建标签
+                             </Button>
+                         </DialogTrigger>
+                         <DialogContent aria-describedby="recording-tag-create-description">
+                             <DialogHeader>
+                                 <DialogTitle>新建标签</DialogTitle>
+                                 <DialogDescription id="recording-tag-create-description">
+                                     创建后会自动添加到这条录音。
+                                 </DialogDescription>
+                             </DialogHeader>
+                             <form
+                                 className="grid gap-5"
+                                 onSubmit={(event) => {
+                                     event.preventDefault();
+                                     void createTag({
+                                         color: createColor,
+                                         icon: createIcon,
+                                         name: createName.trim(),
+                                     });
+                                 }}
+                             >
+                                 {renderError()}
+                                 <div className="grid gap-2">
+                                     <Label htmlFor="recording-tag-create-name">
+                                         标签名称
+                                     </Label>
+                                     <Input
+                                         autoFocus
+                                         disabled={busy}
+                                         id="recording-tag-create-name"
+                                         maxLength={
+                                             MAX_RECORDING_TAG_NAME_LENGTH
+                                         }
+                                         onChange={(event) =>
+                                             setCreateName(event.target.value)
+                                         }
+                                         placeholder="例如:待跟进"
+                                         value={createName}
+                                     />
+                                 </div>
+                                 {renderPicker({
+                                     color: createColor,
+                                     icon: createIcon,
+                                     onColorChange: setCreateColor,
+                                     onIconChange: setCreateIcon,
+                                     prefix: "create",
+                                 })}
+                                 <DialogFooter>
+                                     <Button
+                                         disabled={busy}
+                                         onClick={closeCreateDialog}
+                                         type="button"
+                                         variant="outline"
+                                     >
+                                         取消
+                                     </Button>
+                                     <Button
+                                         disabled={!createName.trim() || busy}
+                                         type="submit"
+                                     >
+                                         {pending === "create" ? (
+                                             <LoaderCircle className="animate-spin" />
+                                         ) : (
+                                             <Plus />
+                                         )}
+                                         创建标签
+                                     </Button>
+                                 </DialogFooter>
+                             </form>
+                         </DialogContent>
+                     </Dialog>
+                 </CardFooter>
+             </Card>
+             <Dialog
+                 open={Boolean(deleteTarget)}
+                 onOpenChange={(open) => {
+                     if (!open) {
+                         setDeleteTarget(null);
+                     }
+                 }}
+             >
+                 <DialogContent>
+                     <DialogHeader>
+                         <DialogTitle>删除标签</DialogTitle>
+                         <DialogDescription>
+                             {deleteTarget
+                                 ? `“${deleteTarget.name}”会从所有录音中移除。`
+                                 : ""}
+                         </DialogDescription>
+                     </DialogHeader>
+                     <DialogFooter>
+                         <Button
+                             onClick={() => setDeleteTarget(null)}
+                             type="button"
+                             variant="outline"
+                         >
+                             取消
+                         </Button>
+                         <Button
+                             disabled={busy}
+                             onClick={confirmDelete}
+                             type="button"
+                             variant="destructive"
+                         >
+                             删除标签
+                         </Button>
+                     </DialogFooter>
+                 </DialogContent>
+             </Dialog>
+         </>
+     );
+ }
+

 ❯ src/tests/full-ui-replacement-regression.test.ts:4025:28
src/tests/full-ui-replacement-regression.test.ts > full UI replacement regression coverage > keeps dashboard source, search, activity, list, and settings SOT entries
Stack Traces | 0.13s run time
AssertionError: expected -1 to be greater than or equal to 0
 ❯ extractOpeningElementAt src/tests/full-ui-replacement-regression.test.ts:1077:19
 ❯ extractOpeningElement src/tests/full-ui-replacement-regression.test.ts:1067:12
 ❯ src/tests/full-ui-replacement-regression.test.ts:8397:43
src/tests/data-sources-core-db-readiness-contract.test.ts > Data Sources core database readiness contract > 'disconnect' returns 500 while the core database lock is retained, then retries to 401 after release
Stack Traces | 16.4s run time
Error: Real HTTP request to disconnect failed:
▲ Next.js 16.2.6 (webpack)
- Local:         http://127.0.0.1:34491
- Network:       http://127.0.0.1:34491
✓ Ready in 521ms

  We detected TypeScript in your project and reconfigured your tsconfig.json file for you.
  The following suggested values were added to your tsconfig.json. These values can be changed to fit your project's needs:

  	- include was updated to add '..../dev/dev/types/**/*.ts'


○ Compiling .../api/data-sources/disconnect ...

 ❯ callRoute src/tests/data-sources-core-db-readiness-contract.test.ts:230:15
 ❯ warmRoute src/tests/data-sources-core-db-readiness-contract.test.ts:238:13
 ❯ src/tests/data-sources-core-db-readiness-contract.test.ts:260:13

Caused by: Caused by: TimeoutError: The operation was aborted due to timeout
 ❯ callRoute src/tests/data-sources-core-db-readiness-contract.test.ts:220:16
 ❯ warmRoute src/tests/data-sources-core-db-readiness-contract.test.ts:238:13
 ❯ src/tests/data-sources-core-db-readiness-contract.test.ts:260:13

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 23, INDEX_SIZE_ERR: 1, DOMSTRING_SIZE_ERR: 2, HIERARCHY_REQUEST_ERR: 3, WRONG_DOCUMENT_ERR: 4, INVALID_CHARACTER_ERR: 5, NO_DATA_ALLOWED_ERR: 6, NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9, INUSE_ATTRIBUTE_ERR: 10, INVALID_STATE_ERR: 11, SYNTAX_ERR: 12, INVALID_MODIFICATION_ERR: 13, NAMESPACE_ERR: 14, INVALID_ACCESS_ERR: 15, VALIDATION_ERR: 16, TYPE_MISMATCH_ERR: 17, SECURITY_ERR: 18, NETWORK_ERR: 19, ABORT_ERR: 20, URL_MISMATCH_ERR: 21, QUOTA_EXCEEDED_ERR: 22, TIMEOUT_ERR: 23, INVALID_NODE_TYPE_ERR: 24, DATA_CLONE_ERR: 25 }
src/tests/data-sources-core-db-readiness-contract.test.ts > Data Sources core database readiness contract > 'sources-put' returns 500 while the core database lock is retained, then retries to 401 after release
Stack Traces | 16.7s run time
Error: Real HTTP request to sources-put failed:
▲ Next.js 16.2.6 (webpack)
- Local:         http://127.0.0.1:33641
- Network:       http://127.0.0.1:33641
✓ Ready in 644ms

  We detected TypeScript in your project and reconfigured your tsconfig.json file for you.
  The following suggested values were added to your tsconfig.json. These values can be changed to fit your project's needs:

  	- include was updated to add '..../dev/dev/types/**/*.ts'


○ Compiling /api/data-sources ...

 ❯ callRoute src/tests/data-sources-core-db-readiness-contract.test.ts:230:15
 ❯ warmRoute src/tests/data-sources-core-db-readiness-contract.test.ts:238:13
 ❯ src/tests/data-sources-core-db-readiness-contract.test.ts:260:13

Caused by: Caused by: TimeoutError: The operation was aborted due to timeout
 ❯ callRoute src/tests/data-sources-core-db-readiness-contract.test.ts:220:16
 ❯ warmRoute src/tests/data-sources-core-db-readiness-contract.test.ts:238:13
 ❯ src/tests/data-sources-core-db-readiness-contract.test.ts:260:13

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 23, INDEX_SIZE_ERR: 1, DOMSTRING_SIZE_ERR: 2, HIERARCHY_REQUEST_ERR: 3, WRONG_DOCUMENT_ERR: 4, INVALID_CHARACTER_ERR: 5, NO_DATA_ALLOWED_ERR: 6, NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9, INUSE_ATTRIBUTE_ERR: 10, INVALID_STATE_ERR: 11, SYNTAX_ERR: 12, INVALID_MODIFICATION_ERR: 13, NAMESPACE_ERR: 14, INVALID_ACCESS_ERR: 15, VALIDATION_ERR: 16, TYPE_MISMATCH_ERR: 17, SECURITY_ERR: 18, NETWORK_ERR: 19, ABORT_ERR: 20, URL_MISMATCH_ERR: 21, QUOTA_EXCEEDED_ERR: 22, TIMEOUT_ERR: 23, INVALID_NODE_TYPE_ERR: 24, DATA_CLONE_ERR: 25 }
src/tests/data-sources-core-db-readiness-contract.test.ts > Data Sources core database readiness contract > 'sources-get' returns 500 while the core database lock is retained, then retries to 401 after release
Stack Traces | 16.7s run time
Error: Real HTTP request to sources-get failed:
▲ Next.js 16.2.6 (webpack)
- Local:         http://127.0.0.1:40099
- Network:       http://127.0.0.1:40099
✓ Ready in 666ms

  We detected TypeScript in your project and reconfigured your tsconfig.json file for you.
  The following suggested values were added to your tsconfig.json. These values can be changed to fit your project's needs:

  	- include was updated to add '..../dev/dev/types/**/*.ts'


○ Compiling /api/data-sources ...

 ❯ callRoute src/tests/data-sources-core-db-readiness-contract.test.ts:230:15
 ❯ warmRoute src/tests/data-sources-core-db-readiness-contract.test.ts:238:13
 ❯ src/tests/data-sources-core-db-readiness-contract.test.ts:260:13

Caused by: Caused by: TimeoutError: The operation was aborted due to timeout
 ❯ callRoute src/tests/data-sources-core-db-readiness-contract.test.ts:220:16
 ❯ warmRoute src/tests/data-sources-core-db-readiness-contract.test.ts:238:13
 ❯ src/tests/data-sources-core-db-readiness-contract.test.ts:260:13

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 23, INDEX_SIZE_ERR: 1, DOMSTRING_SIZE_ERR: 2, HIERARCHY_REQUEST_ERR: 3, WRONG_DOCUMENT_ERR: 4, INVALID_CHARACTER_ERR: 5, NO_DATA_ALLOWED_ERR: 6, NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9, INUSE_ATTRIBUTE_ERR: 10, INVALID_STATE_ERR: 11, SYNTAX_ERR: 12, INVALID_MODIFICATION_ERR: 13, NAMESPACE_ERR: 14, INVALID_ACCESS_ERR: 15, VALIDATION_ERR: 16, TYPE_MISMATCH_ERR: 17, SECURITY_ERR: 18, NETWORK_ERR: 19, ABORT_ERR: 20, URL_MISMATCH_ERR: 21, QUOTA_EXCEEDED_ERR: 22, TIMEOUT_ERR: 23, INVALID_NODE_TYPE_ERR: 24, DATA_CLONE_ERR: 25 }
View the full list of 3 ❄️ flaky test(s)
src/tests/dashboard-ui-foundation.test.ts > dashboard SOT foundation > keeps SOT global tokens and system banner state semantics available

Flake rate in main: 100.00% (Passed 0 times, Failed 1 times)

Stack Traces | 0.0141s run time
AssertionError: expected '"use client";\n\nimport {\n    Downlo…' to contain 'const systemBannerAlertClassNames'

- Expected
+ Received

- const systemBannerAlertClassNames
+ "use client";
+
+ import {
+     Download,
+     LockKeyhole,
+     Package,
+     Search,
+     ShieldX,
+     Upload,
+     WifiOff,
+     X,
+ } from "lucide-react";
+ import {
+     type ComponentProps,
+     type ReactNode,
+     useEffect,
+     useState,
+ } from "react";
+ import { useLanguage } from "@/components/language-provider";
+ import { Alert, AlertDescription, AlertTitle } from "@.../components/ui/alert";
+ import { Button, type ButtonProps } from "@.../components/ui/button";
+ import { Progress } from "@.../components/ui/progress";
+ import { hasBrowserWindow } from "@.../lib/platform/runtime";
+
+ type SystemBannerState =
+     | "offline"
+     | "permission-denied"
+     | "db-locked"
+     | "runtime-unavailable"
+     | "update-available"
+     | "import-progress"
+     | "export-progress";
+
+ interface SystemBannerEventDetail {
+     actionLabel?: string;
+     dismissLabel?: string;
+     id?: string;
+     indeterminate?: boolean;
+     message?: string;
+     progress?: number;
+     secondaryActionLabel?: string;
+     state: SystemBannerState | null;
+     title?: string;
+ }
+
+ type VisibleSystemBanner = SystemBannerEventDetail & {
+     state: SystemBannerState;
+ };
+
+ interface SystemBannerProps {
+     className?: string;
+ }
+
+ interface SystemBannerDefaultActions {
+     actionLabel?: string;
+     dismissLabel?: string;
+     secondaryActionLabel?: string;
+ }
+
+ type SystemBannerActionRole = "primary" | "secondary";
+ type SystemBannerButtonTone = "action" | "primary" | "dismiss";
+
+ interface SystemBannerItemProps {
+     banner: VisibleSystemBanner;
+     className?: string;
+     isStacked: boolean;
+     isZh: boolean;
+     onDismiss: (banner: VisibleSystemBanner) => void;
+ }
+
+ interface SystemBannerAlertProps {
+     a11y: ReturnType<typeof getBannerA11y>;
+     banner: VisibleSystemBanner;
+     children: ReactNode;
+     className?: string;
+ }
+
+ type SystemBannerButtonProps = Omit<ButtonProps, "size" | "variant"> & {
+     tone?: SystemBannerButtonTone;
+ };
+
+ type SystemBannerAlertVariant = NonNullable<
+     Parameters<typeof Alert>[0]["variant"]
+ >;
+ type SystemBannerIconProps = Omit<ComponentProps<typeof WifiOff>, "children">;
+
+ interface SystemBannerProgressProps {
+     indeterminate: boolean | undefined;
+     value: number;
+ }
+
+ const systemBannerAlertVariantByState: Record<
+     SystemBannerState,
+     SystemBannerAlertVariant
+ > = {
+     "db-locked": "destructiveSoftNeutral",
+     "runtime-unavailable": "destructiveSoftNeutral",
+     "export-progress": "default",
+     "import-progress": "default",
+     offline: "default",
+     "permission-denied": "destructiveSoftNeutral",
+     "update-available": "default",
+ } as const;
+
+ function getDefaultCopy(state: SystemBannerState, isZh: boolean) {
+     switch (state) {
+         case "offline":
+             return {
+                 title: isZh ? "当前无网络连接" : "Offline",
+                 message: isZh
+                     ? "所有已下载的录音与逐字稿可继续阅览 · 来源同步与新转写已暂停。"
+                     : "Downloaded recordings and transcripts remain readable. Source sync and new transcription are paused.",
+             };
+         case "permission-denied":
+             return {
+                 title: isZh
+                     ? "未授权访问录音文件夹"
+                     : "Recording folder permission denied",
+                 message: isZh
+                     ? "无法读取来源缓存目录 · 前往「系统设置 · 隐私与安全性 · 完全磁盘访问」打开开关。"
+                     : "The source cache folder cannot be read. Open System Settings > Privacy & Security > Full Disk Access.",
+             };
+         case "db-locked":
+             return {
+                 title: isZh
+                     ? "本地数据库被另一个 BetterAINote 实例占用"
+                     : "Local database is locked by another BetterAINote instance",
+                 message: isZh
+                     ? "同时只允许一个实例写入 · 当前实例已切到只读模式 · 关闭其它窗口后点「重新连接」。"
+                     : "Only one instance can write at a time. This instance is read-only until other windows close.",
+             };
+         case "runtime-unavailable":
+             return {
+                 title: isZh
+                     ? "同步运行时暂时不可用"
+                     : "Sync runtime is temporarily unavailable",
+                 message: isZh
+                     ? "自动同步暂时无法运行。已下载的录音仍可阅览,稍后可重新尝试同步。"
+                     : "Automatic sync is temporarily unavailable. Downloaded recordings remain readable and you can retry sync shortly.",
+             };
+         case "update-available":
+             return {
+                 title: isZh
+                     ? "BetterAINote 有可用更新"
+                     : "BetterAINote update available",
+                 message: isZh
+                     ? "重启后将应用最新版本。"
+                     : "Restart to apply the latest version.",
+             };
+         case "import-progress":
+             return {
+                 title: isZh
+                     ? "正在导入 BetterAINote 备份包"
+                     : "Importing BetterAINote backup",
+                 message: isZh
+                     ? "录音和来源内容正在写入本地。"
+                     : "Recordings and source content are being saved locally.",
+             };
+         case "export-progress":
+             return {
+                 title: isZh ? "正在导出录音" : "Exporting recordings",
+                 message: isZh
+                     ? "导出文件准备中,请保持当前页面打开。"
+                     : "Export files are being prepared. Keep this page open.",
+             };
+     }
+ }
+
+ function getStackedCopy(state: SystemBannerState, isZh: boolean) {
+     switch (state) {
+         case "offline":
+             return {
+                 title: isZh ? "当前无网络连接" : "Offline",
+                 message: isZh
+                     ? "来源同步已暂停 · 已下载的录音仍可阅览。"
+                     : "Source sync is paused. Downloaded recordings remain readable.",
+             };
+         case "update-available":
+             return {
+                 title: isZh ? "有可用更新" : "Update available",
+                 message: isZh
+                     ? "重启后将应用。"
+                     : "It will be applied after restart.",
+             };
+         default:
+             return getDefaultCopy(state, isZh);
+     }
+ }
+
+ function getPriority(state: SystemBannerState) {
+     switch (state) {
+         case "permission-denied":
+         case "db-locked":
+         case "runtime-unavailable":
+             return 0;
+         case "offline":
+             return 1;
+         case "import-progress":
+         case "export-progress":
+             return 2;
+         case "update-available":
+             return 3;
+     }
+ }
+
+ function getBannerA11y(state: SystemBannerState): {
+     "aria-live"?: "polite";
+     role?: "alert" | "status";
+ } {
+     if (state === "offline") {
+         return { "aria-live": "polite" as const, role: "status" as const };
+     }
+     if (
+         state === "permission-denied" ||
+         state === "db-locked" ||
+         state === "runtime-unavailable"
+     ) {
+         return { role: "alert" as const };
+     }
+     return {};
+ }
+
+ function normalizeProgress(progress: number | undefined) {
+     if (typeof progress !== "number" || Number.isNaN(progress)) return null;
+     const clamped = Math.min(100, Math.max(0, progress));
+     return Math.round(clamped / 10) * 10;
+ }
+
+ function getDefaultActions(
+     state: SystemBannerState,
+     isZh: boolean,
+ ): SystemBannerDefaultActions {
+     switch (state) {
+         case "offline":
+             return {
+                 actionLabel: isZh ? "重试" : "Retry",
+                 dismissLabel: isZh ? "收起" : "Dismiss",
+             };
+         case "permission-denied":
+             return {
+                 actionLabel: isZh ? "打开系统设置" : "Open System Settings",
+                 secondaryActionLabel: isZh ? "稍后" : "Later",
+             };
+         case "db-locked":
+             return {
+                 actionLabel: isZh ? "重新连接" : "Reconnect",
+                 secondaryActionLabel: isZh ? "只读继续" : "Continue read-only",
+             };
+         case "runtime-unavailable":
+             return {
+                 actionLabel: isZh ? "重试同步" : "Retry sync",
+                 dismissLabel: isZh ? "收起" : "Dismiss",
+             };
+         case "update-available":
+             return {
+                 actionLabel: isZh ? "重启并更新" : "Restart and update",
+                 secondaryActionLabel: isZh ? "查看更新内容" : "View changes",
+                 dismissLabel: isZh ? "稍后再说" : "Later",
+             };
+         case "import-progress":
+             return {
+                 actionLabel: isZh ? "暂停" : "Pause",
+                 secondaryActionLabel: isZh ? "取消" : "Cancel",
+             };
+         case "export-progress":
+             return {
+                 actionLabel: isZh ? "在 Finder 中显示" : "Show in Finder",
+                 secondaryActionLabel: isZh ? "取消" : "Cancel",
+             };
+     }
+ }
+
+ function SystemBannerIcon({
+     indeterminate,
+     state,
+     ...props
+ }: {
+     indeterminate: boolean | undefined;
+     state: SystemBannerState;
+ } & SystemBannerIconProps) {
+     if (state === "import-progress" && indeterminate) {
+         return <Search {...props} />;
+     }
+
+     switch (state) {
+         case "offline":
+             return <WifiOff {...props} />;
+         case "permission-denied":
+             return <ShieldX {...props} />;
+         case "db-locked":
+         case "runtime-unavailable":
+             return <LockKeyhole {...props} />;
+         case "update-available":
+             return <Package {...props} />;
+         case "import-progress":
+             return <Upload {...props} />;
+         case "export-progress":
+             return <Download {...props} />;
+     }
+ }
+
+ function SystemBannerAlert({
+     a11y,
+     banner,
+     children,
+     className,
+ }: SystemBannerAlertProps) {
+     return (
+         <Alert
+             aria-live={a11y["aria-live"]}
+             data-control="system-banner"
+             data-state={banner.state}
+             density="comfortable"
+             layout="inline"
+             role={a11y.role}
+             variant={systemBannerAlertVariantByState[banner.state]}
+             className={className}
+         >
+             {children}
+         </Alert>
+     );
+ }
+
+ function SystemBannerButton({
+     className,
+     tone = "action",
+     ...props
+ }: SystemBannerButtonProps) {
+     return (
+         <Button
+             size="sm"
+             variant={tone === "primary" ? "outline" : "ghost"}
+             className={className}
+             {...props}
+         />
+     );
+ }
+
+ function SystemBannerProgress({
+     indeterminate,
+     value,
+ }: SystemBannerProgressProps) {
+     return (
+         <Progress
+             aria-hidden="true"
+             className="min-w-[120px] flex-1"
+             indicatorClassName={
+                 indeterminate
+                     ? "w-[32%] animate-[sbn-sweep_1.4s_linear_infinite]"
+                     : undefined
+             }
+             value={value}
+         />
+     );
+ }
+
+ function getRenderedActions(
+     banner: VisibleSystemBanner,
+     defaultActions: SystemBannerDefaultActions,
+     isStacked: boolean,
+     isZh: boolean,
+ ) {
+     if (banner.indeterminate && banner.state === "import-progress") {
+         return {
+             primaryLabel:
+                 banner.actionLabel ??
+                 banner.secondaryActionLabel ??
+                 defaultActions.secondaryActionLabel,
+             primaryRole: "primary" as const,
+             secondaryLabel: undefined,
+             dismissLabel: undefined,
+         };
+     }
+
+     if (isStacked) {
+         return {
+             primaryLabel:
+                 banner.state === "update-available"
+                     ? (banner.secondaryActionLabel ??
+                       banner.actionLabel ??
+                       (isZh ? "查看" : "View"))
+                     : (banner.actionLabel ?? defaultActions.actionLabel),
+             primaryRole:
+                 banner.state === "update-available" && !banner.actionLabel
+                     ? ("secondary" as const)
+                     : ("primary" as const),
+             secondaryLabel: undefined,
+             dismissLabel:
+                 banner.state === "runtime-unavailable"
+                     ? (banner.dismissLabel ?? defaultActions.dismissLabel)
+                     : undefined,
+         };
+     }
+
+     return {
+         primaryLabel: banner.actionLabel ?? defaultActions.actionLabel,
+         primaryRole: "primary" as const,
+         secondaryLabel:
+             banner.secondaryActionLabel ?? defaultActions.secondaryActionLabel,
+         dismissLabel: banner.dismissLabel ?? defaultActions.dismissLabel,
+     };
+ }
+
+ function getSystemBannerActionName(
+     state: SystemBannerState,
+     role: SystemBannerActionRole,
+ ) {
+     if (role === "secondary") {
+         switch (state) {
+             case "permission-denied":
+                 return "later";
+             case "db-locked":
+                 return "continue-read-only";
+             case "runtime-unavailable":
+                 return "dismiss";
+             case "update-available":
+                 return "view-update-changes";
+             case "import-progress":
+                 return "cancel-import";
+             case "export-progress":
+                 return "cancel-export";
+             case "offline":
+                 return "dismiss";
+         }
+     }
+
+     switch (state) {
+         case "offline":
+             return "retry";
+         case "permission-denied":
+             return "open-system-settings";
+         case "db-locked":
+             return "reconnect";
+         case "runtime-unavailable":
+             return "retry-sync";
+         case "update-available":
+             return "restart-and-update";
+         case "import-progress":
+             return "pause-import";
+         case "export-progress":
+             return "show-export";
+     }
+ }
+
+ function dispatchSystemBannerAction(
+     banner: VisibleSystemBanner,
+     role: SystemBannerActionRole,
+ ) {
+     if (!hasBrowserWindow()) {
+         return;
+     }
+
+     window.dispatchEvent(
+         new CustomEvent("betterainote:system-banner-action", {
+             detail: {
+                 action: getSystemBannerActionName(banner.state, role),
+                 id: banner.id ?? banner.state,
+                 role,
+                 state: banner.state,
+             },
+         }),
+     );
+ }
+
+ function SystemBannerItem({
+     banner,
+     className,
+     isStacked,
+     isZh,
+     onDismiss,
+ }: SystemBannerItemProps) {
+     const defaultCopy = isStacked
+         ? getStackedCopy(banner.state, isZh)
+         : getDefaultCopy(banner.state, isZh);
+     const defaultActions = getDefaultActions(banner.state, isZh);
+     const progress = normalizeProgress(banner.progress);
+     const hasProgress =
+         banner.state === "import-progress" ||
+         banner.state === "export-progress";
+     const { dismissLabel, primaryLabel, primaryRole, secondaryLabel } =
+         getRenderedActions(banner, defaultActions, isStacked, isZh);
+     const bannerA11y = getBannerA11y(banner.state);
+     const primaryActionTone =
+         banner.state === "update-available" && !isStacked
+             ? "primary"
+             : "action";
+     const handleAction = (role: SystemBannerActionRole) => {
+         dispatchSystemBannerAction(banner, role);
+
+         if (role === "primary" && banner.state === "update-available") {
+             if (hasBrowserWindow()) {
+                 window.location.reload();
+             }
+             return;
+         }
+
+         if (role === "secondary") {
+             onDismiss(banner);
+         }
+     };
+
+     return (
+         <SystemBannerAlert
+             a11y={bannerA11y}
+             banner={banner}
+             className={className}
+         >
+             <SystemBannerIcon
+                 indeterminate={banner.indeterminate}
+                 state={banner.state}
+                 aria-hidden="true"
+             />
+             <div className="flex min-w-0 flex-1 flex-col gap-0.5">
+                 <AlertTitle>{banner.title ?? defaultCopy.title}</AlertTitle>
+                 <AlertDescription>
+                     {banner.message ?? defaultCopy.message}
+                 </AlertDescription>
+                 {hasProgress ? (
+                     <SystemBannerProgress
+                         indeterminate={banner.indeterminate}
+                         value={progress ?? 0}
+                     />
+                 ) : null}
+             </div>
+             <div className="flex flex-none gap-1.5">
+                 {primaryLabel ? (
+                     <SystemBannerButton
+                         aria-busy={
+                             banner.indeterminate &&
+                             banner.state === "import-progress"
+                                 ? true
+                                 : undefined
+                         }
+                         disabled={
+                             banner.indeterminate &&
+                             banner.state === "import-progress"
+                         }
+                         onClick={() => handleAction(primaryRole)}
+                         tone={primaryActionTone}
+                         type="button"
+                     >
+                         {primaryLabel}
+                     </SystemBannerButton>
+                 ) : null}
+                 {secondaryLabel ? (
+                     <SystemBannerButton
+                         onClick={() => handleAction("secondary")}
+                         type="button"
+                     >
+                         {secondaryLabel}
+                     </SystemBannerButton>
+                 ) : null}
+                 {dismissLabel ? (
+                     <SystemBannerButton
+                         aria-label={dismissLabel}
+                         onClick={() => onDismiss(banner)}
+                         tone="dismiss"
+                         type="button"
+                     >
+                         <X data-icon="inline-start" aria-hidden="true" />
+                     </SystemBannerButton>
+                 ) : null}
+             </div>
+         </SystemBannerAlert>
+     );
+ }
+
+ export function SystemBanner({ className }: SystemBannerProps) {
+     const { language } = useLanguage();
+     const isZh = language === "zh-CN";
+     const [eventDetails, setEventDetails] = useState<SystemBannerEventDetail[]>(
+         [],
+     );
+     const [online, setOnline] = useState(() =>
+         hasBrowserWindow() ? navigator.onLine : true,
+     );
+     const [offlineDismissed, setOfflineDismissed] = useState(false);
+
+     useEffect(() => {
+         if (!hasBrowserWindow()) {
+             return;
+         }
+
+         const handleOnline = () => {
+             setOnline(true);
+             setOfflineDismissed(false);
+         };
+         const handleOffline = () => {
+             setOnline(false);
+             setOfflineDismissed(false);
+         };
+         const handleSystemBanner = (event: Event) => {
+             const detail = (event as CustomEvent<SystemBannerEventDetail>)
+                 .detail;
+             setEventDetails((current) => {
+                 if (!detail?.state) {
+                     if (detail?.id) {
+                         return current.filter((item) => item.id !== detail.id);
+                     }
+                     return [];
+                 }
+
+                 const key = detail.id ?? detail.state;
+                 const next = current.filter(
+                     (item) => (item.id ?? item.state) !== key,
+                 );
+                 next.push({ ...detail, id: key });
+                 return next;
+             });
+         };
+
+         window.addEventListener("online", handleOnline);
+         window.addEventListener("offline", handleOffline);
+         window.addEventListener(
+             "betterainote:system-banner",
+             handleSystemBanner,
+         );
+
+         return () => {
+             window.removeEventListener("online", handleOnline);
+             window.removeEventListener("offline", handleOffline);
+             window.removeEventListener(
+                 "betterainote:system-banner",
+                 handleSystemBanner,
+             );
+         };
+     }, []);
+
+     const visibleBanners = [
+         ...(online
+             ? []
+             : [
+                   ...(offlineDismissed
+                       ? []
+                       : [
+                             {
+                                 id: "offline",
+                                 state: "offline" as const,
+                             },
+                         ]),
+               ]),
+         ...eventDetails.filter((detail): detail is VisibleSystemBanner =>
+             Boolean(detail.state),
+         ),
+     ]
+         .filter(
+             (banner, index, banners) =>
+                 banners.findIndex(
+                     (item) => (item.id ?? item.state) === banner.id,
+                 ) === index,
+         )
+         .sort((a, b) => getPriority(a.state) - getPriority(b.state))
+         .slice(0, 2);
+
+     if (visibleBanners.length === 0) {
+         return null;
+     }
+
+     const dismissBanner = (banner: VisibleSystemBanner) => {
+         if (banner.state === "offline") {
+             setOfflineDismissed(true);
+             return;
+         }
+         setEventDetails((current) =>
+             current.filter(
+                 (item) =>
+                     (item.id ?? item.state) !== (banner.id ?? banner.state),
+             ),
+         );
+     };
+
+     return (
+         <>
+             {visibleBanners.map((banner) => (
+                 <SystemBannerItem
+                     banner={banner}
+                     className={className}
+                     isStacked={visibleBanners.length > 1}
+                     isZh={isZh}
+                     key={banner.id ?? banner.state}
+                     onDismiss={dismissBanner}
+                 />
+             ))}
+         </>
+     );
+ }
+

 ❯ src/tests/dashboard-ui-foundation.test.ts:5536:24
src/tests/full-ui-replacement-regression.test.ts > full UI replacement regression coverage > keeps settings and recording detail surfaces on SOT state contracts

Flake rate in main: 100.00% (Passed 0 times, Failed 1 times)

Stack Traces | 0.0479s run time
AssertionError: expected -1 to be greater than or equal to 0
 ❯ extractBoundedSlice src/tests/full-ui-replacement-regression.test.ts:880:19
 ❯ src/tests/full-ui-replacement-regression.test.ts:10899:38
src/tests/recording-detail-copy-ui-regression.test.ts > recording detail copy and title action UI regressions > keeps recording tag manager on shadcn primitives and semantic tokens

Flake rate in main: 100.00% (Passed 0 times, Failed 1 times)

Stack Traces | 0.00443s run time
AssertionError: expected '"use client";\n\nimport { LoaderCircl…' to contain 'RECORDING_TAG_MANAGER_PANEL_CLASS_NAME'

- Expected
+ Received

- RECORDING_TAG_MANAGER_PANEL_CLASS_NAME
+ "use client";
+
+ import { LoaderCircle, Pencil, Plus, Trash2, X } from "lucide-react";
+ import { useMemo, useState } from "react";
+ import { Alert, AlertDescription, AlertTitle } from "@.../components/ui/alert";
+ import { Button } from "@.../components/ui/button";
+ import {
+     Card,
+     CardContent,
+     CardFooter,
+     CardHeader,
+     CardTitle,
+ } from "@.../components/ui/card";
+ import {
+     Dialog,
+     DialogContent,
+     DialogDescription,
+     DialogFooter,
+     DialogHeader,
+     DialogTitle,
+     DialogTrigger,
+ } from "@.../components/ui/dialog";
+ import { Input } from "@.../components/ui/input";
+ import { Label } from "@.../components/ui/label";
+ import { Separator } from "@.../components/ui/separator";
+ import { ToggleGroup, ToggleGroupItem } from "@.../components/ui/toggle-group";
+ import {
+     MAX_RECORDING_TAG_NAME_LENGTH,
+     RECORDING_TAG_COLORS,
+     RECORDING_TAG_ICONS,
+     type RecordingTag,
+     type RecordingTagColor,
+     type RecordingTagIcon,
+ } from "@/lib/recording-tags";
+ import type { Recording } from "@/types/recording";
+ import {
+     RecordingTagIconGlyph,
+     recordingTagColorLabel,
+ } from "./recording-tag-visuals";
+
+ interface RecordingTagManagerProps {
+     recording: Recording;
+     availableTags: RecordingTag[];
+     onAvailableTagsChange: (tags: RecordingTag[]) => void;
+     onRecordingTagsChange: (recordingId: string, tags: RecordingTag[]) => void;
+     loadError?: string | null;
+     onClose?: () => void;
+ }
+
+ type TagPayload = Pick<RecordingTag, "color" | "icon" | "name">;
+ type AssignmentPayload = { tagIds: string[]; tags: RecordingTag[] };
+ type RetryAction =
+     | { payload: AssignmentPayload; type: "assignment" }
+     | { payload: TagPayload; type: "create" }
+     | { payload: TagPayload; tag: RecordingTag; type: "update" }
+     | { tag: RecordingTag; type: "delete" };
+
+ async function readJsonResponse(response: Response) {
+     const data = await response.json().catch(() => ({}));
+     if (!response.ok) {
+         throw new Error(
+             typeof data?.error === "string" ? data.error : "Request failed",
+         );
+     }
+     return data;
+ }
+
+ function toErrorMessage(error: unknown, fallback: string) {
+     return error instanceof Error && error.message ? error.message : fallback;
+ }
+
+ export function RecordingTagManager({
+     recording,
+     availableTags,
+     loadError,
+     onAvailableTagsChange,
+     onRecordingTagsChange,
+     onClose,
+ }: RecordingTagManagerProps) {
+     const [createName, setCreateName] = useState("");
+     const [createColor, setCreateColor] = useState<RecordingTagColor>("purple");
+     const [createIcon, setCreateIcon] = useState<RecordingTagIcon>("tag");
+     const [editingTagId, setEditingTagId] = useState<string | null>(null);
+     const [editName, setEditName] = useState("");
+     const [editColor, setEditColor] = useState<RecordingTagColor>("purple");
+     const [editIcon, setEditIcon] = useState<RecordingTagIcon>("tag");
+     const [pending, setPending] = useState<string | null>(null);
+     const [operationError, setOperationError] = useState<string | null>(null);
+     const [retryAction, setRetryAction] = useState<RetryAction | null>(null);
+     const [createDialogOpen, setCreateDialogOpen] = useState(false);
+     const [deleteTarget, setDeleteTarget] = useState<RecordingTag | null>(null);
+
+     const selectedTagIds = useMemo(
+         () => new Set(recording.tags.map((tag) => tag.id)),
+         [recording.tags],
+     );
+     const busy = pending !== null;
+     const visibleError = operationError ?? loadError ?? null;
+
+     const updateCatalog = (tag: RecordingTag) => {
+         onAvailableTagsChange([
+             tag,
+             ...availableTags.filter((item) => item.id !== tag.id),
+         ]);
+     };
+
+     const updateAssignments = async (payload: AssignmentPayload) => {
+         const response = await fetch(`/api/recordings/${recording.id}/tags`, {
+             method: "PUT",
+             headers: { "Content-Type": "application/json" },
+             body: JSON.stringify({ tagIds: payload.tagIds }),
+         });
+         const data = await readJsonResponse(response);
+         const tags = Array.isArray(data.tags) ? data.tags : payload.tags;
+         onRecordingTagsChange(recording.id, tags);
+     };
+
+     const runAssignment = async (payload: AssignmentPayload) => {
+         if (busy) return;
+         setPending("assignment");
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             await updateAssignments(payload);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签保存失败"));
+             setRetryAction({ payload, type: "assignment" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const toggleTag = (tag: RecordingTag) => {
+         const nextTags = selectedTagIds.has(tag.id)
+             ? recording.tags.filter((item) => item.id !== tag.id)
+             : [...recording.tags, tag];
+         void runAssignment({
+             tagIds: nextTags.map((item) => item.id),
+             tags: nextTags,
+         });
+     };
+
+     const createTag = async (payload: TagPayload) => {
+         if (busy || !payload.name.trim()) return;
+         setPending("create");
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch("/api/recording-tags", {
+                 method: "POST",
+                 headers: { "Content-Type": "application/json" },
+                 body: JSON.stringify(payload),
+             });
+             const data = await readJsonResponse(response);
+             const tag = data.tag as RecordingTag;
+             updateCatalog(tag);
+             const nextTags = [...recording.tags, tag];
+             await updateAssignments({
+                 tagIds: nextTags.map((item) => item.id),
+                 tags: nextTags,
+             });
+             setCreateName("");
+             setCreateDialogOpen(false);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签创建失败"));
+             setRetryAction({ payload, type: "create" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const updateTag = async (tag: RecordingTag, payload: TagPayload) => {
+         if (busy || !payload.name.trim()) return;
+         setPending(`update-${tag.id}`);
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch(`/api/recording-tags/${tag.id}`, {
+                 method: "PATCH",
+                 headers: { "Content-Type": "application/json" },
+                 body: JSON.stringify(payload),
+             });
+             const data = await readJsonResponse(response);
+             const updatedTag = data.tag as RecordingTag;
+             updateCatalog(updatedTag);
+             onRecordingTagsChange(
+                 recording.id,
+                 recording.tags.map((item) =>
+                     item.id === updatedTag.id ? updatedTag : item,
+                 ),
+             );
+             setEditingTagId(null);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签更新失败"));
+             setRetryAction({ payload, tag, type: "update" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const deleteTag = async (tag: RecordingTag) => {
+         if (busy) return;
+         setPending(`delete-${tag.id}`);
+         setOperationError(null);
+         setRetryAction(null);
+         try {
+             const response = await fetch(`/api/recording-tags/${tag.id}`, {
+                 method: "DELETE",
+             });
+             await readJsonResponse(response);
+             onAvailableTagsChange(
+                 availableTags.filter((item) => item.id !== tag.id),
+             );
+             onRecordingTagsChange(
+                 recording.id,
+                 recording.tags.filter((item) => item.id !== tag.id),
+             );
+             setEditingTagId(null);
+         } catch (error) {
+             setOperationError(toErrorMessage(error, "标签删除失败"));
+             setRetryAction({ tag, type: "delete" });
+         } finally {
+             setPending(null);
+         }
+     };
+
+     const confirmDelete = () => {
+         if (!deleteTarget || busy) return;
+         const tag = deleteTarget;
+         setDeleteTarget(null);
+         void deleteTag(tag);
+     };
+
+     const retry = () => {
+         if (!retryAction) return;
+         if (retryAction.type === "assignment") {
+             void runAssignment(retryAction.payload);
+         } else if (retryAction.type === "create") {
+             void createTag(retryAction.payload);
+         } else if (retryAction.type === "update") {
+             void updateTag(retryAction.tag, retryAction.payload);
+         } else {
+             void deleteTag(retryAction.tag);
+         }
+     };
+
+     const startEdit = (tag: RecordingTag) => {
+         setEditingTagId(tag.id);
+         setEditName(tag.name);
+         setEditColor(tag.color);
+         setEditIcon(tag.icon);
+         setOperationError(null);
+         setRetryAction(null);
+     };
+
+     const closeCreateDialog = () => {
+         if (busy) return;
+         setCreateDialogOpen(false);
+         setCreateName("");
+         setOperationError(null);
+         setRetryAction(null);
+     };
+
+     const renderError = () =>
+         visibleError ? (
+             <Alert role="alert" variant="destructive">
+                 <AlertTitle>标签操作失败</AlertTitle>
+                 <AlertDescription className="flex flex-wrap items-center gap-3">
+                     <span>{visibleError}</span>
+                     {retryAction ? (
+                         <Button
+                             disabled={busy}
+                             onClick={retry}
+                             size="sm"
+                             type="button"
+                             variant="outline"
+                         >
+                             重试
+                         </Button>
+                     ) : null}
+                 </AlertDescription>
+             </Alert>
+         ) : null;
+
+     const renderPicker = ({
+         color,
+         icon,
+         onColorChange,
+         onIconChange,
+         prefix,
+     }: {
+         color: RecordingTagColor;
+         icon: RecordingTagIcon;
+         onColorChange: (value: RecordingTagColor) => void;
+         onIconChange: (value: RecordingTagIcon) => void;
+         prefix: string;
+     }) => (
+         <div className="grid gap-3 sm:grid-cols-2">
+             <div className="grid gap-2">
+                 <Label id={`${prefix}-color-label`}>颜色</Label>
+                 <ToggleGroup
+                     aria-labelledby={`${prefix}-color-label`}
+                     disabled={busy}
+                     onValueChange={(value) => {
+                         if (
+                             RECORDING_TAG_COLORS.includes(
+                                 value as RecordingTagColor,
+                             )
+                         ) {
+                             onColorChange(value as RecordingTagColor);
+                         }
+                     }}
+                     size="sm"
+                     type="single"
+                     value={color}
+                     variant="outline"
+                 >
+                     {RECORDING_TAG_COLORS.map((item) => (
+                         <ToggleGroupItem
+                             aria-label={recordingTagColorLabel[item]}
+                             key={item}
+                             value={item}
+                         >
+                             {recordingTagColorLabel[item]}
+                         </ToggleGroupItem>
+                     ))}
+                 </ToggleGroup>
+             </div>
+             <div className="grid gap-2">
+                 <Label id={`${prefix}-icon-label`}>图标</Label>
+                 <ToggleGroup
+                     aria-labelledby={`${prefix}-icon-label`}
+                     disabled={busy}
+                     layout="iconGrid"
+                     onValueChange={(value) => {
+                         if (
+                             RECORDING_TAG_ICONS.includes(
+                                 value as RecordingTagIcon,
+                             )
+                         ) {
+                             onIconChange(value as RecordingTagIcon);
+                         }
+                     }}
+                     size="sm"
+                     spacing={1}
+                     type="single"
+                     value={icon}
+                     variant="outline"
+                 >
+                     {RECORDING_TAG_ICONS.map((item) => (
+                         <ToggleGroupItem
+                             aria-label={item}
+                             key={item}
+                             value={item}
+                         >
+                             <RecordingTagIconGlyph icon={item} />
+                         </ToggleGroupItem>
+                     ))}
+                 </ToggleGroup>
+             </div>
+         </div>
+     );
+
+     return (
+         <>
+             <Card
+                 aria-busy={busy || undefined}
+                 className="w-full max-w-xl"
+                 data-control="recording-tag-manager"
+                 data-state={visibleError ? "error" : busy ? "saving" : "ready"}
+             >
+                 <CardHeader className="flex flex-row items-center justify-between gap-4">
+                     <CardTitle className="text-base">管理标签</CardTitle>
+                     {onClose ? (
+                         <Button
+                             aria-label="关闭标签管理"
+                             disabled={busy}
+                             onClick={onClose}
+                             size="icon-sm"
+                             type="button"
+                             variant="ghost"
+                         >
+                             <X />
+                         </Button>
+                     ) : null}
+                 </CardHeader>
+                 <CardContent className="grid gap-5">
+                     {!createDialogOpen ? renderError() : null}
+
+                     <section
+                         aria-labelledby="recording-tags-title"
+                         className="grid gap-3"
+                     >
+                         <div className="flex items-center justify-between gap-3">
+                             <Label id="recording-tags-title">
+                                 这条录音的标签
+                             </Label>
+                             <span className="text-sm text-muted-foreground">
+                                 {recording.tags.length} 个
+                             </span>
+                         </div>
+                         <div className="flex flex-wrap gap-2">
+                             {availableTags.length === 0 ? (
+                                 <p className="text-sm text-muted-foreground">
+                                     尚未创建标签
+                                 </p>
+                             ) : (
+                                 availableTags.map((tag) => {
+                                     const selected = selectedTagIds.has(tag.id);
+                                     return (
+                                         <Button
+                                             aria-pressed={selected}
+                                             data-control="recording-tag-toggle"
+                                             data-tag-id={tag.id}
+                                             disabled={busy}
+                                             key={tag.id}
+                                             onClick={() => toggleTag(tag)}
+                                             size="sm"
+                                             type="button"
+                                             variant={
+                                                 selected
+                                                     ? "secondary"
+                                                     : "outline"
+                                             }
+                                         >
+                                             {pending === "assignment" ? (
+                                                 <LoaderCircle className="animate-spin" />
+                                             ) : (
+                                                 <RecordingTagIconGlyph
+                                                     icon={tag.icon}
+                                                 />
+                                             )}
+                                             {tag.name}
+                                         </Button>
+                                     );
+                                 })
+                             )}
+                         </div>
+                     </section>
+
+                     <Separator />
+
+                     <section
+                         aria-labelledby="recording-tag-catalog-title"
+                         className="grid gap-3"
+                     >
+                         <Label id="recording-tag-catalog-title">标签目录</Label>
+                         <div className="grid gap-2">
+                             {availableTags.map((tag) =>
+                                 editingTagId === tag.id ? (
+                                     <div
+                                         className="grid gap-3 rounded-md border p-3"
+                                         key={tag.id}
+                                     >
+                                         <Input
+                                             aria-label="重命名标签"
+                                             disabled={busy}
+                                             maxLength={
+                                                 MAX_RECORDING_TAG_NAME_LENGTH
+                                             }
+                                             onChange={(event) =>
+                                                 setEditName(event.target.value)
+                                             }
+                                             value={editName}
+                                         />
+                                         {renderPicker({
+                                             color: editColor,
+                                             icon: editIcon,
+                                             onColorChange: setEditColor,
+                                             onIconChange: setEditIcon,
+                                             prefix: `edit-${tag.id}`,
+                                         })}
+                                         <div className="flex flex-wrap justify-end gap-2">
+                                             <Button
+                                                 disabled={busy}
+                                                 onClick={() =>
+                                                     setEditingTagId(null)
+                                                 }
+                                                 size="sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 取消
+                                             </Button>
+                                             <Button
+                                                 disabled={
+                                                     !editName.trim() || busy
+                                                 }
+                                                 onClick={() =>
+                                                     void updateTag(tag, {
+                                                         color: editColor,
+                                                         icon: editIcon,
+                                                         name: editName.trim(),
+                                                     })
+                                                 }
+                                                 size="sm"
+                                                 type="button"
+                                             >
+                                                 {pending ===
+                                                 `update-${tag.id}` ? (
+                                                     <LoaderCircle className="animate-spin" />
+                                                 ) : null}
+                                                 保存
+                                             </Button>
+                                         </div>
+                                     </div>
+                                 ) : (
+                                     <div
+                                         className="flex items-center justify-between gap-3 rounded-md border p-3"
+                                         key={tag.id}
+                                     >
+                                         <div className="flex min-w-0 items-center gap-2">
+                                             <RecordingTagIconGlyph
+                                                 icon={tag.icon}
+                                             />
+                                             <span className="truncate font-medium">
+                                                 {tag.name}
+                                             </span>
+                                             <span className="text-sm text-muted-foreground">
+                                                 {tag.recordingCount ?? 0} 条录音
+                                             </span>
+                                         </div>
+                                         <div className="flex shrink-0 gap-1">
+                                             <Button
+                                                 aria-label={`编辑 ${tag.name}`}
+                                                 disabled={busy}
+                                                 onClick={() => startEdit(tag)}
+                                                 size="icon-sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 <Pencil />
+                                             </Button>
+                                             <Button
+                                                 aria-label={`删除 ${tag.name}`}
+                                                 disabled={busy}
+                                                 onClick={() =>
+                                                     setDeleteTarget(tag)
+                                                 }
+                                                 size="icon-sm"
+                                                 type="button"
+                                                 variant="ghost"
+                                             >
+                                                 <Trash2 />
+                                             </Button>
+                                         </div>
+                                     </div>
+                                 ),
+                             )}
+                         </div>
+                     </section>
+                 </CardContent>
+                 <CardFooter className="border-t pt-5">
+                     <Dialog
+                         onOpenChange={(open) => {
+                             if (open) {
+                                 setDeleteTarget(null);
+                                 setOperationError(null);
+                                 setRetryAction(null);
+                                 setCreateDialogOpen(true);
+                                 return;
+                             }
+                             closeCreateDialog();
+                         }}
+                         open={createDialogOpen}
+                     >
+                         <DialogTrigger asChild>
+                             <Button
+                                 data-control="recording-tag-create"
+                                 type="button"
+                             >
+                                 <Plus />
+                                 新建标签
+                             </Button>
+                         </DialogTrigger>
+                         <DialogContent aria-describedby="recording-tag-create-description">
+                             <DialogHeader>
+                                 <DialogTitle>新建标签</DialogTitle>
+                                 <DialogDescription id="recording-tag-create-description">
+                                     创建后会自动添加到这条录音。
+                                 </DialogDescription>
+                             </DialogHeader>
+                             <form
+                                 className="grid gap-5"
+                                 onSubmit={(event) => {
+                                     event.preventDefault();
+                                     void createTag({
+                                         color: createColor,
+                                         icon: createIcon,
+                                         name: createName.trim(),
+                                     });
+                                 }}
+                             >
+                                 {renderError()}
+                                 <div className="grid gap-2">
+                                     <Label htmlFor="recording-tag-create-name">
+                                         标签名称
+                                     </Label>
+                                     <Input
+                                         autoFocus
+                                         disabled={busy}
+                                         id="recording-tag-create-name"
+                                         maxLength={
+                                             MAX_RECORDING_TAG_NAME_LENGTH
+                                         }
+                                         onChange={(event) =>
+                                             setCreateName(event.target.value)
+                                         }
+                                         placeholder="例如:待跟进"
+                                         value={createName}
+                                     />
+                                 </div>
+                                 {renderPicker({
+                                     color: createColor,
+                                     icon: createIcon,
+                                     onColorChange: setCreateColor,
+                                     onIconChange: setCreateIcon,
+                                     prefix: "create",
+                                 })}
+                                 <DialogFooter>
+                                     <Button
+                                         disabled={busy}
+                                         onClick={closeCreateDialog}
+                                         type="button"
+                                         variant="outline"
+                                     >
+                                         取消
+                                     </Button>
+                                     <Button
+                                         disabled={!createName.trim() || busy}
+                                         type="submit"
+                                     >
+                                         {pending === "create" ? (
+                                             <LoaderCircle className="animate-spin" />
+                                         ) : (
+                                             <Plus />
+                                         )}
+                                         创建标签
+                                     </Button>
+                                 </DialogFooter>
+                             </form>
+                         </DialogContent>
+                     </Dialog>
+                 </CardFooter>
+             </Card>
+             <Dialog
+                 open={Boolean(deleteTarget)}
+                 onOpenChange={(open) => {
+                     if (!open) {
+                         setDeleteTarget(null);
+                     }
+                 }}
+             >
+                 <DialogContent>
+                     <DialogHeader>
+                         <DialogTitle>删除标签</DialogTitle>
+                         <DialogDescription>
+                             {deleteTarget
+                                 ? `“${deleteTarget.name}”会从所有录音中移除。`
+                                 : ""}
+                         </DialogDescription>
+                     </DialogHeader>
+                     <DialogFooter>
+                         <Button
+                             onClick={() => setDeleteTarget(null)}
+                             type="button"
+                             variant="outline"
+                         >
+                             取消
+                         </Button>
+                         <Button
+                             disabled={busy}
+                             onClick={confirmDelete}
+                             type="button"
+                             variant="destructive"
+                         >
+                             删除标签
+                         </Button>
+                     </DialogFooter>
+                 </DialogContent>
+             </Dialog>
+         </>
+     );
+ }
+

 ❯ src/tests/recording-detail-copy-ui-regression.test.ts:3678:28

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

本 PR 将 recordings 的转写(transcription)区域与骨架屏(skeletons)对齐到共享的 shadcn UI primitives,并将原先偏“源码形状守卫”的回归测试调整为基于实际渲染语义(data-slot / aria / data-control / data-state)的覆盖,从而更稳定地验证 UI 状态与交互控件。

Changes:

  • 更新 TranscriptionSection 的错误态与整体布局:使用 shadcn AlertDescription、新增“重试/收起错误”控制,并统一 badge 变体写法与 spacing。
  • 简化并重排 transcription-skeletons 的骨架结构与样式(grid/gap/尺寸),更贴近当前 Card/Skeleton primitives。
  • 将两套 UI 回归测试改为 SSR 渲染组件并断言语义标记(data-control、data-slot、aria-*),同时覆盖 job display state 映射与 active job 判定。

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/features/recordings/components/transcription-section.tsx 调整转写面板结构与错误态交互(retry/dismiss),并对齐 shadcn primitives 的布局与语义属性。
src/features/recordings/components/transcription-skeletons.tsx 重构转写相关 skeleton 结构与样式,简化占位实现并对齐 Card/Skeleton 组件用法。
src/tests/recording-detail-copy-ui-regression.test.ts 从源码字符串断言切换为 SSR 渲染断言,验证录音详情转写区在 loaded/failed/empty/loading 等状态下的语义输出。
src/tests/full-ui-replacement-regression.test.ts 同步采用 SSR 渲染断言方式,增强对转写区 primitives/语义标记与 job display state 的回归覆盖。

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to +23
function renderRecordingTranscription(
props: React.ComponentProps<typeof TranscriptionSection>,
) {
return renderToStaticMarkup(
React.createElement(
Comment on lines +38 to +42
function renderRecordingTranscription(
props: React.ComponentProps<typeof TranscriptionSection>,
) {
return renderToStaticMarkup(
React.createElement(
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants