import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Settings, MessageSquare, 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 } 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"; 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}
); }; // Constants for UI const SIDEBAR_GROUPS = [ { title: 'group.learning', modules: [ { id: AppModule.TUTOR, icon: MessageSquare, label: 'module.tutor', desc: 'desc.tutor' }, { id: AppModule.THINKER, icon: Brain, label: 'module.thinker', desc: 'desc.thinker' }, { id: AppModule.RESEARCH, icon: Search, label: 'module.research', desc: 'desc.research' }, ] }, { 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' }, ] } ]; // Main App export default function App() { // --- State --- const [settings, setSettings] = useState(() => { const saved = localStorage.getItem('bitsage_settings'); return saved ? JSON.parse(saved) : { apiKey: '', language: 'zh-CN', theme: 'system', hasCompletedOnboarding: false }; }); 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.TUTOR); 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 [isLoading, setIsLoading] = useState(false); const [loadingText, setLoadingText] = useState(''); // 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' }); 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 Creative Studio modules const sessionsToSave = sessions.filter(s => ![AppModule.VISION, AppModule.STUDIO, AppModule.AUDIO].includes(s.module) ); localStorage.setItem('bitsage_sessions', JSON.stringify(sessionsToSave)); }, [sessions]); useEffect(() => { if (!isHome && !isCreativeModule(currentModule)) { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [sessions, currentSessionId, isLoading, isHome]); // --- Helpers --- const isCreativeModule = (mod: AppModule) => { return [AppModule.VISION, AppModule.STUDIO, AppModule.AUDIO].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 creative module if (isCreativeModule(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); alert(t('alert.import_success', settings.language)); } catch (err) { alert(t('alert.invalid_file', settings.language)); } }; 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); } }; // --- Core Actions --- const handleSend = async () => { if ((!inputText.trim() && attachments.length === 0) || isLoading) return; if (!settings.apiKey) { 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: isCreativeModule(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: currentModule === AppModule.THINKER }]); 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 (currentModule === AppModule.THINKER) 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 }; }); const stream = await geminiRef.current!.generateText(inputText, currentModule, historyParts, attachments); 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; } 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) { alert('TTS Error: ' + (e as Error).message); } finally { setIsLoading(false); } }; const handleCopy = (text: string) => { navigator.clipboard.writeText(text); }; const handleShare = async () => { const el = document.getElementById('chat-container'); if (el) { const canvas = await html2canvas(el); const link = document.createElement('a'); link.download = `bitsage-share-${Date.now()}.png`; link.href = canvas.toDataURL(); link.click(); } }; // --- 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 Creative Modules if (isHome || isCreativeModule(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 renderMessage = (msg: Message) => (
{msg.images && msg.images.map((img, i) => ( Generated ))} {msg.video && (
); // Creative Studio Specific Components const renderCreativeStudio = () => { const session = getCurrentSession(); const messages = session ? [...session.messages].reverse() : []; // Group Model outputs with their preceding user inputs const generations: { id: string, prompt: string, output: Message, timestamp: number }[] = []; // Simple pairing logic: Find model message, look for immediate preceding user message for (let i = 0; i < messages.length; i++) { if (messages[i].role === MessageRole.MODEL) { const userMsg = messages[i+1]; // since reversed if (userMsg && userMsg.role === MessageRole.USER) { generations.push({ id: messages[i].id, prompt: userMsg.text || '', output: messages[i], timestamp: messages[i].timestamp }); } } } return (
{/* Top: Workbench / Input Area */}
{t('ui.workbench', settings.language)}
{/* Config Row */}
{currentModule === AppModule.STUDIO && ( <>
)} {currentModule === AppModule.VISION && ( <>
)}
{/* Input & Action */}