diff --git a/web-app/src/store/index.ts b/web-app/src/store/index.ts new file mode 100644 index 0000000..eafca40 --- /dev/null +++ b/web-app/src/store/index.ts @@ -0,0 +1,286 @@ +/** + * MVU (Model-View-Update) 状态管理 Store + * 基于 Zustand 实现的轻量级状态管理 + */ + +import { create } from 'zustand' +import { devtools, persist } from 'zustand/middleware' + +// ============= Model (状态模型) ============= + +export interface User { + id: number + username: string + email: string + avatar?: string +} + +export interface Character { + id: number + name: string + avatar: string + description: string +} + +export interface Conversation { + id: number + characterId: number + title: string + lastMessage?: string + updatedAt: string +} + +export interface Message { + id: number + role: 'user' | 'assistant' + content: string + timestamp: string +} + +export interface AppState { + // 用户状态 + user: User | null + isAuthenticated: boolean + + // 当前选中的实体 + currentCharacter: Character | null + currentConversation: Conversation | null + + // 消息列表 + messages: Message[] + + // UI 状态 + sidebarOpen: boolean + loading: boolean + error: string | null + + // 变量系统 + variables: Record +} + +// ============= Update (状态更新) ============= + +export interface AppActions { + // 用户操作 + setUser: (user: User | null) => void + login: (user: User) => void + logout: () => void + + // 角色操作 + setCurrentCharacter: (character: Character | null) => void + + // 对话操作 + setCurrentConversation: (conversation: Conversation | null) => void + + // 消息操作 + setMessages: (messages: Message[]) => void + addMessage: (message: Message) => void + updateMessage: (id: number, content: string) => void + deleteMessage: (id: number) => void + clearMessages: () => void + + // UI 操作 + toggleSidebar: () => void + setSidebarOpen: (open: boolean) => void + setLoading: (loading: boolean) => void + setError: (error: string | null) => void + + // 变量系统操作 + setVariable: (key: string, value: string) => void + getVariable: (key: string) => string | undefined + deleteVariable: (key: string) => void + clearVariables: () => void + + // 批量更新(用于复杂操作) + batchUpdate: (updater: (state: AppState) => Partial) => void +} + +// ============= Store (状态容器) ============= + +const initialState: AppState = { + user: null, + isAuthenticated: false, + currentCharacter: null, + currentConversation: null, + messages: [], + sidebarOpen: true, + loading: false, + error: null, + variables: { + user: '', + char: '', + }, +} + +export const useAppStore = create()( + devtools( + persist( + (set, get) => ({ + ...initialState, + + // 用户操作 + setUser: (user) => set({ user, isAuthenticated: !!user }), + + login: (user) => set({ + user, + isAuthenticated: true, + variables: { + ...get().variables, + user: user.username, + } + }), + + logout: () => set({ + user: null, + isAuthenticated: false, + currentCharacter: null, + currentConversation: null, + messages: [], + variables: { + user: '', + char: '', + } + }), + + // 角色操作 + setCurrentCharacter: (character) => set({ + currentCharacter: character, + variables: { + ...get().variables, + char: character?.name || '', + } + }), + + // 对话操作 + setCurrentConversation: (conversation) => set({ currentConversation: conversation }), + + // 消息操作 + setMessages: (messages) => set({ messages }), + + addMessage: (message) => set((state) => ({ + messages: [...state.messages, message] + })), + + updateMessage: (id, content) => set((state) => ({ + messages: state.messages.map(msg => + msg.id === id ? { ...msg, content } : msg + ) + })), + + deleteMessage: (id) => set((state) => ({ + messages: state.messages.filter(msg => msg.id !== id) + })), + + clearMessages: () => set({ messages: [] }), + + // UI 操作 + toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), + setSidebarOpen: (open) => set({ sidebarOpen: open }), + setLoading: (loading) => set({ loading }), + setError: (error) => set({ error }), + + // 变量系统操作 + setVariable: (key, value) => set((state) => ({ + variables: { ...state.variables, [key]: value } + })), + + getVariable: (key) => get().variables[key], + + deleteVariable: (key) => set((state) => { + const { [key]: _, ...rest } = state.variables + return { variables: rest } + }), + + clearVariables: () => set({ + variables: { user: get().user?.username || '', char: get().currentCharacter?.name || '' } + }), + + // 批量更新 + batchUpdate: (updater) => set((state) => updater(state)), + }), + { + name: 'app-storage', + partialize: (state) => ({ + user: state.user, + isAuthenticated: state.isAuthenticated, + sidebarOpen: state.sidebarOpen, + variables: state.variables, + }), + } + ) + ) +) + +// ============= Selectors (状态选择器) ============= + +export const selectUser = (state: AppState & AppActions) => state.user +export const selectIsAuthenticated = (state: AppState & AppActions) => state.isAuthenticated +export const selectCurrentCharacter = (state: AppState & AppActions) => state.currentCharacter +export const selectCurrentConversation = (state: AppState & AppActions) => state.currentConversation +export const selectMessages = (state: AppState & AppActions) => state.messages +export const selectSidebarOpen = (state: AppState & AppActions) => state.sidebarOpen +export const selectLoading = (state: AppState & AppActions) => state.loading +export const selectError = (state: AppState & AppActions) => state.error +export const selectVariables = (state: AppState & AppActions) => state.variables + +// ============= 变量替换工具函数 ============= + +/** + * 替换文本中的变量 + * 支持的变量格式:{{user}}, {{char}}, {{random}}, {{time}}, {{date}} 等 + */ +export function substituteVariables(text: string, customVars?: Record): string { + const state = useAppStore.getState() + const variables = { ...state.variables, ...customVars } + + let result = text + + // 替换用户定义的变量 + Object.entries(variables).forEach(([key, value]) => { + const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'gi') + result = result.replace(regex, value) + }) + + // 替换时间变量 + const now = new Date() + result = result.replace(/\{\{time\}\}/gi, now.toLocaleTimeString('en-US', { hour12: false })) + result = result.replace(/\{\{time_12h\}\}/gi, now.toLocaleTimeString('en-US', { hour12: true })) + result = result.replace(/\{\{date\}\}/gi, now.toLocaleDateString('en-CA')) // YYYY-MM-DD + result = result.replace(/\{\{date_short\}\}/gi, now.toLocaleDateString('en-US')) // MM/DD/YYYY + result = result.replace(/\{\{datetime\}\}/gi, now.toLocaleString()) + result = result.replace(/\{\{timestamp\}\}/gi, now.getTime().toString()) + result = result.replace(/\{\{weekday\}\}/gi, now.toLocaleDateString('en-US', { weekday: 'long' })) + result = result.replace(/\{\{month\}\}/gi, now.toLocaleDateString('en-US', { month: 'long' })) + result = result.replace(/\{\{year\}\}/gi, now.getFullYear().toString()) + + // 替换随机数变量 + result = result.replace(/\{\{random:(\d+)-(\d+)\}\}/gi, (_, min, max) => { + const minNum = parseInt(min) + const maxNum = parseInt(max) + return Math.floor(Math.random() * (maxNum - minNum + 1) + minNum).toString() + }) + result = result.replace(/\{\{random\}\}/gi, () => Math.floor(Math.random() * 100).toString()) + + // 随机选择 {{pick:option1|option2|option3}} + result = result.replace(/\{\{pick:([^}]+)\}\}/gi, (_, options) => { + const optionList = options.split('|') + return optionList[Math.floor(Math.random() * optionList.length)] + }) + + // 替换特殊字符 + result = result.replace(/\{\{newline\}\}/gi, '\n') + result = result.replace(/\{\{tab\}\}/gi, '\t') + result = result.replace(/\{\{space\}\}/gi, ' ') + result = result.replace(/\{\{empty\}\}/gi, '') + + return result +} + +/** + * 从文本中提取变量 + */ +export function extractVariables(text: string): string[] { + const regex = /\{\{([^}]+)\}\}/g + const matches = text.matchAll(regex) + return Array.from(matches, m => m[1]) +}