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 空段过滤 => ⑦ 红)证明测试仍在真测那些逻辑。
This commit is contained in:
1 parent
789e3af74e
commit
811e463cea
6 files changed
+565
-551
No files matched your search
@@ -0,0 +1,70 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user