- 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 空段过滤 => ⑦ 红)证明测试仍在真测那些逻辑。
61 lines
2.6 KiB
TypeScript
61 lines
2.6 KiB
TypeScript
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>
|
||
);
|
||
}
|