Files
Cyrene_v2/frontend/web/src/store/useAppStore.ts
T
Aska 4aab0f501a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(web): 前端收敛为「共享消息流 + 按消息 mode 渲染」,并补全 emoji→MD3 图标
架构变更(主人要求,design/07 §1/§2 已改定稿):把「9 个行为模式 = 9 个独立页面」改为
「一个共享页面 + 每条消息绑 mode,消息框按对应 mode 渲染」。
- ChatMessage 增 mode?(types.ts);useAppStore addUserMessage/beginAssistant 带 mode;
  useSendChat.send(text, mode) / chatStream body 带 mode(通知后端按 mode 回复)。
- MessageBubble 按 message.mode 分发到 components/messages/ 各形态渲染器
  (chat 气泡 / code 等宽代码块 / home 设备卡 / docs 文档卡 / work 任务卡 / vision 视觉卡)。
- ChatInput 顶部加「行为模式」chip 切换下一条消息的 mode(默认 chat);输入框上方显示当前模式。
- ModeRouter 收敛为只渲染 ChatShell;删除 9 个旧独立 mode 页面;Nav/TopBar 的 mode 改为
  「输入模式」语义;settings 归管理面板(design/10)。

同时补全上一轮 emoji→MD3 图标(约 23 文件):新增 Icon.tsx(MD3 SVG、IconName、零依赖、
离线优先),全部界面 emoji 替换为 <Icon name=... />;正文说明箭头保留。

npm run build 绿(tsc 0 错,79→71 模块,JS 284KB/gzip 86KB)。
2026-09-06 13:32:44 +08:00

223 lines
6.2 KiB
TypeScript

import { create } from "zustand";
import { persist } from "zustand/middleware";
import type {
AgentInfo,
BackthinkItem,
ChatMessage,
FlowGraph,
Mode,
ToolUsed,
WsStatus,
} from "../lib/types";
import { now, uid } from "../lib/util";
interface ChatContext {
topic?: string;
lastAgentMode?: string;
notes?: string[];
}
export interface AppStore {
sessionId: string;
wsStatus: WsStatus;
messages: ChatMessage[];
mood: string | null;
context: ChatContext;
agents: AgentInfo[];
backthink: BackthinkItem[];
flowGraph: FlowGraph | null;
deviceUpdates: string[];
setWsStatus: (status: WsStatus) => void;
setSessionId: (id: string) => void;
setContext: (patch: Partial<ChatContext>) => void;
setMood: (mood: string | null) => void;
addUserMessage: (text: string, mode?: Mode) => void;
beginAssistant: (mode?: Mode) => string;
appendChunk: (delta: string) => void;
endStream: () => void;
setThinking: (text: string) => void;
addTool: (tool: ToolUsed) => void;
setAgents: (agents: AgentInfo[]) => void;
upsertAgent: (agent: AgentInfo) => void;
addBackthink: (item: BackthinkItem) => void;
revealBackthink: (id: string, conclusion: string) => void;
markBackthinkPushed: (id: string) => void;
setFlowGraph: (graph: FlowGraph) => void;
addDeviceUpdate: (text: string) => void;
taskLogs: Record<string, string>;
appendTaskLog: (taskId: string, text: string) => void;
clearTaskLog: (taskId: string) => void;
resetChat: () => void;
}
export const useAppStore = create<AppStore>()(
persist(
(set, get) => ({
sessionId: "web",
wsStatus: "idle",
messages: [],
mood: null,
context: {},
agents: [
{ id: "main", name: "昔涟", status: "idle", role: "owner" },
],
backthink: [],
flowGraph: null,
deviceUpdates: [],
taskLogs: {},
setWsStatus: (status) => set({ wsStatus: status }),
setSessionId: (id) => set({ sessionId: id }),
setContext: (patch) =>
set((s) => ({ context: { ...s.context, ...patch } })),
setMood: (mood) => set({ mood }),
addUserMessage: (text, mode) =>
set((s) => ({
messages: [
...s.messages,
{
id: uid(),
role: "user",
content: text,
mode,
createdAt: now(),
},
],
})),
beginAssistant: (mode) => {
const id = uid();
set((s) => ({
messages: [
...s.messages,
{
id,
role: "assistant",
content: "",
mode,
streaming: true,
tools: [],
createdAt: now(),
},
],
}));
return id;
},
appendChunk: (delta) =>
set((s) => {
const messages = s.messages.slice();
const last = messages[messages.length - 1];
if (!last || last.role !== "assistant") return s;
messages[messages.length - 1] = { ...last, content: last.content + delta };
return { messages };
}),
endStream: () =>
set((s) => {
const messages = s.messages.slice();
const last = messages[messages.length - 1];
if (!last || last.role !== "assistant") return s;
messages[messages.length - 1] = { ...last, streaming: false };
return { messages };
}),
setThinking: (text) =>
set((s) => {
const messages = s.messages.slice();
const last = messages[messages.length - 1];
if (!last || last.role !== "assistant") return s;
messages[messages.length - 1] = { ...last, thinking: text };
return { messages };
}),
addTool: (tool) =>
set((s) => {
const messages = s.messages.slice();
const last = messages[messages.length - 1];
if (!last || last.role !== "assistant") return s;
messages[messages.length - 1] = {
...last,
tools: [...(last.tools ?? []), tool],
};
return { messages };
}),
setAgents: (agents) => set({ agents }),
upsertAgent: (agent) =>
set((s) => {
const idx = s.agents.findIndex((a) => a.id === agent.id);
if (idx === -1) return { agents: [...s.agents, agent] };
const agents = s.agents.slice();
agents[idx] = { ...agents[idx], ...agent };
return { agents };
}),
addBackthink: (item) =>
set((s) => ({ backthink: [item, ...s.backthink] })),
revealBackthink: (id, conclusion) =>
set((s) => ({
backthink: s.backthink.map((b) =>
b.id === id ? { ...b, conclusion, shouldNotify: true } : b
),
})),
markBackthinkPushed: (id) =>
set((s) => ({
backthink: s.backthink.map((b) =>
b.id === id ? { ...b, pushed: true } : b
),
})),
setFlowGraph: (graph) => set({ flowGraph: graph }),
addDeviceUpdate: (text) =>
set((s) => ({ deviceUpdates: [text, ...s.deviceUpdates].slice(0, 50) })),
appendTaskLog: (taskId, text) =>
set((s) => ({
taskLogs: {
...s.taskLogs,
[taskId]: (s.taskLogs[taskId] ?? "") + text,
},
})),
clearTaskLog: (taskId) =>
set((s) => {
if (!(taskId in s.taskLogs)) return s;
const taskLogs = { ...s.taskLogs };
delete taskLogs[taskId];
return { taskLogs };
}),
resetChat: () =>
set((s) => ({
messages: [],
mood: null,
flowGraph: null,
backthink: s.backthink,
})),
}),
{
name: "cyrene.app",
version: 1,
partialize: (s) => ({
sessionId: s.sessionId,
messages: s.messages,
mood: s.mood,
context: s.context,
backthink: s.backthink,
}),
}
)
);
/** Convenience selector for the last streaming assistant message id. */
export function lastStreamingMessage(s: AppStore): ChatMessage | undefined {
const last = s.messages[s.messages.length - 1];
return last?.role === "assistant" ? last : undefined;
}