diff --git a/App.tsx b/App.tsx index 28bb99c..d3c166b 100644 --- a/App.tsx +++ b/App.tsx @@ -30,9 +30,16 @@ import { Copy, Image as ImageIcon, FileText, - Download + Download, + Moon, + Sun, + Monitor, + Globe, + CheckCircle2, + AlertCircle } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import html2canvas from 'html2canvas'; import { TRANSLATIONS, DEFAULT_LANGUAGE } from './constants'; import { AppLanguage, ChatMode, Message, UserSettings, ChatSession, ChatScenario } from './types'; @@ -48,12 +55,19 @@ const SCENARIOS = [ ChatScenario.RESEARCH ]; +interface Toast { + id: string; + message: string; + type: 'success' | 'error' | 'info'; +} + const App: React.FC = () => { const [settings, setSettingsState] = useState(loadSettings()); const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(null); const [selectedScenario, setSelectedScenario] = useState(ChatScenario.GENERAL); const [selectedMode, setSelectedMode] = useState(ChatMode.STANDARD); + const [replyLanguage, setReplyLanguage] = useState('system'); // 'system', 'auto', or specific language code const [input, setInput] = useState(''); const [activeView, setActiveView] = useState<'home' | 'chat' | 'tools' | 'settings'>('home'); const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(false); @@ -66,6 +80,7 @@ const App: React.FC = () => { const [loadingStep, setLoadingStep] = useState(0); const [showShareMenu, setShowShareMenu] = useState(false); const [installPrompt, setInstallPrompt] = useState(null); + const [toasts, setToasts] = useState([]); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); @@ -104,6 +119,41 @@ const App: React.FC = () => { }; }, []); + // Theme Logic + useEffect(() => { + const applyTheme = () => { + const root = document.documentElement; + const isDark = + settings.theme === 'dark' || + (settings.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches); + + if (isDark) { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + }; + + applyTheme(); + + // Listen for system changes if auto + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = () => { + if (settings.theme === 'auto') applyTheme(); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [settings.theme]); + + const addToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => { + const id = Date.now().toString(); + setToasts(prev => [...prev, { id, message, type }]); + setTimeout(() => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, 3000); + }; + const handleFinishOnboarding = () => { setShowOnboarding(false); setSettingsState(prev => ({ ...prev, isOnboarded: true })); @@ -283,11 +333,18 @@ const App: React.FC = () => { let groundingData: any = null; const usedModel = getModelNameForMode(session!.mode); + // Determine target language for AI + let targetLangCode = replyLanguage; + if (replyLanguage === 'system') { + targetLangCode = settings.language; + } + // if 'auto', we pass 'auto' to the service which handles it. + await streamChatResponse( updatedMessages, userMsg.content, session!.mode, - settings.language, + targetLangCode, session!.scenario || ChatScenario.GENERAL, userMsg.attachments as any, (text, grounding) => { @@ -313,6 +370,7 @@ const App: React.FC = () => { } catch (err: any) { console.error(err); + addToast(err.message || t.apiError, 'error'); const errorMsg: Message = { id: Date.now().toString(), role: 'model', content: err.message || t.apiError, timestamp: Date.now() }; setSessions(prev => prev.map(s => s.id === session!.id ? { ...s, messages: [...updatedMessages, errorMsg] } : s)); } finally { @@ -355,7 +413,7 @@ const App: React.FC = () => { const text = await transcribeAudio(base64, 'audio/webm', settings.language); setInput(prev => prev + " " + text); } catch (e) { - alert(t.transcriptionFail); + addToast(t.transcriptionFail, 'error'); } finally { setIsProcessing(false); } @@ -366,7 +424,7 @@ const App: React.FC = () => { mediaRecorder.start(); setIsRecording(true); } catch (e) { - alert(t.micError); + addToast(t.micError, 'error'); } }; @@ -375,11 +433,11 @@ const App: React.FC = () => { if (!file) return; const success = await importData(file); if (success) { - alert(t.importSuccess); + addToast(t.importSuccess, 'success'); setSessions(loadSessions()); setSettingsState(loadSettings()); } else { - alert(t.importFail); + addToast(t.importFail, 'error'); } e.target.value = ''; }; @@ -408,7 +466,7 @@ const App: React.FC = () => { ).join('\n\n'); navigator.clipboard.writeText(text).then(() => { - alert("对话已复制到剪贴板!"); + addToast(t.toast.copySuccess, 'success'); setShowShareMenu(false); }); }; @@ -453,7 +511,8 @@ const App: React.FC = () => { maxHeight: 'none', overflow: 'visible', zIndex: '-1000', - background: '#f8fafc', // slate-50 + background: settings.theme === 'dark' ? '#0f172a' : '#f8fafc', + color: settings.theme === 'dark' ? '#f1f5f9' : '#0f172a' }); document.body.appendChild(clone); @@ -463,7 +522,7 @@ const App: React.FC = () => { // 4. 使用 html2canvas 截图 const canvas = await html2canvas(clone, { - backgroundColor: '#f8fafc', + backgroundColor: settings.theme === 'dark' ? '#0f172a' : '#f8fafc', useCORS: true, logging: false, scale: 2, // 提升清晰度 @@ -484,7 +543,7 @@ const App: React.FC = () => { setShowShareMenu(false); } catch (error) { console.error("Image generation failed", error); - alert("生成图片失败,请重试。"); + addToast(t.genError, 'error'); } finally { setIsProcessing(false); } @@ -507,34 +566,54 @@ const App: React.FC = () => { return ( // 使用 100dvh 适配移动端浏览器视口,防止被地址栏遮挡 -
+
+ + {/* Toast Notification */} +
+ {toasts.map(toast => ( +
+ {toast.type === 'success' && } + {toast.type === 'error' && } + {toast.message} +
+ ))} +
+ {/* ONBOARDING MODAL */} {showOnboarding && (
-
-
+
+
-

{t.appName} - {t.tagline}

+

{t.appName} - {t.tagline}

  • - 1 -

    {t.onboarding.step1}

    + 1 +

    {t.onboarding.step1}

  • - 2 -

    {t.onboarding.step2}

    + 2 +

    {t.onboarding.step2}

  • - 3 -

    {t.onboarding.step3}

    + 3 +

    {t.onboarding.step3}

@@ -560,16 +639,16 @@ const App: React.FC = () => { {/* LEFT SIDEBAR */}
{/* 底部菜单适配安全区域 */} -
+
+ {installPrompt && ( + + )} {activeView === 'chat' && ( -
- {getScenarioIcon(currentSession ? currentSession.scenario : selectedScenario)} +
+ {getScenarioIcon(currentSession ? currentSession.scenario : selectedScenario)} {t.scenarios[(currentSession ? currentSession.scenario : selectedScenario) || ChatScenario.GENERAL].title}
)} {activeView === 'home' && ( -
- +
+ {t.home}
)} @@ -655,11 +743,31 @@ const App: React.FC = () => {
{activeView === 'chat' && ( <> -
+ {/* Language Selector in Chat Header */} +
+
+ + +
+
+ +
{[ - { mode: ChatMode.STANDARD, label: t.modeStandard, color: 'text-blue-600' }, - { mode: ChatMode.DEEP, label: t.modeDeep, color: 'text-purple-600' }, - { mode: ChatMode.FAST, label: t.modeFast, color: 'text-green-600' } + { mode: ChatMode.STANDARD, label: t.modeStandard, color: 'text-blue-600 dark:text-blue-400' }, + { mode: ChatMode.DEEP, label: t.modeDeep, color: 'text-purple-600 dark:text-purple-400' }, + { mode: ChatMode.FAST, label: t.modeFast, color: 'text-green-600 dark:text-green-400' } ].map(m => ( @@ -678,20 +786,20 @@ const App: React.FC = () => {
{showShareMenu && ( -
- - -
@@ -705,7 +813,7 @@ const App: React.FC = () => { )} {activeView !== 'home' && activeView !== 'settings' && activeView !== 'tools' && ( - )} @@ -717,48 +825,48 @@ const App: React.FC = () => {
-

+

{t.homeWelcome}

-

+

{t.tagline} {t.homeDesc}

-
-
+
+
-
+
{t.homeQuoteTitle}
-
+
“{randomQuote.text}”
-
+
—— {randomQuote.author}
-

+

{t.homeFeatureTitle}

@@ -767,14 +875,14 @@ const App: React.FC = () => { ))}
@@ -787,11 +895,11 @@ const App: React.FC = () => {
{!currentSession ? ( -
-
+
+
{getScenarioIcon(selectedScenario)}
-

{t.scenarios[selectedScenario].title}

+

{t.scenarios[selectedScenario].title}

{t.scenarios[selectedScenario].greeting}

) : ( @@ -800,32 +908,32 @@ const App: React.FC = () => {
{msg.role === 'model' && (
-
+
{getScenarioIcon(currentSession.scenario)}
)}
{msg.attachments?.map((att, i) => ( -
+
attachment
))}
-
- {msg.content} +
+ {msg.content}
-
+
{new Date(msg.timestamp).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})} {msg.role === 'model' && ( <> {msg.model && ( <> - - {formatModelName(msg.model)} + + {formatModelName(msg.model)} )} @@ -840,17 +948,17 @@ const App: React.FC = () => { {isProcessing && !streamingContent && (
-
+
{getScenarioIcon(currentSession?.scenario)}
-
+
-
+
{getLoadingText(currentSession?.scenario)}
@@ -860,10 +968,10 @@ const App: React.FC = () => { {/* 正在生成(有内容)的状态 */} {isProcessing && streamingContent && (
-
-
-
- {streamingContent} +
+
+
+ {streamingContent}
{currentSession?.mode === ChatMode.DEEP ? t.thinking : t.generating} @@ -877,24 +985,24 @@ const App: React.FC = () => {
{/* 输入框区域增加底部安全距离适配 pb-[max(1rem,env(safe-area-inset-bottom))] */} -
+
-
+
- +