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 `` / `` 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 (
{items.map((item, i) => (
*{item.type === "action" ? "动作" : "旁白"}:{item.text}*
))}
); } /** 零气泡时是否还有「回复外壳」要出(见 `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 (
{text ? ( {renderContent(text)} ) : ( 生成中… )}
); }