import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Settings, Brain, Search, Image as ImageIcon, Video, Mic, Send, Upload, Download, Copy, Share2, Menu, X, Sun, Moon, Volume2, Globe, Trash2, Plus, Info, PanelRight, PanelRightClose, History, Home as HomeIcon, Sparkles, Layers, Sliders, MonitorDown, AlertTriangle, Sigma, Cpu, Code, Box, Network, Bot, Binary, FileText, Database, ChevronDown, CheckCircle, AlertCircle, Languages } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import html2canvas from 'html2canvas'; import { AppModule, Message, MessageRole, Session, AppSettings, VeoConfig, ImageConfig } from './types'; import { GeminiService } from './services/gemini'; import { t } from './services/i18n'; import { GenerateContentResponse } from '@google/genai'; // --- Components --- // Button Component const Button: React.FC & { variant?: 'primary' | 'secondary' | 'ghost' | 'danger' }> = ({ className = '', variant = 'primary', children, ...props }) => { const baseStyle = "px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95 disabled:active:scale-100"; const variants = { primary: "bg-blue-600 text-white hover:bg-blue-700 shadow-md hover:shadow-lg hover:-translate-y-0.5", secondary: "bg-white dark:bg-slate-800 text-gray-700 dark:text-gray-200 border border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-700 hover:-translate-y-0.5 shadow-sm", ghost: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800", danger: "bg-red-50 text-red-600 border border-red-200 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-400 dark:border-red-900/50 dark:hover:bg-red-900/30 hover:-translate-y-0.5" }; return ; }; // Modal Component const Modal: React.FC<{ isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }> = ({ isOpen, onClose, title, children }) => { if (!isOpen) return null; return (

{title}

