From 4ad1e465da899d98c110af611d89cb58fb78d4c9 Mon Sep 17 00:00:00 2001 From: Aska Date: Mon, 7 Sep 2026 02:57:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=81=8A=E5=A4=A9=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E6=B8=B2=E6=9F=93=20+=20=E6=80=9D=E7=BB=B4=E9=93=BE/?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=8D=A1=E5=AF=8C=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - persona.ts: 新增 personaChatBackground(p, orientation),只取本角色对应方向的图 - ChatShell.tsx: 按横/竖屏渲染角色聊天背景(衬底+scrim),null/无图回退纯 Miuix 面 - chat.tsx: 思维链块加图标 chip+spinner+收起预览;工具卡加状态色 chip+ok/fail Tag - 红线: 绝不 fallback 昔涟/其它角色;TemplateCard detail 折叠判定等行为不变 --- frontend/web/src/components/chat.tsx | 99 ++++++++++++++++++++-------- frontend/web/src/lib/persona.ts | 13 ++++ frontend/web/src/modes/ChatShell.tsx | 76 ++++++++++++++------- 3 files changed, 139 insertions(+), 49 deletions(-) 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({
{open && text ? ( -
+        
           {text}
         
+ ) : !open && text ? ( +

{text}

) : null}
); } -/** Tool echo card (工具回显). */ +/** Tool echo card (工具回显). A status chip + tool name + muted one-line summary + * on the left, a color-coded result tag on the right, and collapsible detail. */ export function ToolCard({ tool }: { tool: ToolUsed }): React.ReactNode { const [open, setOpen] = useState(false); const status = tool.ok === undefined ? "neutral" : tool.ok ? "ok" : "err"; @@ -50,25 +65,26 @@ export function ToolCard({ tool }: { tool: ToolUsed }): React.ReactNode {
{(open || tool.detail) && tool.detail ? ( -
+        
           {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 ? ( - 生成中… + + + 生成中… + ) : null}
); @@ -136,7 +179,7 @@ function AssistantParaBubble({ return (
{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" ? ( @@ -222,7 +267,7 @@ export function SplitChatBubble({ /> ), )} - +
); } @@ -265,7 +310,7 @@ function AssistantBubble({ const currentPersona = useAppStore((s) => s.currentPersona); return (
- +
{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 ? ( +
+ - ) : ( - messages.map((m) => ( - +
+ ) : null} +
+
+ + {messages.length === 0 ? ( + } + title="开始与昔涟对话" + hint="模式只改变消息框的呈现方式,会话上下文与记忆保持不变" /> - )) - )} -
+ ) : ( + messages.map((m) => ( + + )) + )} +
+