diff --git a/frontend/web/src/components/chat.tsx b/frontend/web/src/components/chat.tsx index db822ea..d2c0f23 100644 --- a/frontend/web/src/components/chat.tsx +++ b/frontend/web/src/components/chat.tsx @@ -8,7 +8,9 @@ import { WakePath } from "./status"; import { modeBody } from "./messages"; import { renderContent } from "../lib/renderContent"; -/** Collapsible inline chain-of-thought block. */ +/** Collapsible inline chain-of-thought block. A Miuix disclosure row: icon chip + * + label, spinner when still thinking, and a single-line muted preview of the + * reasoning while collapsed so the block reads at a glance. */ export function ThinkingBlock({ text, streaming, @@ -23,26 +25,39 @@ export function ThinkingBlock({
+{text}+ ) : !open && text ? ( +{text}
) : null}
+{tool.detail}) : null} @@ -76,6 +92,30 @@ export function ToolCard({ tool }: { tool: ToolUsed }): React.ReactNode { ); } +type ToolStatus = "ok" | "err" | "neutral"; + +function statusBadge(status: ToolStatus): string { + switch (status) { + case "ok": return "bg-[var(--mu-ok)]/15 text-[var(--mu-ok)]"; + case "err": return "bg-[var(--mu-err)]/15 text-[var(--mu-err)]"; + default: return "bg-[var(--mu-surface-4)] text-[var(--mu-text-mid)]"; + } +} +function statusIcon(status: ToolStatus): IconName { + switch (status) { + case "ok": return "check"; + case "err": return "close"; + default: return "refresh"; + } +} +function statusLabel(status: ToolStatus): string { + switch (status) { + case "ok": return "成功"; + case "err": return "失败"; + default: return "运行中"; + } +} + /** A single message bubble. Per design/07 §1 the bubble shape is driven by the * message's own `mode`: the same role renders differently under different modes, * and a single session may mix shapes. Falls back to `chat` when mode is absent @@ -108,11 +148,14 @@ function splitParagraphs(content: string): string[] { * with ``/` ` rendered as muted italic via renderContent. */ function ChatBody({ text, streaming }: { text: string; streaming?: boolean }): React.ReactNode { return ( - +{text ? ( {renderContent(text)} ) : streaming ? ( - 生成中… + +); @@ -136,7 +179,7 @@ function AssistantParaBubble({ return (+ 生成中… + ) : null} {full ? ( -+
) : ( )} @@ -207,8 +250,10 @@ export function SplitChatBubble({ return
; } + // Group the paragraphs of one reply into a tighter column so intra-reply + // segments read as one bubble while separate replies keep the 16px list gap. return ( - <> + {segments.map((seg, i) => message.role === "user" ? (); } @@ -265,7 +310,7 @@ function AssistantBubble({ const currentPersona = useAppStore((s) => s.currentPersona); return (@@ -222,7 +267,7 @@ export function SplitChatBubble({ /> ), )} - > + -+
![]()
{message.mood ? ( diff --git a/frontend/web/src/lib/persona.ts b/frontend/web/src/lib/persona.ts index 14827c6..35509bc 100644 --- a/frontend/web/src/lib/persona.ts +++ b/frontend/web/src/lib/persona.ts @@ -8,3 +8,16 @@ const DEFAULT_AVATAR = "/images/User_Avatar/Default_Avatar.png"; export function personaAvatar(p: PersonaRecord | null | undefined): string { return p?.avatar || DEFAULT_AVATAR; } + +/** Orientation for a persona's chat wallpaper. */ +export type Orientation = "landscape" | "portrait"; + +/** Resolve the persona's own chat wallpaper for the given orientation. Returns + * undefined when the persona has none (or is null) so callers keep the plain + * Miuix surface. Never another role's image. */ +export function personaChatBackground( + p: PersonaRecord | null | undefined, + orientation: Orientation +): string | undefined { + return p?.chatBackground?.[orientation]; +} diff --git a/frontend/web/src/modes/ChatShell.tsx b/frontend/web/src/modes/ChatShell.tsx index 50b410a..356f246 100644 --- a/frontend/web/src/modes/ChatShell.tsx +++ b/frontend/web/src/modes/ChatShell.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useAppStore } from "../store/useAppStore"; import { useModeStore } from "../store/useModeStore"; import { useSendChat } from "../lib/useChat"; @@ -8,6 +8,7 @@ import { ChatInput } from "../components/ChatInput"; import { EmptyState } from "../components/primitives"; import { Icon } from "../components/Icon"; import { SessionStatusHint } from "../components/status"; +import { personaChatBackground, type Orientation } from "../lib/persona"; /** * The single shared page (design/07 §1). All behavior modes live in ONE message @@ -17,9 +18,27 @@ import { SessionStatusHint } from "../components/status"; */ export function ChatShell(): React.ReactNode { const messages = useAppStore((s) => s.messages); + const currentPersona = useAppStore((s) => s.currentPersona); const { send, sending } = useSendChat(); const bottomRef = useRef (null); + // Chat wallpaper follows the current orientation (landscape when wide, else + // portrait) and re-evaluates on window resize. + const [orientation, setOrientation] = useState (() => + window.innerWidth > window.innerHeight ? "landscape" : "portrait" + ); + + useEffect(() => { + const onResize = () => + setOrientation(window.innerWidth > window.innerHeight ? "landscape" : "portrait"); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + // The persona's own wallpaper for the current orientation; undefined keeps the + // plain Miuix surface (never another role's image). + const chatBackground = personaChatBackground(currentPersona, orientation); + // The latest assistant message is the one being streamed right now; only it // carries the live 唤醒路径 node sequence (WakePath), so the capacity state // reflects the current turn rather than a collapsed historical message. @@ -36,29 +55,42 @@ export function ChatShell(): React.ReactNode { return ( ---- {messages.length === 0 ? ( - } - title="开始与昔涟对话" - hint="模式只改变消息框的呈现方式,会话上下文与记忆保持不变" + + {chatBackground ? ( +++ ) : null} +- ) : ( - messages.map((m) => ( -
+ +++ {messages.length === 0 ? ( + } + title="开始与昔涟对话" + hint="模式只改变消息框的呈现方式,会话上下文与记忆保持不变" /> - )) - )} - + ) : ( + messages.map((m) => ( + + )) + )} + +