{children}
); }; // Toast Type interface ToastItem { id: number; message: string; type: 'success' | 'error' | 'info' | 'warning'; } // Constants for UI const SIDEBAR_GROUPS = [ { title: 'group.cs', modules: [ { id: AppModule.MATH, icon: Sigma, label: 'module.math', desc: 'desc.math' }, { id: AppModule.THEORY, icon: Binary, label: 'module.theory', desc: 'desc.theory' }, { id: AppModule.PRINCIPLES, icon: Cpu, label: 'module.principles', desc: 'desc.principles' }, { id: AppModule.SOFT_ENG, icon: Code, label: 'module.soft_eng', desc: 'desc.soft_eng' }, { id: AppModule.GRAPHICS, icon: Box, label: 'module.graphics', desc: 'desc.graphics' }, { id: AppModule.NETWORK, icon: Network, label: 'module.network', desc: 'desc.network' }, { id: AppModule.AI_LAB, icon: Bot, label: 'module.ai_lab', desc: 'desc.ai_lab' }, ] }, { title: 'group.tools', modules: [ { id: AppModule.RESEARCH, icon: Search, label: 'module.research', desc: 'desc.research' }, { id: AppModule.SQL, icon: Database, label: 'module.sql', desc: 'desc.sql' }, ] }, { title: 'group.creation', modules: [ { id: AppModule.VISION, icon: ImageIcon, label: 'module.vision', desc: 'desc.vision' }, { id: AppModule.STUDIO, icon: Video, label: 'module.studio', desc: 'desc.studio' }, { id: AppModule.AUDIO, icon: Mic, label: 'module.audio', desc: 'desc.audio' }, ] } ]; // Helper for safe URL parsing const getHostname = (url: string) => { try { if (!url) return ''; return new URL(url).hostname; } catch (e) { return url; } }; // Main App export default function App() { // --- State --- const [settings, setSettings] = useState(() => { const saved = localStorage.getItem('bitsage_settings'); const parsed = saved ? JSON.parse(saved) : {}; return { apiKey: '', language: 'zh-CN', theme: 'system', hasCompletedOnboarding: false, aiResponseLanguage: 'system', // Default: Follow system language ...parsed }; }); const [sessions, setSessions] = useState(() => { const saved = localStorage.getItem('bitsage_sessions'); return saved ? JSON.parse(saved) : []; }); const [currentSessionId, setCurrentSessionId] = useState(null); const [currentModule, setCurrentModule] = useState(AppModule.MATH); const [isHome, setIsHome] = useState(true); const [inputText, setInputText] = useState(''); const [isSidebarOpen, setIsSidebarOpen] = useState(false); // Left Sidebar const [isHistoryOpen, setIsHistoryOpen] = useState(() => typeof window !== 'undefined' && window.innerWidth >= 1024); // Right Sidebar const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isShareModalOpen, setIsShareModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [loadingText, setLoadingText] = useState(''); // Toast State const [toasts, setToasts] = useState([]); // Thinking Toggle State const [isThinkingMode, setIsThinkingMode] = useState(false); // PWA Install Prompt const [installPrompt, setInstallPrompt] = useState(null); // Attachments const [attachments, setAttachments] = useState<{data: string, mimeType: string}[]>([]); // Gen Configs const [veoConfig, setVeoConfig] = useState({ aspectRatio: '16:9', resolution: '720p' }); const [imgConfig, setImgConfig] = useState({ size: '1K', aspectRatio: '1:1' }); // SQL Tool State const [sqlInput, setSqlInput] = useState(''); const [sqlOutput, setSqlOutput] = useState(''); const [sqlTargetDB, setSqlTargetDB] = useState('MySQL'); const [isCustomSqlOpen, setIsCustomSqlOpen] = useState(false); const [sqlCustomPrompt, setSqlCustomPrompt] = useState(''); const messagesEndRef = useRef(null); const geminiRef = useRef(null); // --- Effects --- useEffect(() => { const handleBeforeInstallPrompt = (e: any) => { e.preventDefault(); setInstallPrompt(e); }; window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); return () => window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); }, []); useEffect(() => { localStorage.setItem('bitsage_settings', JSON.stringify(settings)); if (settings.theme === 'dark' || (settings.theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } geminiRef.current = new GeminiService(settings.apiKey); }, [settings]); useEffect(() => { // Only save sessions that are NOT from Custom View modules const sessionsToSave = sessions.filter(s => ![AppModule.VISION, AppModule.STUDIO, AppModule.AUDIO, AppModule.SQL].includes(s.module) ); localStorage.setItem('bitsage_sessions', JSON.stringify(sessionsToSave)); }, [sessions]); useEffect(() => { if (!isHome && !isCustomViewModule(currentModule)) { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [sessions, currentSessionId, isLoading, isHome]); // --- Helpers --- const showToast = (message: string, type: ToastItem['type'] = 'info') => { const id = Date.now(); setToasts(prev => [...prev, { id, message, type }]); setTimeout(() => { setToasts(prev => prev.filter(t => t.id !== id)); }, 3000); }; const isCustomViewModule = (mod: AppModule) => { return [AppModule.VISION, AppModule.STUDIO, AppModule.AUDIO, AppModule.SQL].includes(mod); }; const getCurrentSession = useCallback(() => { return sessions.find(s => s.id === currentSessionId); }, [sessions, currentSessionId]); const handleModuleSelect = (module: AppModule) => { setIsHome(false); setCurrentModule(module); setCurrentSessionId(null); // Draft mode if (window.innerWidth < 768) setIsSidebarOpen(false); // Auto-open history on large screens ONLY if NOT custom view module if (isCustomViewModule(module)) { setIsHistoryOpen(false); } else if (window.innerWidth >= 1024) { setIsHistoryOpen(true); } }; const handleHomeSelect = () => { setIsHome(true); if (window.innerWidth < 768) setIsSidebarOpen(false); }; const handleSessionSelect = (sessionId: string, module: AppModule) => { setIsHome(false); setCurrentModule(module); setCurrentSessionId(sessionId); if (window.innerWidth < 768) { setIsSidebarOpen(false); // Close left nav setIsHistoryOpen(false); // Close right nav on mobile after selection } }; const deleteSession = (e: React.MouseEvent, sessionId: string) => { e.stopPropagation(); if (window.confirm(t('confirm.delete', settings.language))) { setSessions(prev => prev.filter(s => s.id !== sessionId)); if (currentSessionId === sessionId) { setCurrentSessionId(null); // Go to draft mode } } }; const updateSessionMessages = (sessionId: string, newMessages: Message[]) => { setSessions(prev => prev.map(s => { if (s.id === sessionId) { // Auto update title if it's the first user message let title = s.title; const userMsg = newMessages.find(m => m.role === MessageRole.USER); if (s.messages.length === 0 && userMsg?.text) { title = userMsg.text.slice(0, 30) + (userMsg.text.length > 30 ? '...' : ''); } return { ...s, messages: newMessages, title, updatedAt: Date.now() }; } return s; })); }; const handleFileUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { const base64 = (evt.target?.result as string).split(',')[1]; setAttachments(prev => [...prev, { data: base64, mimeType: file.type }]); }; reader.readAsDataURL(file); }; const exportData = () => { const dataStr = JSON.stringify({ settings, sessions }); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bitsage_backup_${new Date().toISOString()}.json`; a.click(); }; const importData = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { try { const data = JSON.parse(evt.target?.result as string); if (data.settings) setSettings(data.settings); if (data.sessions) setSessions(data.sessions); showToast(t('success.data_imported', settings.language), 'success'); } catch (err) { showToast(t('alert.invalid_file', settings.language), 'error'); } }; reader.readAsText(file); }; const handleClearData = () => { if (window.confirm(t('confirm.clear_data', settings.language))) { localStorage.clear(); window.location.reload(); } }; const handleInstallClick = async () => { if (!installPrompt) return; installPrompt.prompt(); const { outcome } = await installPrompt.userChoice; if (outcome === 'accepted') { setInstallPrompt(null); } }; // --- SQL Tool Actions --- const handleSqlFormat = async () => { if(!sqlInput.trim()) { showToast(t('warning.no_sql', settings.language), 'warning'); return; } if(!settings.apiKey) { setIsSettingsOpen(true); return; } setIsLoading(true); setLoadingText(t('sql.processing', settings.language)); try { const res = await geminiRef.current!.toolsSql(sqlInput, 'format'); setSqlOutput(res); } catch (e) { setSqlOutput("Error: " + (e as Error).message); } finally { setIsLoading(false); setLoadingText(''); } }; const handleSqlConvert = async () => { if(!sqlInput.trim()) { showToast(t('warning.no_sql', settings.language), 'warning'); return; } if(!settings.apiKey) { setIsSettingsOpen(true); return; } setIsLoading(true); setLoadingText(t('sql.processing', settings.language)); try { const res = await geminiRef.current!.toolsSql(sqlInput, 'convert', sqlTargetDB); setSqlOutput(res); } catch (e) { setSqlOutput("Error: " + (e as Error).message); } finally { setIsLoading(false); setLoadingText(''); } }; const handleSqlReplace = () => { if(!sqlInput.trim()) { showToast(t('warning.no_sql', settings.language), 'warning'); return; } let counter = 1; // Regex to find AS followed by whitespace and an identifier // Handles quoted identifiers roughly const regex = /\bAS\s+((?:`[^`]+`)|(?:"[^"]+")|(?:'[^']+')|(?:\w+))/gi; const result = sqlInput.replace(regex, () => { return `AS ${counter++}`; }); setSqlOutput(result); }; const handleSqlMinify = () => { if(!sqlInput.trim()) { showToast(t('warning.no_sql', settings.language), 'warning'); return; } // Basic minification: remove comments, collapse whitespace let res = sqlInput .replace(/--.*$/gm, '') // remove single line comments .replace(/\/\*[\s\S]*?\*\//g, '') // remove multi line comments .replace(/\s+/g, ' ') // collapse whitespace .trim(); setSqlOutput(res); }; const handleSqlCustom = async () => { // Allow empty sqlInput if user is asking for generation if(!sqlCustomPrompt.trim()) { showToast("Please describe what you want the AI to do.", 'warning'); return; } if(!settings.apiKey) { setIsSettingsOpen(true); return; } setIsLoading(true); setLoadingText(t('sql.processing', settings.language)); try { const res = await geminiRef.current!.toolsSql(sqlInput, 'custom', undefined, sqlCustomPrompt); setSqlOutput(res); } catch (e) { setSqlOutput("Error: " + (e as Error).message); } finally { setIsLoading(false); setLoadingText(''); setIsCustomSqlOpen(false); // Auto close after run } }; // --- Share & Download Actions --- const getModelDisplayName = (module: AppModule) => { if (module === AppModule.STUDIO) return 'Veo 3.1'; if (module === AppModule.RESEARCH || module === AppModule.AUDIO) return 'Gemini 3 Flash'; return 'Gemini 3 Pro'; }; const handleCopySession = () => { const session = getCurrentSession(); if (!session) return; const text = session.messages.map(m => { const role = m.role === MessageRole.USER ? t('role.user', settings.language) : getModelDisplayName(session.module); const time = new Date(m.timestamp).toLocaleString(); return `[${role} - ${time}]\n${m.text || '[Media]'}\n`; }).join('\n-------------------\n'); navigator.clipboard.writeText(text); showToast(t('success.copy', settings.language), 'success'); setIsShareModalOpen(false); }; const handleDownloadText = () => { const session = getCurrentSession(); if (!session) return; const text = session.messages.map(m => { const role = m.role === MessageRole.USER ? t('role.user', settings.language) : getModelDisplayName(session.module); const time = new Date(m.timestamp).toLocaleString(); return `[${role} - ${time}]\n${m.text || '[Media]'}\n`; }).join('\n-------------------\n'); const blob = new Blob([text], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bitsage-chat-${session.id}.txt`; a.click(); setIsShareModalOpen(false); }; const handleDownloadImage = async () => { const el = document.getElementById('chat-content'); if (!el) return; try { const isDark = document.documentElement.classList.contains('dark'); const canvas = await html2canvas(el, { backgroundColor: isDark ? '#0f172a' : '#f9fafb', // slate-900 or gray-50 scale: 2, // High res }); const link = document.createElement('a'); link.download = `bitsage-chat-${Date.now()}.png`; link.href = canvas.toDataURL(); link.click(); setIsShareModalOpen(false); } catch (e) { console.error("Screenshot failed", e); showToast(t('error.screenshot', settings.language), 'error'); } }; // --- Core Actions --- const handleSend = async () => { if ((!inputText.trim() && attachments.length === 0) || isLoading) return; if (!settings.apiKey) { showToast(t('error.no_key', settings.language), 'error'); setIsSettingsOpen(true); return; } let activeSessionId = currentSessionId; let currentSessionMessages: Message[] = []; // Lazy Creation: Create session now if it doesn't exist if (!activeSessionId) { const newSession: Session = { id: Date.now().toString(), title: isCustomViewModule(currentModule) ? inputText.slice(0, 20) : t('action.new_chat', settings.language), module: currentModule, messages: [], createdAt: Date.now(), updatedAt: Date.now() }; setSessions(prev => [newSession, ...prev]); setCurrentSessionId(newSession.id); activeSessionId = newSession.id; currentSessionMessages = []; } else { const session = getCurrentSession(); if (session) currentSessionMessages = session.messages; } if (!activeSessionId) return; const userMsg: Message = { id: Date.now().toString(), role: MessageRole.USER, text: inputText, timestamp: Date.now(), images: attachments.filter(a => a.mimeType.startsWith('image/')).map(a => a.data) }; const newMessages = [...currentSessionMessages, userMsg]; updateSessionMessages(activeSessionId, newMessages); setInputText(''); setAttachments([]); setIsLoading(true); // Initial placeholder for AI response const botMsgId = (Date.now() + 1).toString(); updateSessionMessages(activeSessionId, [...newMessages, { id: botMsgId, role: MessageRole.MODEL, timestamp: Date.now(), text: '', isThinking: isThinkingMode && !isCustomViewModule(currentModule) }]); try { if (currentModule === AppModule.STUDIO && inputText) { setLoadingText(t('status.generating', settings.language)); const videoUrl = await geminiRef.current!.generateVideo(inputText, veoConfig); updateSessionMessages(activeSessionId, [...newMessages, { id: botMsgId, role: MessageRole.MODEL, timestamp: Date.now(), video: videoUrl }]); } else if (currentModule === AppModule.VISION && inputText.toLowerCase().includes('generate') && attachments.length === 0) { setLoadingText(t('status.generating', settings.language)); const imgUrl = await geminiRef.current!.generateImage(inputText, imgConfig); updateSessionMessages(activeSessionId, [...newMessages, { id: botMsgId, role: MessageRole.MODEL, timestamp: Date.now(), images: [imgUrl.split(',')[1]] }]); } else { // Text/Chat Generation if (isThinkingMode) setLoadingText(t('status.thinking', settings.language)); else setLoadingText(t('status.generating', settings.language)); // Prepare history for API const historyParts = newMessages.map(m => { const parts: any[] = []; if (m.text) parts.push({ text: m.text }); if (m.images) m.images.forEach(img => parts.push({ inlineData: { mimeType: 'image/jpeg', data: img } })); return { role: m.role, parts }; }); // Pass isThinkingMode to the service const stream = await geminiRef.current!.generateText( inputText, currentModule, historyParts, settings.language, attachments, isThinkingMode, settings.aiResponseLanguage ); let fullText = ''; let sources: any[] = []; for await (const chunk of stream) { const c = chunk as GenerateContentResponse; if (c.text) { fullText += c.text; // Update UI in real-time setSessions(prev => prev.map(s => { if (s.id === activeSessionId) { const msgs = [...s.messages]; const lastMsg = msgs[msgs.length - 1]; if (lastMsg.id === botMsgId) { lastMsg.text = fullText; lastMsg.isThinking = false; // Turn off thinking indicator once text starts arriving (simplification) } return { ...s, messages: msgs }; } return s; })); } if (c.candidates?.[0]?.groundingMetadata?.groundingChunks) { const chunks = c.candidates[0].groundingMetadata.groundingChunks; chunks.forEach((ch: any) => { if (ch.web?.uri) sources.push({ uri: ch.web.uri, title: ch.web.title }); }); } } // Final update with sources setSessions(prev => prev.map(s => { if (s.id === activeSessionId) { const msgs = [...s.messages]; const lastMsg = msgs[msgs.length - 1]; if (lastMsg.id === botMsgId) { lastMsg.sources = sources.length > 0 ? sources : undefined; } return { ...s, messages: msgs }; } return s; })); } } catch (err) { console.error(err); updateSessionMessages(activeSessionId, [...newMessages, { id: botMsgId, role: MessageRole.MODEL, timestamp: Date.now(), text: `Error: ${(err as Error).message}` }]); } finally { setIsLoading(false); setLoadingText(''); } }; const handleTTS = async (text: string) => { try { setIsLoading(true); const audioBase64 = await geminiRef.current!.generateSpeech(text); const audio = new Audio(`data:audio/mp3;base64,${audioBase64}`); audio.play(); } catch (e) { showToast(t('error.tts', settings.language) + ': ' + (e as Error).message, 'error'); } finally { setIsLoading(false); } }; const handleCopy = (text: string) => { navigator.clipboard.writeText(text); showToast(t('success.copy', settings.language), 'success'); }; // --- Renderers --- const renderSidebar = () => (

