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,60 @@
|
||||
import type { ChatMessage } from "../../lib/types";
|
||||
import { Icon } from "../Icon";
|
||||
import { renderContent } from "../../lib/renderContent";
|
||||
import type { ContentPart } from "../../lib/parseContent";
|
||||
|
||||
/** Small muted attachment note for a single `<action>` / `<narration>` block.
|
||||
* Rendered as a grey italic micro-line OUTSIDE the bubble (anchored to the
|
||||
* message's head/tail by `splitBubble`) — never inside the bubble body.
|
||||
* `align` mirrors the bubble column: `left` indents under the assistant avatar
|
||||
* column, `right` lines the note up with a right-aligned user bubble. */
|
||||
export function BubbleNotes({
|
||||
items,
|
||||
align = "left",
|
||||
}: {
|
||||
items: ContentPart[];
|
||||
align?: "left" | "right";
|
||||
}): React.ReactNode {
|
||||
if (items.length === 0) return null;
|
||||
const container =
|
||||
align === "right"
|
||||
? "flex flex-col items-end gap-0.5"
|
||||
: "flex flex-col items-start gap-0.5 pl-10";
|
||||
return (
|
||||
<div className={container}>
|
||||
{items.map((item, i) => (
|
||||
<div key={i} className="text-[10px] italic leading-tight text-[var(--mu-text-lo)]">
|
||||
*{item.type === "action" ? "动作" : "旁白"}:{item.text}*
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 零气泡时是否还有「回复外壳」要出(见 `SplitChatBubble`):思维链 / 工具卡 / 心情是
|
||||
* 用户可见内容,流式中的「生成中…」是必要的等待反馈 —— 有其一就补一行外壳;纯注记回复
|
||||
* (这几样都没有)保持「只剩注记本身」的观感,与改动前一致。 */
|
||||
export function hasBubbleChrome(message: ChatMessage): boolean {
|
||||
return Boolean(message.streaming || message.thinking || message.mood || message.tools?.length);
|
||||
}
|
||||
|
||||
/** Chat-mode body: a Miuix surface shell around a whitespace-preserving span.
|
||||
* `text` is already the annotation-stripped body paragraph from `splitBubble`,
|
||||
* so it renders as plain text only — action/narration notes live outside.
|
||||
* Empty text with no stream ⇒ **不画外壳**(零气泡回复里的那行「注记外壳」不因为是空的
|
||||
* 就变成一个空气泡)。 */
|
||||
export function ChatBody({ text, streaming }: { text: string; streaming?: boolean }): React.ReactNode {
|
||||
if (!text && !streaming) return null;
|
||||
return (
|
||||
<div className="liquid-glass-soft rounded-[var(--mu-radius-lg)] px-4 py-2.5 text-sm text-[var(--mu-text-hi)] shadow-[var(--mu-shadow-1)]">
|
||||
{text ? (
|
||||
<span className="whitespace-pre-wrap">{renderContent(text)}</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 text-[var(--mu-text-lo)]">
|
||||
<Icon name="refresh" size={12} className="animate-spin" />
|
||||
生成中…
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user