diff --git a/frontend-web/src/contexts/ChatContext.tsx b/frontend-web/src/contexts/ChatContext.tsx new file mode 100644 index 00000000..67682896 --- /dev/null +++ b/frontend-web/src/contexts/ChatContext.tsx @@ -0,0 +1,81 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; + +export interface ChatMessage { + id: string; + role: 'user' | 'assistant'; + content: string; + timestamp: number; + type?: 'general' | 'agent' | 'compare'; + agentName?: string; +} + +interface ChatContextType { + messages: ChatMessage[]; + addMessage: (role: 'user' | 'assistant', content: string, type?: 'general' | 'agent' | 'compare', agentName?: string) => void; + clearMessages: () => void; + deleteMessage: (id: string) => void; +} + +const ChatContext = createContext(undefined); + +export function ChatProvider({ children }: { children: ReactNode }) { + const [messages, setMessages] = useState(() => { + try { + const stored = localStorage.getItem('qa-chat-history'); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } + }); + + const saveToStorage = useCallback((newMessages: ChatMessage[]) => { + try { + localStorage.setItem('qa-chat-history', JSON.stringify(newMessages)); + } catch (error) { + console.error('Failed to save chat history:', error); + } + }, []); + + const addMessage = useCallback((role: 'user' | 'assistant', content: string, type?: 'general' | 'agent' | 'compare', agentName?: string) => { + const newMessage: ChatMessage = { + id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + role, + content, + timestamp: Date.now(), + type, + agentName, + }; + setMessages((prev) => { + const updated = [...prev, newMessage]; + saveToStorage(updated); + return updated; + }); + }, [saveToStorage]); + + const clearMessages = useCallback(() => { + setMessages([]); + localStorage.removeItem('qa-chat-history'); + }, []); + + const deleteMessage = useCallback((id: string) => { + setMessages((prev) => { + const updated = prev.filter((msg) => msg.id !== id); + saveToStorage(updated); + return updated; + }); + }, [saveToStorage]); + + return ( + + {children} + + ); +} + +export function useChatContext() { + const context = useContext(ChatContext); + if (context === undefined) { + throw new Error('useChatContext must be used within a ChatProvider'); + } + return context; +} \ No newline at end of file diff --git a/frontend-web/src/contexts/ThemeContext.tsx b/frontend-web/src/contexts/ThemeContext.tsx new file mode 100644 index 00000000..c6ce37b8 --- /dev/null +++ b/frontend-web/src/contexts/ThemeContext.tsx @@ -0,0 +1,145 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; + +// 主题类型 +export type ThemeType = 'dark' | 'light'; + +// 主题配置 +export interface ThemeConfig { + type: ThemeType; + name: string; + colors: { + background: string; + backgroundSecondary: string; + text: string; + textSecondary: string; + primary: string; + primaryHover: string; + border: string; + accent: string; + success: string; + warning: string; + error: string; + card: string; + cardHover: string; + input: string; + inputBorder: string; + overlay: string; + }; +} + +// 主题配置 +const themes: Record = { + dark: { + type: 'dark', + name: '深色主题', + colors: { + background: 'slate-900', + backgroundSecondary: 'slate-800', + text: 'white', + textSecondary: 'slate-400', + primary: 'blue-500', + primaryHover: 'blue-400', + border: 'blue-800', + accent: 'cyan-400', + success: 'emerald-500', + warning: 'amber-500', + error: 'rose-500', + card: 'slate-800', + cardHover: 'slate-700', + input: 'slate-800', + inputBorder: 'blue-800', + overlay: 'slate-900/50', + }, + }, + light: { + type: 'light', + name: '红白主题', + colors: { + background: 'white', + backgroundSecondary: 'red-50', + text: 'gray-900', + textSecondary: 'gray-600', + primary: 'red-600', + primaryHover: 'red-500', + border: 'red-200', + accent: 'red-700', + success: 'green-600', + warning: 'amber-500', + error: 'red-600', + card: 'white', + cardHover: 'red-50', + input: 'white', + inputBorder: 'red-200', + overlay: 'black/30', + }, + }, +}; + +// 主题上下文 +interface ThemeContextType { + theme: ThemeType; + themeConfig: ThemeConfig; + toggleTheme: () => void; + setTheme: (theme: ThemeType) => void; +} + +const ThemeContext = createContext(undefined); + +// 主题提供者 +interface ThemeProviderProps { + children: ReactNode; +} + +export const ThemeProvider: React.FC = ({ children }) => { + const [theme, setThemeState] = useState(() => { + // 从 localStorage 读取保存的主题 + const saved = localStorage.getItem('strategy-theme'); + return (saved as ThemeType) || 'dark'; + }); + + // 切换主题 + const toggleTheme = () => { + setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark')); + }; + + // 设置特定主题 + const setTheme = (newTheme: ThemeType) => { + setThemeState(newTheme); + }; + + // 保存到 localStorage + useEffect(() => { + localStorage.setItem('strategy-theme', theme); + // 更新文档类名 + document.documentElement.classList.remove('dark', 'light'); + document.documentElement.classList.add(theme); + }, [theme]); + + const themeConfig = themes[theme]; + + return ( + + {children} + + ); +}; + +// 使用主题的 Hook +export const useTheme = (): ThemeContextType => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +// 主题工具函数 - 生成动态类名 +export const getThemeClasses = (colorKey: keyof ThemeConfig['colors']): string => { + const { themeConfig } = useTheme(); + return themeConfig.colors[colorKey]; +}; + +// 组合类名工具函数 +export const cn = (...classes: (string | undefined | null | false)[]): string => { + return classes.filter(Boolean).join(' '); +};