Files
Cyrene_v2/frontend/web/src/components/chat/labels.tsx
T
AskaEth 811e463cea
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
refactor(web): chat.tsx 551 行按职责拆成 5 片(守 500 行铁律,零行为变化)
- chat/index.tsx 272 行(编排层:SplitChatBubble + MessageBubble + 各气泡)
- chat/toolviz.tsx 112(工具卡 / 思维链可视件)
- chat/labels.tsx 70(元数据小字)
- chat/chrome.tsx 60(外壳判定与注记)
- chat/split.ts 51(纯函数:br 分隔与段落切分,最易测)
- 旧 chat.tsx 删除;调用方一行未动(沿用既有目录索引解析,与 messages/ 同一模式)

纯搬家证据:把旧文件与新 5 文件做行多重集 diff,差异只有 export 前缀、
一行 re-export(ThinkingBlock / ToolCard)与 import 重写 —— 没有一行
JSX / 样式类名 / 逻辑变化;测试文件一字未改。
对外导出名与形状逐字不变(仍是 ThinkingBlock / ToolCard / MessageBubble / SplitChatBubble)。

验收:tsc 0、render 189 checks、bubble 27 checks、四个邻接套件全绿、
vite build 294 模块;变异(拆后去掉 br 空段过滤 => ⑦ 红)证明测试仍在真测那些逻辑。
2026-10-07 18:56:30 +08:00

71 lines
3.0 KiB
TypeScript

import type { ChatMessage, PersonaRecord } from "../../lib/types";
import { useAppStore } from "../../store/useAppStore";
import { useCurrentPersona } from "../../store/personaSelectors";
import { resolveMessagePersona } from "../../lib/persona";
import { DeliveryChip, TurnTag } from "../messages/TurnMeta";
/** Small muted source label for a persisted message (design/16 §4.5): shows the
* concrete client the message came from, e.g. "来自 web:chrome:a1b2c3". Rendered
* only when a deviceId is present, so legacy/unlabelled messages stay clean. The
* Web client defaults to this label; other clients decide themselves. */
export function SourceLabel({ deviceId, role }: { deviceId?: string; role?: string }): React.ReactNode {
if (!deviceId) return null;
// A user message is received FROM a client; an assistant reply goes TO the
// client that prompted it. Semantic direction differs — "来自" vs "发送至".
const dir = role === "assistant" ? "发送至" : "来自";
return (
<div className="text-[10px] leading-tight text-[var(--mu-text-lo)]">
{dir} {deviceId}
</div>
);
}
/** Small sender label for a third-party message (design/16 §4.6): shows the
* sender's display name, e.g. "第三方 · 张三". Blank for the owner's private chat
* (no senderName), so owner bubbles stay unlabelled. */
export function SenderLabel({ name }: { name?: string }): React.ReactNode {
if (!name) return null;
return (
<div className="text-[10px] leading-tight text-[var(--mu-text-lo)]">
第三方 · {name}
</div>
);
}
/** The bubble-side binding of `lib/persona.ts#resolveMessagePersona` (design/22
* §7-5): which role spoke this message, and whether it differs from the role the
* session speaks as now. */
export function useSpokenPersona(personaId?: string): {
persona: PersonaRecord | null;
switched: boolean;
} {
const personas = useAppStore((s) => s.personas);
const active = useCurrentPersona();
return resolveMessagePersona(personaId, personas, active);
}
/** Muted "角色 · X" line for a bubble whose authoring role differs from the role
* the session speaks as now. */
export function PersonaLabel({ name }: { name?: string }): React.ReactNode {
if (!name) return null;
return (
<div className="text-[10px] leading-tight text-[var(--mu-text-lo)]">角色 · {name}</div>
);
}
/** 用户气泡下面那行小字:回合小标 + 投递态角标 + 来源。两个用户气泡(单块 / 分段)
* 共用一处,避免「四份渲染各写一遍、日后漂移」。 */
export function UserMeta({ message }: { message: ChatMessage }): React.ReactNode {
return (
<div className="flex flex-wrap items-center justify-end gap-1.5">
<TurnTag turnId={message.turnId} align="right" />
<DeliveryChip
delivery={message.delivery}
steerStep={message.steerStep}
rejectReason={message.rejectReason}
/>
<SourceLabel deviceId={message.deviceId} role={message.role} />
</div>
);
}