{t('app.name', settings.language)}

{/* Home Link */} {/* Module Groups */} {SIDEBAR_GROUPS.map((group, groupIdx) => (

{t(group.title, settings.language)}

{group.modules.map((m) => ( ))}
))}
{installPrompt && ( )}
); const renderHistorySidebar = () => { // Hide history on Home page or Custom View Modules if (isHome || isCustomViewModule(currentModule)) return null; const moduleSessions = sessions.filter(s => s.module === currentModule); return ( <> {/* Mobile Backdrop */} {isHistoryOpen && (
setIsHistoryOpen(false)} /> )} {/* Sidebar Panel */}

{t('history.title', settings.language)}

{moduleSessions.length === 0 ? (
{t('history.empty', settings.language)}
) : (
{moduleSessions.map(s => (
handleSessionSelect(s.id, s.module)} className={`group relative p-3 rounded-lg cursor-pointer transition-colors text-sm border ${currentSessionId === s.id ? 'bg-white dark:bg-slate-800 border-blue-200 dark:border-blue-900 shadow-sm' : 'border-transparent hover:bg-white dark:hover:bg-slate-800 hover:border-gray-200 dark:hover:border-slate-700'}`} >

{s.title}

{new Date(s.updatedAt).toLocaleDateString()}
))}
)}
); }; const renderHome = () => (

{t('welcome.title', settings.language)}

{t('welcome.subtitle', settings.language)}

{!settings.apiKey && (
)}
{SIDEBAR_GROUPS.flatMap(g => g.modules).map((m, i) => ( ))}
); const renderSQLTool = () => { return (
{t('module.sql', settings.language)}
{/* Controls */}
{/* Custom Prompt Area */} {isCustomSqlOpen && (
setSqlCustomPrompt(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSqlCustom()} />
)} {/* Editor Area */}