2 Commits

Author SHA1 Message Date
9bc88f7d6a 更新至 v0.3.0_20251226 版本 2025-12-26 16:06:34 +08:00
efa471aa2e 更新至 v0.2.0_20251225 版本 2025-12-25 16:39:44 +08:00
9 changed files with 1011 additions and 126 deletions

505
App.tsx
View File

@@ -1,10 +1,12 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Settings, MessageSquare, Brain, Search, Image as ImageIcon,
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
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';
@@ -20,7 +22,7 @@ import { GenerateContentResponse } from '@google/genai';
// Button Component
const Button: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement> & { 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 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",
@@ -46,14 +48,32 @@ const Modal: React.FC<{ isOpen: boolean; onClose: () => void; title: string; chi
);
};
// Toast Type
interface ToastItem {
id: number;
message: string;
type: 'success' | 'error' | 'info' | 'warning';
}
// Constants for UI
const SIDEBAR_GROUPS = [
{
title: 'group.learning',
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.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' },
{ id: AppModule.SQL, icon: Database, label: 'module.sql', desc: 'desc.sql' },
]
},
{
@@ -66,16 +86,29 @@ const SIDEBAR_GROUPS = [
}
];
// 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<AppSettings>(() => {
const saved = localStorage.getItem('bitsage_settings');
return saved ? JSON.parse(saved) : {
const parsed = saved ? JSON.parse(saved) : {};
return {
apiKey: '',
language: 'zh-CN',
theme: 'system',
hasCompletedOnboarding: false
hasCompletedOnboarding: false,
aiResponseLanguage: 'system', // Default: Follow system language
...parsed
};
});
@@ -85,7 +118,7 @@ export default function App() {
});
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
const [currentModule, setCurrentModule] = useState<AppModule>(AppModule.TUTOR);
const [currentModule, setCurrentModule] = useState<AppModule>(AppModule.MATH);
const [isHome, setIsHome] = useState(true);
const [inputText, setInputText] = useState('');
@@ -93,9 +126,16 @@ export default function App() {
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<ToastItem[]>([]);
// Thinking Toggle State
const [isThinkingMode, setIsThinkingMode] = useState(false);
// PWA Install Prompt
const [installPrompt, setInstallPrompt] = useState<any>(null);
@@ -106,6 +146,13 @@ export default function App() {
const [veoConfig, setVeoConfig] = useState<VeoConfig>({ aspectRatio: '16:9', resolution: '720p' });
const [imgConfig, setImgConfig] = useState<ImageConfig>({ 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<HTMLDivElement>(null);
const geminiRef = useRef<GeminiService | null>(null);
@@ -131,23 +178,31 @@ export default function App() {
}, [settings]);
useEffect(() => {
// Only save sessions that are NOT from Creative Studio modules
// Only save sessions that are NOT from Custom View modules
const sessionsToSave = sessions.filter(s =>
![AppModule.VISION, AppModule.STUDIO, AppModule.AUDIO].includes(s.module)
![AppModule.VISION, AppModule.STUDIO, AppModule.AUDIO, AppModule.SQL].includes(s.module)
);
localStorage.setItem('bitsage_sessions', JSON.stringify(sessionsToSave));
}, [sessions]);
useEffect(() => {
if (!isHome && !isCreativeModule(currentModule)) {
if (!isHome && !isCustomViewModule(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 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(() => {
@@ -160,8 +215,8 @@ export default function App() {
setCurrentSessionId(null); // Draft mode
if (window.innerWidth < 768) setIsSidebarOpen(false);
// Auto-open history on large screens ONLY if NOT creative module
if (isCreativeModule(module)) {
// Auto-open history on large screens ONLY if NOT custom view module
if (isCustomViewModule(module)) {
setIsHistoryOpen(false);
} else if (window.innerWidth >= 1024) {
setIsHistoryOpen(true);
@@ -239,9 +294,9 @@ export default function App() {
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));
showToast(t('success.data_imported', settings.language), 'success');
} catch (err) {
alert(t('alert.invalid_file', settings.language));
showToast(t('alert.invalid_file', settings.language), 'error');
}
};
reader.readAsText(file);
@@ -263,11 +318,175 @@ export default function App() {
}
};
// --- 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;
}
@@ -279,7 +498,7 @@ export default function App() {
if (!activeSessionId) {
const newSession: Session = {
id: Date.now().toString(),
title: isCreativeModule(currentModule) ? inputText.slice(0, 20) : t('action.new_chat', settings.language),
title: isCustomViewModule(currentModule) ? inputText.slice(0, 20) : t('action.new_chat', settings.language),
module: currentModule,
messages: [],
createdAt: Date.now(),
@@ -318,7 +537,7 @@ export default function App() {
role: MessageRole.MODEL,
timestamp: Date.now(),
text: '',
isThinking: currentModule === AppModule.THINKER
isThinking: isThinkingMode && !isCustomViewModule(currentModule)
}]);
try {
@@ -336,7 +555,7 @@ export default function App() {
}]);
} else {
// Text/Chat Generation
if (currentModule === AppModule.THINKER) setLoadingText(t('status.thinking', settings.language));
if (isThinkingMode) setLoadingText(t('status.thinking', settings.language));
else setLoadingText(t('status.generating', settings.language));
// Prepare history for API
@@ -347,7 +566,16 @@ export default function App() {
return { role: m.role, parts };
});
const stream = await geminiRef.current!.generateText(inputText, currentModule, historyParts, attachments);
// 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[] = [];
@@ -363,7 +591,7 @@ export default function App() {
const lastMsg = msgs[msgs.length - 1];
if (lastMsg.id === botMsgId) {
lastMsg.text = fullText;
lastMsg.isThinking = false;
lastMsg.isThinking = false; // Turn off thinking indicator once text starts arriving (simplification)
}
return { ...s, messages: msgs };
}
@@ -409,7 +637,7 @@ export default function App() {
const audio = new Audio(`data:audio/mp3;base64,${audioBase64}`);
audio.play();
} catch (e) {
alert('TTS Error: ' + (e as Error).message);
showToast(t('error.tts', settings.language) + ': ' + (e as Error).message, 'error');
} finally {
setIsLoading(false);
}
@@ -417,18 +645,9 @@ export default function App() {
const handleCopy = (text: string) => {
navigator.clipboard.writeText(text);
showToast(t('success.copy', settings.language), 'success');
};
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 ---
@@ -495,8 +714,8 @@ export default function App() {
);
const renderHistorySidebar = () => {
// Hide history on Home page or Creative Modules
if (isHome || isCreativeModule(currentModule)) return null;
// Hide history on Home page or Custom View Modules
if (isHome || isCustomViewModule(currentModule)) return null;
const moduleSessions = sessions.filter(s => s.module === currentModule);
@@ -572,8 +791,8 @@ export default function App() {
};
const renderHome = () => (
<div className="flex flex-col items-center min-h-full p-4 animate-fade-in">
<div className="max-w-4xl w-full my-auto py-10">
<div className="h-full w-full overflow-y-auto flex flex-col items-center p-4 animate-fade-in">
<div className="max-w-4xl w-full my-auto py-10 pb-24">
<div className="text-center mb-12 animate-slide-up">
<div className="bg-blue-100 dark:bg-blue-900/30 p-6 rounded-full inline-block mb-6 shadow-lg shadow-blue-500/20">
<Brain size={64} className="text-blue-600 dark:text-blue-400" />
@@ -614,9 +833,117 @@ export default function App() {
</div>
);
const renderSQLTool = () => {
return (
<div className="flex flex-col h-full bg-gray-50 dark:bg-slate-900">
<div className="bg-white dark:bg-slate-800 border-b dark:border-slate-700 p-6 shrink-0 z-10 shadow-sm transition-all">
<div className="max-w-5xl mx-auto">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-blue-600 uppercase tracking-wider">{t('module.sql', settings.language)}</span>
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap items-center gap-2 mb-4 p-2 bg-gray-100 dark:bg-slate-700 rounded-lg">
<Button onClick={handleSqlFormat} disabled={isLoading} variant="ghost" className="text-sm h-9">
{t('sql.format', settings.language)}
</Button>
<div className="w-px bg-gray-300 dark:bg-slate-600 mx-1 self-center h-6"></div>
<Button onClick={handleSqlReplace} disabled={isLoading} variant="ghost" className="text-sm h-9">
{t('sql.replace', settings.language)}
</Button>
<Button onClick={handleSqlMinify} disabled={isLoading} variant="ghost" className="text-sm h-9">
{t('sql.minify', settings.language)}
</Button>
<div className="w-px bg-gray-300 dark:bg-slate-600 mx-1 self-center h-6"></div>
<div className="flex items-center gap-2 pl-2 border-r border-gray-300 dark:border-slate-600 pr-2">
<select
className="bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-600 rounded text-sm h-8 px-2 outline-none"
value={sqlTargetDB} onChange={e => setSqlTargetDB(e.target.value)}
>
<option value="MySQL">MySQL</option>
<option value="PostgreSQL">PostgreSQL</option>
<option value="Oracle">Oracle</option>
<option value="SQL Server">SQL Server</option>
<option value="SQLite">SQLite</option>
</select>
<Button onClick={handleSqlConvert} disabled={isLoading} variant="primary" className="text-sm h-8 px-3">
{t('sql.convert', settings.language)}
</Button>
</div>
<Button
onClick={() => setIsCustomSqlOpen(!isCustomSqlOpen)}
disabled={isLoading}
variant={isCustomSqlOpen ? "primary" : "ghost"}
className="text-sm h-9 ml-auto"
>
<Sparkles size={14} /> {t('sql.custom', settings.language)} <ChevronDown size={14} className={`transform transition-transform ${isCustomSqlOpen ? 'rotate-180' : ''}`}/>
</Button>
</div>
{/* Custom Prompt Area */}
{isCustomSqlOpen && (
<div className="mb-4 animate-slide-up flex gap-2">
<input
type="text"
className="flex-1 bg-gray-50 dark:bg-slate-900 border border-gray-200 dark:border-slate-700 rounded-lg px-4 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-500"
placeholder={t('sql.custom_prompt', settings.language)}
value={sqlCustomPrompt}
onChange={e => setSqlCustomPrompt(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSqlCustom()}
/>
<Button onClick={handleSqlCustom} disabled={!sqlCustomPrompt.trim() || isLoading} variant="primary" className="text-sm h-10">
{t('sql.run', settings.language)}
</Button>
</div>
)}
{/* Editor Area */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 h-[calc(100vh-280px)]">
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-500 mb-2 uppercase">{t('sql.input', settings.language)}</label>
<textarea
className="flex-1 w-full bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 rounded-xl p-4 font-mono text-sm resize-none focus:ring-2 focus:ring-blue-500 outline-none"
placeholder={t('sql.placeholder', settings.language)}
value={sqlInput}
onChange={e => setSqlInput(e.target.value)}
/>
</div>
<div className="flex flex-col">
<label className="text-xs font-semibold text-gray-500 mb-2 uppercase flex justify-between">
{t('sql.output', settings.language)}
{sqlOutput && (
<button onClick={() => handleCopy(sqlOutput)} className="text-blue-500 hover:text-blue-600 flex items-center gap-1">
<Copy size={12}/> Copy
</button>
)}
</label>
<div className="flex-1 w-full bg-gray-100 dark:bg-slate-900/50 border border-gray-200 dark:border-slate-700 rounded-xl p-4 font-mono text-sm overflow-auto whitespace-pre">
{sqlOutput || <span className="text-gray-400 italic">...</span>}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
const renderMessage = (msg: Message) => (
<div key={msg.id} className={`flex ${msg.role === MessageRole.USER ? 'justify-end' : 'justify-start'} mb-6 group animate-slide-up`}>
<div className={`max-w-[85%] md:max-w-[75%] rounded-2xl p-4 transition-all duration-300 hover:shadow-md ${msg.role === MessageRole.USER ? 'bg-blue-600 text-white' : 'bg-white dark:bg-slate-800 border dark:border-slate-700 shadow-sm'}`}>
<div key={msg.id} className={`flex flex-col mb-6 group animate-slide-up ${msg.role === MessageRole.USER ? 'items-end' : 'items-start'}`}>
{/* Message Metadata */}
<div className={`flex items-center gap-2 mb-1 text-xs text-gray-400 ${msg.role === MessageRole.USER ? 'flex-row-reverse' : 'flex-row'}`}>
<span className="font-semibold text-gray-500 dark:text-gray-400">
{msg.role === MessageRole.USER ? t('role.user', settings.language) : getModelDisplayName(currentModule)}
</span>
<span className="w-1 h-1 rounded-full bg-gray-300 dark:bg-gray-600"></span>
<span>{new Date(msg.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
</div>
<div className={`max-w-[85%] md:max-w-[75%] rounded-2xl p-4 transition-all duration-300 hover:shadow-md ${msg.role === MessageRole.USER ? 'bg-blue-600 text-white rounded-tr-sm' : 'bg-white dark:bg-slate-800 border dark:border-slate-700 shadow-sm rounded-tl-sm'}`}>
{msg.images && msg.images.map((img, i) => (
<img key={i} src={`data:image/jpeg;base64,${img}`} alt="Generated" className="rounded-lg mb-2 max-h-64 object-contain bg-black/5" />
@@ -652,7 +979,7 @@ export default function App() {
<div className="flex flex-wrap gap-2">
{msg.sources.map((src, i) => (
<a key={i} href={src.uri} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1 text-xs bg-gray-100 dark:bg-slate-700 px-2 py-1 rounded hover:bg-gray-200 dark:hover:bg-slate-600 transition-colors truncate max-w-[200px]">
<Globe size={10} /> {src.title || new URL(src.uri).hostname}
<Globe size={10} /> {src.title || getHostname(src.uri)}
</a>
))}
</div>
@@ -906,9 +1233,22 @@ export default function App() {
};
return (
<div className="flex h-screen bg-gray-50 dark:bg-slate-900 overflow-hidden">
<div className="flex h-screen bg-gray-50 dark:bg-slate-900 overflow-hidden relative">
{renderSidebar()}
{/* Toast Container */}
<div className="fixed top-6 left-1/2 -translate-x-1/2 z-[100] flex flex-col gap-2 w-full max-w-sm px-4 pointer-events-none">
{toasts.map(toast => (
<div key={toast.id} className={`pointer-events-auto bg-white dark:bg-slate-800 text-gray-800 dark:text-gray-100 px-4 py-3 rounded-lg shadow-xl border border-gray-100 dark:border-slate-700 flex items-center gap-3 animate-slide-up`}>
{toast.type === 'success' && <CheckCircle className="text-green-500 shrink-0" size={20} />}
{toast.type === 'error' && <AlertCircle className="text-red-500 shrink-0" size={20} />}
{toast.type === 'warning' && <AlertTriangle className="text-yellow-500 shrink-0" size={20} />}
{toast.type === 'info' && <Info className="text-blue-500 shrink-0" size={20} />}
<span className="text-sm font-medium">{toast.message}</span>
</div>
))}
</div>
{/* Overlay for mobile sidebar */}
{isSidebarOpen && <div onClick={() => setIsSidebarOpen(false)} className="fixed inset-0 bg-black/50 z-30 md:hidden backdrop-blur-sm" />}
@@ -925,19 +1265,23 @@ export default function App() {
{isHome ? t('app.name', settings.language) : (
<>
<span className="text-gray-400 hidden sm:inline">{t(`module.${currentModule}`, settings.language)} /</span>
{getCurrentSession()?.title || (isCreativeModule(currentModule) ? t('ui.workbench', settings.language) : t('action.new_chat', settings.language))}
{getCurrentSession()?.title || (isCustomViewModule(currentModule) ? t('ui.workbench', settings.language) : t('action.new_chat', settings.language))}
</>
)}
</h2>
{isLoading && !isCreativeModule(currentModule) && (
{isLoading && !isCustomViewModule(currentModule) && (
<span className="text-xs text-blue-500 animate-pulse bg-blue-50 dark:bg-blue-900/20 px-2 py-1 rounded-full whitespace-nowrap">
{loadingText}
</span>
)}
</div>
<div className="flex items-center gap-1">
{!isHome && currentSessionId && !isCreativeModule(currentModule) && <button onClick={handleShare} className="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-lg"><Share2 size={20}/></button>}
{!isHome && !isHistoryOpen && !isCreativeModule(currentModule) && (
{!isHome && currentSessionId && !isCustomViewModule(currentModule) && (
<button onClick={() => setIsShareModalOpen(true)} className="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-lg">
<Share2 size={20}/>
</button>
)}
{!isHome && !isHistoryOpen && !isCustomViewModule(currentModule) && (
<button onClick={() => setIsHistoryOpen(true)} className="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-lg">
<PanelRight size={20} />
</button>
@@ -948,21 +1292,32 @@ export default function App() {
{/* Viewport - Main Scrolling Area */}
<main className="flex-1 overflow-hidden relative bg-gray-50 dark:bg-slate-900" id="chat-container">
{isHome ? renderHome() : (
isCreativeModule(currentModule) ? renderCreativeStudio() : (
currentModule === AppModule.SQL ? renderSQLTool() :
isCustomViewModule(currentModule) ? renderCreativeStudio() : (
<div className="h-full flex flex-col overflow-y-auto scroll-smooth">
{!currentSessionId ? (
<div className="flex-1 flex items-center justify-center p-8 text-center text-gray-400">
<div className="animate-bounce-in">
<div className="w-16 h-16 bg-gray-100 dark:bg-slate-800 rounded-2xl flex items-center justify-center mx-auto mb-4">
{AppModule.TUTOR === currentModule && <MessageSquare size={32} />}
{AppModule.THINKER === currentModule && <Brain size={32} />}
{AppModule.MATH === currentModule && <Sigma size={32} />}
{AppModule.THEORY === currentModule && <Binary size={32} />}
{AppModule.PRINCIPLES === currentModule && <Cpu size={32} />}
{AppModule.SOFT_ENG === currentModule && <Code size={32} />}
{AppModule.GRAPHICS === currentModule && <Box size={32} />}
{AppModule.NETWORK === currentModule && <Network size={32} />}
{AppModule.AI_LAB === currentModule && <Bot size={32} />}
{AppModule.RESEARCH === currentModule && <Search size={32} />}
</div>
<p>{t('prompt.placeholder', settings.language)}</p>
<h3 className="text-xl font-semibold mb-2 text-gray-800 dark:text-gray-200">
{t(`hello.${currentModule}`, settings.language)}
</h3>
<p className="text-sm max-w-sm mx-auto">
{t(`desc.${currentModule}`, settings.language)}
</p>
</div>
</div>
) : (
<div className="flex-1 p-4 md:p-8 max-w-4xl mx-auto w-full">
<div className="flex-1 p-4 md:p-8 max-w-4xl mx-auto w-full" id="chat-content">
{getCurrentSession()?.messages.map(renderMessage)}
<div ref={messagesEndRef} />
</div>
@@ -973,7 +1328,7 @@ export default function App() {
</main>
{/* Input Area (Only for Chat Modules) */}
{!isHome && !isCreativeModule(currentModule) && (
{!isHome && !isCustomViewModule(currentModule) && (
<div className="bg-white dark:bg-slate-900 border-t dark:border-slate-700 p-4 shrink-0 z-20">
<div className="max-w-4xl mx-auto">
@@ -996,6 +1351,29 @@ export default function App() {
<Plus size={20} />
</label>
</div>
{/* Deep Thinking Toggle */}
<button
onClick={() => setIsThinkingMode(!isThinkingMode)}
className={`p-2 rounded-lg transition-colors pb-3 ${isThinkingMode ? 'text-blue-600 bg-blue-100 dark:bg-blue-900/30' : 'text-gray-400 hover:bg-gray-200 dark:hover:bg-slate-700'}`}
title={t('action.toggle_think', settings.language)}
>
<Brain size={20} />
</button>
{/* AI Language Response Mode Selector */}
<div className="relative group flex items-center pb-3">
<Languages size={18} className="text-gray-400 absolute left-2 pointer-events-none" />
<select
value={settings.aiResponseLanguage}
onChange={(e) => setSettings({ ...settings, aiResponseLanguage: e.target.value as any })}
className="bg-transparent text-xs font-medium text-gray-500 hover:text-blue-600 outline-none cursor-pointer pl-8 appearance-none w-[110px]"
title={t('action.lang_mode', settings.language)}
>
<option value="system">{t('action.lang_system', settings.language)}</option>
<option value="input">{t('action.lang_input', settings.language)}</option>
</select>
</div>
<textarea
value={inputText}
@@ -1006,7 +1384,7 @@ export default function App() {
handleSend();
}
}}
placeholder={t('prompt.placeholder', settings.language)}
placeholder={t(`placeholder.${currentModule}`, settings.language)}
className="flex-1 bg-transparent border-none outline-none resize-none max-h-32 py-3 text-sm"
rows={1}
/>
@@ -1028,6 +1406,24 @@ export default function App() {
{renderHistorySidebar()}
</div>
{/* Share Modal */}
<Modal isOpen={isShareModalOpen} onClose={() => setIsShareModalOpen(false)} title={t('share.title', settings.language)}>
<div className="space-y-4">
<Button variant="secondary" onClick={handleCopySession} className="w-full justify-start h-12">
<Copy size={20} className="text-gray-500" />
<span className="flex-1 text-left">{t('share.copy', settings.language)}</span>
</Button>
<Button variant="secondary" onClick={handleDownloadText} className="w-full justify-start h-12">
<FileText size={20} className="text-gray-500" />
<span className="flex-1 text-left">{t('share.txt', settings.language)}</span>
</Button>
<Button variant="secondary" onClick={handleDownloadImage} className="w-full justify-start h-12">
<ImageIcon size={20} className="text-gray-500" />
<span className="flex-1 text-left">{t('share.img', settings.language)}</span>
</Button>
</div>
</Modal>
{/* Settings Modal */}
<Modal isOpen={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} title={t('settings.title', settings.language)}>
<div className="space-y-6">
@@ -1037,6 +1433,9 @@ export default function App() {
type="password"
value={settings.apiKey}
onChange={(e) => setSettings({...settings, apiKey: e.target.value})}
onBlur={() => {
if (settings.apiKey) showToast(t('success.apikey_updated', settings.language), 'success');
}}
className="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-slate-800 dark:border-slate-700 outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Expires monthly..."
/>

127
README.md
View File

@@ -1,20 +1,121 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# BitSage
# Run and deploy your AI Studio app
<p align="center">
<img src="public/icon.svg" alt="BitSage Logo" width="100" />
</p>
This contains everything you need to run your app locally.
BitSage is an intelligent AI companion designed specifically for learning Computer Science and Technology. It leverages advanced Gemini models to provide expert Q&A, deep reasoning, academic research, and multimedia generation capabilities.
View your app in AI Studio: https://ai.studio/apps/drive/1wH16LSTzg8m7RgPeCzYP99HiinOHmyij
---
## Run Locally
### 🌐 Language / 语言 / 言語
- [English](#english)
- [简体中文](#简体中文)
- [繁體中文](#繁體中文)
- [日本語](#日本語)
**Prerequisites:** Node.js
---
## <a id="english"></a>English
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`
### Features
1. **Specialized CS Domains**: Dedicated modules for Mathematics, Computer Principles, Software Engineering, Computer Graphics, Computer Networks, and Artificial Intelligence.
2. **Deep Thinking Mode**: A toggleable "Thinking" mode powered by Gemini's reasoning capabilities for complex problem solving.
3. **Academic Research**: Web-grounded search for finding up-to-date papers and technical documentation.
4. **Creative Studio**:
* **Vision Lab**: Generate diagrams or analyze code screenshots.
* **Video Studio**: Create concept videos using Veo.
* **Audio Lab**: Text-to-Speech and audio transcription.
5. **PWA Support**: Installable on PC and Mobile for a native app experience.
6. **Privacy Focused**: All API keys and chat history are stored locally in your browser.
### Getting Started
1. Open the application.
2. Click "Get Started" or the Settings icon.
3. Enter your Google Gemini API Key.
4. Select a module from the sidebar and start learning!
---
## <a id="简体中文"></a>简体中文
### 功能特性
1. **计算机科学细分领域**:包含数学基础、计算机组成原理、软件工程、图形学、网络和人工智能等专属模块。
2. **深度思考模式**:在对话框中可一键开启“深度思考”,利用 Gemini 的推理能力解决复杂算法或架构问题。
3. **学术搜索**:基于网络搜索的问答,适合查找最新的技术文档和学术论文。
4. **创意工作室**
* **视觉实验室**:生成技术架构图或分析代码截图。
* **视频工作室**:使用 Veo 模型生成概念演示视频。
* **音频实验室**:文字转语音及音频转录。
5. **PWA 支持**:支持在 PC 和手机端安装,提供原生应用般的体验。
6. **隐私保护**:所有的 API Key 和聊天记录仅保存在您的浏览器本地。
### 快速开始
1. 打开应用。
2. 点击“开始体验”或设置图标。
3. 输入您的 Google Gemini API Key。
4. 在左侧栏选择一个模块,开始您的学习之旅!
---
## <a id="繁體中文"></a>繁體中文
### 功能特性
1. **計算機科學細分領域**:包含數學基礎、計算機組成原理、軟體工程、圖形學、網路和人工智慧等專屬模組。
2. **深度思考模式**:在對話框中可一鍵開啟「深度思考」,利用 Gemini 的推理能力解決複雜演算法或架構問題。
3. **學術搜尋**:基於網路搜尋的問答,適合尋找最新的技術文件和學術論文。
4. **創意工作室**
* **視覺實驗室**:生成技術架構圖或分析程式碼截圖。
* **影片工作室**:使用 Veo 模型生成概念演示影片。
* **音訊實驗室**:文字轉語音及音訊轉錄。
5. **PWA 支援**:支援在 PC 和手機端安裝,提供原生應用般的體驗。
6. **隱私保護**:所有的 API Key 和聊天記錄僅保存在您的瀏覽器本地。
### 快速開始
1. 打開應用。
2. 點擊「開始體驗」或設定圖示。
3. 輸入您的 Google Gemini API Key。
4. 在左側欄選擇一個模組,開始您的學習之旅!
---
## <a id="日本語"></a>日本語
### 機能
1. **CS専門分野**: 数学基礎、計算機原理、ソフトウェア工学、CG、ネットワーク、AIラボなど、分野ごとの専用モジュール。
2. **深い思考モード**: 会話画面で「深い思考」をオンにすると、Geminiの推論能力を使って複雑な問題を解決できます。
3. **学術検索**: 最新の技術文書や論文を見つけるためのWeb検索機能。
4. **クリエイティブスタジオ**:
* **ビジョンラボ**: 図の生成やコードのスクリーンショット分析。
* **ビデオスタジオ**: Veoモデルを使用したコンセプトビデオの生成。
* **オーディオラボ**: テキスト読み上げと音声文字起こし。
5. **PWA対応**: PCやモバイルにインストールして、ネイティブアプリのように使用できます。
6. **プライバシー重視**: APIキーとチャット履歴はブラウザにローカル保存されます。
### 始め方
1. アプリを開きます。
2. 「始める」または設定アイコンをクリックします。
3. Google Gemini APIキーを入力します。
4. サイドバーからモジュールを選択して、学習を始めましょう!
---
## Tech Stack / 技术栈
* React 18 + TypeScript
* Vite
* Tailwind CSS
* Google GenAI SDK (Gemini 2.5/3.0 Models)
* Lucide React Icons
## License
MIT

View File

@@ -49,10 +49,10 @@
}
</script>
<!-- Direct Favicon Link -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232563eb' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z'/%3E%3Cpath d='M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z'/%3E%3C/svg%3E" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232563eb' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z'/%3E%3Cpath d='M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z'/%3E%3C/svg%3E" />
<!-- PWA Manifest -->
<link rel="manifest" href="data:application/json;base64,ewogICJuYW1lIjogIkJpdFNhZ2UgLSBDUyBMZWFybmluZyBDb21wYW5pb24iLAogICJzaG9ydF9uYW1lIjogIkJpdFNhZ2UiLAogICJzdGFydF91cmwiOiAiLyIsCiAgImRpc3BsYXkiOiAiYnJvd3NlciIsCiAgImJhY2tncm91bmRfY29xvciI6ICIjZmZmZmZmIiwKICAidGhlbWVfY29xvciI6ICIjMjU2M2ViIiwKICAiaWNvbnMiOiBbCiAgICB7CiAgICAgICJzcmMiOiAiZGF0YTppbWFnZS9zdmcreG1sLCUzQ3N2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPSclMjMyNTYzZWInIHN0cm9rZS13aWR0aD0nMicgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJ%2BJTNDcGF0aCBkPSdNOS41IDJBMi41IDIuNSAwIDAgMSAxMiA0LjV2MTVhMi41IDIuNSAwIDAgMS00Ljk2LjQ0IDIuNSAyLjUgMCAwIDEtMi45Ni0zLjA4IDMgMyAwIDAgMS0uMzQtNS41OCAyLjUgMi41IDAgMCAxIDEuMzItNC4yNCAyLjUgMi41IDAgMCAxIDEuMzItNC4yNCAyLjUgMi41IDAgMCAxIDEuOTgtM0EyLjUgMi41IDAgMCAxIDkuNSAyWicvJTNFJTNDcGF0aCBkPSdNMTQuNSAyQTIuNSAyLjUgMCAwIDAgMTIgNC41djE1YTIuNSAyLjUgMCAwIDAgNC45Ni40NCAyLjUgMi41IDAgMCAwIDIuOTYtMy4wOCAzIDMgMCAwIDAgLjM0LTUuNTggMi41IDIuNSAwIDAgMC0xLjMyLTQuMjQgMi41IDIuNSAwIDAgMC0xLjk4LTNBMi41IDIuNSAwIDAgMCAxNC41IDJaJy8lM0UlM0Mvc3ZnJTNFLg0KIiwKICAgICAgInR5cGUiOiAiaW1hZ2Uvc3ZnK3htbCIsCiAgICAgICJzaXplcyI6ICIxOTJ4MTkyIDUxMng1MTIiCiAgICB9CiAgXQp9" />
<!-- PWA Manifest (display: standalone is critical for installability) -->
<link rel="manifest" href='data:application/json;charset=utf-8,{"name":"BitSage - CS Learning Companion","short_name":"BitSage","start_url":"/","display":"standalone","background_color":"#ffffff","theme_color":"#2563eb","icons":[{"src":"data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27 fill=%27none%27 stroke=%27%232563eb%27 stroke-width=%272%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27%3E%3Cpath d=%27M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z%27/%3E%3Cpath d=%27M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z%27/%3E%3C/svg%3E","type":"image/svg+xml","sizes":"192x192 512x512"}]}' />
<style>
/* Custom scrollbar for webkit */
@@ -118,5 +118,14 @@
<body class="bg-gray-50 dark:bg-slate-900 text-gray-900 dark:text-gray-100 h-[100dvh] overflow-hidden">
<div id="root" class="h-full"></div>
<script type="module" src="/index.tsx"></script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js').catch(err => {
console.error('SW registration failed:', err);
});
});
}
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

View File

@@ -34,26 +34,16 @@ export class GeminiService {
prompt: string,
module: AppModule,
history: {role: string, parts: any[]}[],
media?: { data: string, mimeType: string }[]
language: string,
media?: { data: string, mimeType: string }[],
enableThinking: boolean = false,
responseLangMode: 'system' | 'input' = 'system'
) {
const ai = this.getClient();
let model = MODEL_CHAT_PRO;
let config: any = {};
switch (module) {
case AppModule.TUTOR:
// Use fast model for simple queries if possible, but user wants options.
// We default to Pro for quality in Tutor, but could swap.
// Requirement says: "Use Pro for complex tasks and Flash or Flash-Lite for tasks that should happen fast."
// We'll stick to Pro for general "Tutor" advice as it implies teaching.
model = MODEL_CHAT_PRO;
break;
case AppModule.THINKER:
model = MODEL_CHAT_PRO;
config.thinkingConfig = { thinkingBudget: 32768 };
// config.maxOutputTokens should NOT be set when using max thinking budget if not careful,
// but recommendation says: "Avoid setting this if not required".
break;
case AppModule.RESEARCH:
model = MODEL_RESEARCH;
config.tools = [{ googleSearch: {} }];
@@ -67,18 +57,67 @@ export class GeminiService {
case AppModule.AUDIO:
model = MODEL_AUDIO_TRANS; // For transcription/analysis
break;
default:
// Math, Principles, SoftEng, Graphics, Network, AI_LAB
// Use Pro for these complex domains
model = MODEL_CHAT_PRO;
if (enableThinking) {
config.thinkingConfig = { thinkingBudget: 32768 };
}
break;
}
// Build contents
// Chat history + new prompt
// Note: @google/genai chat history format differs slightly from simple array.
// For simplicity in this single-file service, we'll use `generateContent` with a constructed history
// OR just use `chats.create`. `chats.create` is better for history.
// Construct System Instruction based on module and language
const langMap: Record<string, string> = {
'en': 'English',
'zh-CN': 'Simplified Chinese',
'zh-TW': 'Traditional Chinese',
'ja': 'Japanese'
};
let languageInstruction = '';
if (responseLangMode === 'input') {
languageInstruction = `- Detect the language of the user's input and reply exclusively in that language.`;
} else {
const targetLang = langMap[language] || 'English';
languageInstruction = `- Please provide your response in ${targetLang}.
- However, if the user explicitly asks a question in a different language, you should adapt and reply in the language of the question to ensure effective communication.`;
}
const contextMap: Record<string, string> = {
[AppModule.MATH]: 'Discrete Mathematics, Calculus, Linear Algebra, and Logic for Computer Science.',
[AppModule.THEORY]: 'Theory of Computation, Automata, Complexity Theory, and Computability.',
[AppModule.PRINCIPLES]: 'Computer Architecture, Organization, Digital Logic, and Assembly.',
[AppModule.SOFT_ENG]: 'Software Engineering, Design Patterns, Architecture, and DevOps.',
[AppModule.GRAPHICS]: 'Computer Graphics, Rendering, WebGL, and Linear Algebra for Graphics.',
[AppModule.NETWORK]: 'Computer Networks, Protocols (TCP/IP), Security, and Distributed Systems.',
[AppModule.AI_LAB]: 'Artificial Intelligence, Machine Learning, Deep Learning, and Neural Networks.',
[AppModule.RESEARCH]: 'Academic Research, Paper Search, and Citations.',
[AppModule.VISION]: 'Computer Vision, Image Analysis, and Generation.',
[AppModule.STUDIO]: 'Video Generation and Multimedia Processing.',
[AppModule.AUDIO]: 'Audio Processing, Speech Synthesis, and Transcription.',
[AppModule.SQL]: 'SQL Database Administration and Query Optimization.'
};
const domain = contextMap[module] || 'Computer Science';
const systemInstruction = `You are BitSage, an expert tutor and companion specializing in ${domain}.
Primary Goal: Help the user learn, understand, and explore concepts in this domain.
Language Preference:
${languageInstruction}
Style:
- Be precise, educational, and helpful.
- Use code blocks for code snippets.
- Use Markdown for formatting.
${enableThinking ? '- Thinking process is enabled. Use it to break down complex problems.' : ''}
`;
config.systemInstruction = systemInstruction;
// Convert generic history to SDK format
// The SDK `sendMessage` handles the current turn.
// We need to initialize history first.
const sdkHistory = history.map(h => ({
role: h.role,
parts: h.parts
@@ -144,12 +183,6 @@ export class GeminiService {
}
}
// Creating a NEW instance for Veo calls to ensure latest key if using the selection dialog flow?
// The prompt says "Create a new GoogleGenAI instance right before making an API call...".
// Since we are using our own stored key primarily, we stick to `this.ai`.
// If the user used the dialog, that key isn't automatically in our `this.apiKey`.
// We will assume `this.apiKey` (user entered) is the paid key required.
let operation = await ai.models.generateVideos({
model: MODEL_VIDEO,
prompt: prompt,
@@ -207,4 +240,29 @@ export class GeminiService {
if (!base64Audio) throw new Error("No audio generated");
return base64Audio;
}
async toolsSql(text: string, type: 'format' | 'convert' | 'custom', target?: string, instruction?: string) {
const ai = this.getClient();
let prompt = "";
if (type === 'custom') {
if (!text.trim()) {
// Generation mode
prompt = `You are a SQL expert. The user wants you to generate SQL code based on this request: "${instruction}".\n\nReturn ONLY the generated SQL code. Do not wrap in markdown backticks unless asked for explanation.`;
} else {
// Manipulation mode
prompt = `You are a SQL expert. The user wants you to perform the following action on the SQL code provided: "${instruction}".\n\nReturn ONLY the processed SQL code (or the answer if it's an analysis). Do not wrap in markdown backticks unless asked for explanation.\n\nSQL Code:\n${text}`;
}
} else if (type === 'format') {
prompt = `You are a SQL formatter. Format the following SQL code to be readable, with proper indentation and uppercased keywords. Return ONLY the formatted SQL code, no markdown backticks.\n\n${text}`;
} else if (type === 'convert') {
prompt = `You are a SQL converter. Convert the following SQL code to ${target} dialect. Return ONLY the converted SQL code, no markdown backticks.\n\n${text}`;
}
const response = await ai.models.generateContent({
model: MODEL_CHAT_PRO,
contents: [{ parts: [{ text: prompt }] }]
});
return response.text?.trim() || "";
}
}

View File

@@ -4,22 +4,57 @@ const translations: Record<Language, Record<string, string>> = {
'en': {
'app.name': 'BitSage',
'menu.home': 'Home',
'group.learning': 'Learning & Research',
'group.cs': 'Computer Science Domains',
'group.tools': 'Research Tools',
'group.creation': 'Creative Studio',
'module.tutor': 'CS Tutor',
'module.thinker': 'Deep Thinker',
'module.research': 'Research',
'module.math': 'Mathematics',
'module.theory': 'Theory of Computation',
'module.principles': 'Comp. Architecture',
'module.soft_eng': 'Software Eng.',
'module.graphics': 'Comp. Graphics',
'module.network': 'Comp. Network',
'module.ai_lab': 'AI Laboratory',
'module.research': 'Academic Search',
'module.sql': 'SQL Toolbox',
'module.vision': 'Vision Lab',
'module.studio': 'Video Studio',
'module.audio': 'Audio Lab',
'desc.tutor': 'Expert Q&A and coding help',
'desc.thinker': 'Deep reasoning for complex problems',
'desc.math': 'Discrete math, calculus & logic',
'desc.theory': 'Automata, computability & complexity',
'desc.principles': 'ISA, pipelining & memory hierarchy',
'desc.soft_eng': 'Design patterns & DevOps',
'desc.graphics': 'Rendering, OpenGL & WebGL',
'desc.network': 'Protocols, security & distributed systems',
'desc.ai_lab': 'ML, DL & Neural Networks',
'desc.research': 'Web-grounded academic research',
'desc.sql': 'Format, convert & manipulate SQL',
'desc.vision': 'Image analysis and generation',
'desc.studio': 'AI video generation studio',
'desc.audio': 'Speech-to-text and Text-to-speech',
// Module specific greetings
'hello.math': 'Mathematics Workspace',
'hello.theory': 'Theoretical Computer Science',
'hello.principles': 'Computer Architecture',
'hello.soft_eng': 'Software Engineering',
'hello.graphics': 'Graphics & Rendering',
'hello.network': 'Network Engineering',
'hello.ai_lab': 'AI & Machine Learning',
'hello.research': 'Academic Research',
// Module specific placeholders
'placeholder.math': 'Ask about discrete math, linear algebra, or proofs...',
'placeholder.theory': 'Ask about Turing machines, P vs NP, or automata...',
'placeholder.principles': 'Ask about RISC-V, cache coherence, or digital logic...',
'placeholder.soft_eng': 'Ask about design patterns, agile, or system architecture...',
'placeholder.graphics': 'Ask about ray tracing, shaders, or WebGL...',
'placeholder.network': 'Ask about TCP/IP, OSI model, or network security...',
'placeholder.ai_lab': 'Ask about transformers, backpropagation, or PyTorch...',
'placeholder.research': 'Search for papers, technical docs, or citations...',
'welcome.title': 'Welcome to BitSage',
'welcome.subtitle': 'Your AI companion for Computer Science & Technology.',
'welcome.setup': 'Please enter your Gemini API Key to get started.',
@@ -42,11 +77,15 @@ const translations: Record<Language, Record<string, string>> = {
'action.generate': 'Generate',
'action.new_chat': 'New Chat',
'action.install': 'Install App',
'action.toggle_think': 'Deep Thinking Mode',
'action.lang_mode': 'AI Language',
'action.lang_system': 'System Lang',
'action.lang_input': 'Follow Input',
'history.title': 'History',
'history.empty': 'No history for this module.',
'prompt.placeholder': 'Ask me anything about CS...',
'prompt.placeholder': 'Ask me anything about this domain...',
'status.thinking': 'Thinking deeply...',
'status.generating': 'Generating...',
'status.recording': 'Recording...',
@@ -71,7 +110,6 @@ const translations: Record<Language, Record<string, string>> = {
'audio.prompt': 'Enter text to generate speech...',
'btn.start': 'Get Started',
// New Creative Guide Strings
'guide.vision.title': 'Vision Lab',
'guide.vision.desc': 'Generate high-quality images from text or analyze uploaded images for code and diagrams.',
'guide.vision.tip1': 'Describe the scene, style, and lighting in detail.',
@@ -90,26 +128,91 @@ const translations: Record<Language, Record<string, string>> = {
'ui.workbench': 'Workbench',
'ui.gallery': 'Results',
'ui.config': 'Configuration',
// Share & Metadata
'share.title': 'Share Session',
'share.copy': 'Copy Full Chat',
'share.txt': 'Download Text',
'share.img': 'Download Image',
'success.copy': 'Copied to clipboard!',
'role.user': 'You',
'role.model': 'Gemini',
'role.veo': 'Veo',
// SQL Tool
'sql.format': 'Format SQL',
'sql.convert': 'Convert Dialect',
'sql.replace': 'Anonymize AS',
'sql.minify': 'Minify (Single Line)',
'sql.input': 'Input SQL',
'sql.output': 'Output SQL',
'sql.target_db': 'Target Database',
'sql.placeholder': 'Paste your SQL here...',
'sql.processing': 'Processing...',
'sql.custom': 'Other / AI Assist',
'sql.custom_prompt': 'Describe operation or generation needed...',
'sql.run': 'Run',
// New Toasts
'warning.no_sql': 'Please enter SQL code first.',
'success.apikey_updated': 'API Key configuration updated.',
'success.data_imported': 'Data imported successfully.',
'error.invalid_file': 'Invalid file format.',
'error.screenshot': 'Failed to generate screenshot.',
'error.tts': 'TTS Generation failed.',
},
'zh-CN': {
'app.name': '比特智者',
'menu.home': '首页',
'group.learning': '学习与研究',
'group.cs': '计算机科学领域',
'group.tools': '研究工具',
'group.creation': '创意工作室',
'module.tutor': 'CS 导师',
'module.thinker': '深度思考',
'module.math': '数学基础',
'module.theory': '计算理论',
'module.principles': '计算机体系结构',
'module.soft_eng': '软件工程',
'module.graphics': '计算机图形学',
'module.network': '计算机网络',
'module.ai_lab': '人工智能',
'module.research': '学术搜索',
'module.sql': 'SQL 工具箱',
'module.vision': '视觉实验室',
'module.studio': '视频工作室',
'module.audio': '音频实验室',
'desc.tutor': '专家级问答与代码辅助',
'desc.thinker': '针对复杂问题的深度推理',
'desc.math': '离散数学、微积分与逻辑',
'desc.theory': '自动机、可计算性与复杂性',
'desc.principles': '指令集、流水线与存储层次',
'desc.soft_eng': '设计模式与 DevOps',
'desc.graphics': '渲染、OpenGL 与 WebGL',
'desc.network': '协议、安全与分布式系统',
'desc.ai_lab': '机器学习、深度学习与神经网络',
'desc.research': '基于网络的学术研究',
'desc.sql': 'SQL 格式化、转换与处理',
'desc.vision': '图像分析与生成',
'desc.studio': 'AI 视频生成工作室',
'desc.audio': '语音转文字与文字转语音',
'hello.math': 'CS 数学基础',
'hello.theory': '计算机理论基础',
'hello.principles': '计算机体系结构',
'hello.soft_eng': '软件工程与架构',
'hello.graphics': '图形学与渲染',
'hello.network': '网络工程与安全',
'hello.ai_lab': 'AI 与深度学习',
'hello.research': '学术研究助手',
'placeholder.math': '询问关于离散数学、线性代数或证明的问题...',
'placeholder.theory': '询问关于图灵机、P vs NP 或有限自动机...',
'placeholder.principles': '询问关于RISC-V、缓存一致性或数字逻辑...',
'placeholder.soft_eng': '询问关于设计模式、敏捷开发或系统架构...',
'placeholder.graphics': '询问关于光线追踪、着色器或WebGL...',
'placeholder.network': '询问关于TCP/IP、OSI模型或网络安全...',
'placeholder.ai_lab': '询问关于Transformer、反向传播或PyTorch...',
'placeholder.research': '搜索论文、技术文档或引用...',
'welcome.title': '欢迎使用比特智者',
'welcome.subtitle': '您的计算机科学与技术学习 AI 助手。',
'welcome.setup': '请输入您的 Gemini API Key 以开始使用。',
@@ -132,11 +235,15 @@ const translations: Record<Language, Record<string, string>> = {
'action.generate': '生成',
'action.new_chat': '新会话',
'action.install': '安装应用',
'action.toggle_think': '深度思考模式',
'action.lang_mode': 'AI 语言',
'action.lang_system': '跟随系统',
'action.lang_input': '跟随输入',
'history.title': '历史记录',
'history.empty': '暂无该模块的历史记录',
'prompt.placeholder': '问我任何关于计算机科学的问题...',
'prompt.placeholder': '在此领域提问...',
'status.thinking': '深度思考中...',
'status.generating': '生成中...',
'status.recording': '录音中...',
@@ -179,26 +286,90 @@ const translations: Record<Language, Record<string, string>> = {
'ui.workbench': '工作台',
'ui.gallery': '生成结果',
'ui.config': '参数配置',
// Share & Metadata
'share.title': '分享会话',
'share.copy': '复制完整对话',
'share.txt': '下载文本 (.txt)',
'share.img': '下载长图',
'success.copy': '已复制到剪贴板!',
'role.user': '你',
'role.model': 'Gemini',
'role.veo': 'Veo',
// SQL Tool
'sql.format': '格式化 SQL',
'sql.convert': '方言转换',
'sql.replace': 'AS 序号替换',
'sql.minify': '单行压缩',
'sql.input': '输入 SQL',
'sql.output': '输出 SQL',
'sql.target_db': '目标数据库',
'sql.placeholder': '在此粘贴您的 SQL...',
'sql.processing': '处理中...',
'sql.custom': '其他 / AI 助手',
'sql.custom_prompt': '描述您的需求(例如“提取表名”或“生成建表语句”)',
'sql.run': '执行',
// New Toasts
'warning.no_sql': '请先输入 SQL 代码。',
'success.apikey_updated': 'API Key 配置已更新。',
'success.data_imported': '数据导入成功。',
'error.invalid_file': '无效的文件格式。',
'error.screenshot': '截图生成失败。',
'error.tts': '语音生成失败。',
},
'ja': {
'app.name': 'BitSage',
'menu.home': 'ホーム',
'group.learning': '学習と研究',
'group.cs': 'コンピュータサイエンス分野',
'group.tools': '研究ツール',
'group.creation': 'クリエイティブスタジオ',
'module.tutor': 'CS 講師',
'module.thinker': '深い思考',
'module.research': '研究',
'module.math': '数学基礎',
'module.theory': '計算理論',
'module.principles': 'コンピュータ・アーキテクチャ',
'module.soft_eng': 'ソフトウェア工学',
'module.graphics': 'CG・グラフィックス',
'module.network': 'コンピュータネットワーク',
'module.ai_lab': '人工知能ラボ',
'module.research': '学術検索',
'module.sql': 'SQLツール',
'module.vision': 'ビジョンラボ',
'module.studio': 'ビデオスタジオ',
'module.audio': 'オーディオラボ',
'desc.tutor': '専門的なQ&Aとコーディング支援',
'desc.thinker': '複雑な問題に対する深い推論',
'desc.math': '離散数学、微積分、論理学',
'desc.theory': 'オートマトン、計算可能性、複雑性',
'desc.principles': '命令セット、パイプライン、メモリ階層',
'desc.soft_eng': 'デザインパターン、DevOps',
'desc.graphics': 'レンダリング、OpenGL、WebGL',
'desc.network': 'プロトコル、セキュリティ、分散システム',
'desc.ai_lab': '機械学習、深層学習、ニューラルネットワーク',
'desc.research': 'Webに基づく学術研究',
'desc.sql': 'SQLのフォーマット、変換、操作',
'desc.vision': '画像分析と生成',
'desc.studio': 'AIビデオ生成スタジオ',
'desc.audio': '音声認識と音声合成',
'hello.math': 'CS 数学',
'hello.theory': '計算機科学の理論',
'hello.principles': 'コンピュータ・アーキテクチャ',
'hello.soft_eng': 'CG & レンダリング',
'hello.graphics': 'CG & レンダリング',
'hello.network': 'ネットワーク工学',
'hello.ai_lab': 'AI & 機械学習',
'hello.research': '学術研究アシスタント',
'placeholder.math': '離散数学、線形代数、証明について質問する...',
'placeholder.theory': 'チューリングマシン、P vs NP、有限オートマトンについて質問する...',
'placeholder.principles': 'RISC-V、キャッシュコヒーレンス、デジタル論理について質問する...',
'placeholder.soft_eng': 'デザインパターン、アジャイル、システムアーキテクチャについて質問する...',
'placeholder.graphics': 'レイトレーシング、シェーダー、WebGLについて質問する...',
'placeholder.network': 'TCP/IP、OSIモデル、ネットワークセキュリティについて質問する...',
'placeholder.ai_lab': 'Transformer、バックプロパゲーション、PyTorchについて質問する...',
'placeholder.research': '論文、技術文書、引用を検索...',
'welcome.title': 'BitSageへようこそ',
'welcome.subtitle': 'コンピュータサイエンス学習のためのAIパートナー。',
'welcome.setup': '開始するにはGemini APIキーを入力してください。',
@@ -221,11 +392,15 @@ const translations: Record<Language, Record<string, string>> = {
'action.generate': '生成',
'action.new_chat': '新しいチャット',
'action.install': 'アプリをインストール',
'action.toggle_think': '深い思考モード',
'action.lang_mode': 'AI 言語',
'action.lang_system': 'システム言語',
'action.lang_input': '入力に合わせる',
'history.title': '履歴',
'history.empty': 'このモジュールの履歴はありません。',
'prompt.placeholder': 'CSについて何でも聞いてください...',
'prompt.placeholder': 'この分野について質問してください...',
'status.thinking': '深く考えています...',
'status.generating': '生成中...',
'status.recording': '録音中...',
@@ -268,26 +443,90 @@ const translations: Record<Language, Record<string, string>> = {
'ui.workbench': 'ワークベンチ',
'ui.gallery': '生成結果',
'ui.config': '設定',
// Share & Metadata
'share.title': 'セッションを共有',
'share.copy': '会話をコピー',
'share.txt': 'テキストをダウンロード',
'share.img': '長い画像を保存',
'success.copy': 'クリップボードにコピーしました!',
'role.user': 'あなた',
'role.model': 'Gemini',
'role.veo': 'Veo',
// SQL Tool
'sql.format': 'SQL整形',
'sql.convert': '方言変換',
'sql.replace': 'AS番号置換',
'sql.minify': '一行化',
'sql.input': '入力SQL',
'sql.output': '出力SQL',
'sql.target_db': 'ターゲットDB',
'sql.placeholder': 'ここにSQLを貼り付けてください...',
'sql.processing': '処理中...',
'sql.custom': 'その他 / AI アシスト',
'sql.custom_prompt': '操作を説明してください(例:「テーブル名を抽出」)',
'sql.run': '実行',
// New Toasts
'warning.no_sql': '先にSQLコードを入力してください。',
'success.apikey_updated': 'APIキーの設定が更新されました。',
'success.data_imported': 'データのインポートが完了しました。',
'error.invalid_file': '無効なファイル形式です。',
'error.screenshot': 'スクリーンショットの生成に失敗しました。',
'error.tts': '音声生成に失敗しました。',
},
'zh-TW': {
'app.name': '比特智者',
'menu.home': '首頁',
'group.learning': '學習與研究',
'group.cs': '計算機科學領域',
'group.tools': '研究工具',
'group.creation': '創意工作室',
'module.tutor': 'CS 導師',
'module.thinker': '深度思考',
'module.math': '數學基礎',
'module.theory': '計算理論',
'module.principles': '電腦體系結構',
'module.soft_eng': '軟體工程',
'module.graphics': '計算機圖形學',
'module.network': '計算機網路',
'module.ai_lab': '人工智慧',
'module.research': '學術搜尋',
'module.sql': 'SQL 工具箱',
'module.vision': '視覺實驗室',
'module.studio': '影片工作室',
'module.audio': '音訊實驗室',
'desc.tutor': '專家級問答與程式碼輔助',
'desc.thinker': '針對複雜問題的深度推理',
'desc.math': '離散數學、微積分與邏輯',
'desc.theory': '自動機、可計算性與複雜性',
'desc.principles': '指令集、管線化與儲存層次',
'desc.soft_eng': '設計模式與 DevOps',
'desc.graphics': '渲染、OpenGL 與 WebGL',
'desc.network': '通訊協定、資安與分散式系統',
'desc.ai_lab': '機器學習、深度學習與神經網路',
'desc.research': '基於網路的學術研究',
'desc.sql': 'SQL 格式化、轉換與處理',
'desc.vision': '圖像分析與生成',
'desc.studio': 'AI 影片生成工作室',
'desc.audio': '語音轉文字與文字轉語音',
'hello.math': 'CS 數學基礎',
'hello.theory': '電腦理論基礎',
'hello.principles': '電腦體系結構',
'hello.soft_eng': '軟體工程與架構',
'hello.graphics': '圖形學與渲染',
'hello.network': '網路工程與資安',
'hello.ai_lab': 'AI 與深度學習',
'hello.research': '學術研究助手',
'placeholder.math': '詢問關於離散數學、線性代數或證明的問題...',
'placeholder.theory': '詢問關於圖靈機、P vs NP 或有限自動機...',
'placeholder.principles': '詢問關於RISC-V、快取一致性或數位邏輯...',
'placeholder.soft_eng': '詢問關於設計模式、敏捷開發或系統架構...',
'placeholder.graphics': '詢問關於光線追蹤、著色器或WebGL...',
'placeholder.network': '詢問關於TCP/IP、OSI模型或網路安全...',
'placeholder.ai_lab': '詢問關於Transformer、反向傳播或PyTorch...',
'placeholder.research': '搜尋論文、技術文件或引用...',
'welcome.title': '歡迎使用比特智者',
'welcome.subtitle': '您的計算機科學與技術學習 AI 助手。',
'welcome.setup': '請輸入您的 Gemini API Key 以開始使用。',
@@ -310,11 +549,15 @@ const translations: Record<Language, Record<string, string>> = {
'action.generate': '生成',
'action.new_chat': '新對話',
'action.install': '安裝應用',
'action.toggle_think': '深度思考模式',
'action.lang_mode': 'AI 語言',
'action.lang_system': '跟隨系統',
'action.lang_input': '跟隨輸入',
'history.title': '歷史記錄',
'history.empty': '暫無該模組的歷史記錄',
'prompt.placeholder': '問我任何關於計算機科學的問題...',
'prompt.placeholder': '在此領域提問...',
'status.thinking': '深度思考中...',
'status.generating': '生成中...',
'status.recording': '錄音中...',
@@ -357,6 +600,38 @@ const translations: Record<Language, Record<string, string>> = {
'ui.workbench': '工作台',
'ui.gallery': '生成結果',
'ui.config': '參數配置',
// Share & Metadata
'share.title': '分享會話',
'share.copy': '複製完整對話',
'share.txt': '下載文字 (.txt)',
'share.img': '下載長圖',
'success.copy': '已複製到剪貼簿!',
'role.user': '你',
'role.model': 'Gemini',
'role.veo': 'Veo',
// SQL Tool
'sql.format': '格式化 SQL',
'sql.convert': '方言轉換',
'sql.replace': 'AS 序號替換',
'sql.minify': '單行壓縮',
'sql.input': '輸入 SQL',
'sql.output': '輸出 SQL',
'sql.target_db': '目標資料庫',
'sql.placeholder': '在此貼上您的 SQL...',
'sql.processing': '處理中...',
'sql.custom': '其他 / AI 助手',
'sql.custom_prompt': '描述您的需求(例如「提取表名」或「生成建表語句」)',
'sql.run': '執行',
// New Toasts
'warning.no_sql': '請先輸入 SQL 程式碼。',
'success.apikey_updated': 'API Key 設定已更新。',
'success.data_imported': '資料匯入成功。',
'error.invalid_file': '無效的檔案格式。',
'error.screenshot': '截圖生成失敗。',
'error.tts': '語音生成失敗。',
}
};

31
sw.js Normal file
View File

@@ -0,0 +1,31 @@
// BitSage Service Worker
const CACHE_NAME = 'bitsage-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/index.tsx'
];
self.addEventListener('install', (event) => {
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
// Best effort caching
return cache.addAll(ASSETS_TO_CACHE).catch(() => {});
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', (event) => {
// Simple pass-through fetch handler to satisfy PWA requirements
// In a full production app, you might want more robust offline caching
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});

View File

@@ -1,7 +1,18 @@
export enum AppModule {
TUTOR = 'tutor', // Q&A
THINKER = 'thinker', // Deep Thinking
// CS Domains
MATH = 'math', // Mathematics
THEORY = 'theory', // Theory of Computation
PRINCIPLES = 'principles', // Computer Architecture (formerly Principles)
SOFT_ENG = 'soft_eng', // Software Engineering
GRAPHICS = 'graphics', // Computer Graphics
NETWORK = 'network', // Computer Networks
AI_LAB = 'ai_lab', // Artificial Intelligence
// Tools
RESEARCH = 'research', // Search Grounding
SQL = 'sql', // SQL Tools
// Creative
VISION = 'vision', // Image Gen & Analysis
STUDIO = 'studio', // Video Gen & Analysis
AUDIO = 'audio' // TTS & Transcribe
@@ -40,6 +51,7 @@ export interface AppSettings {
language: 'en' | 'ja' | 'zh-CN' | 'zh-TW';
theme: 'light' | 'dark' | 'system';
hasCompletedOnboarding: boolean;
aiResponseLanguage: 'system' | 'input'; // 'system' = use App Language, 'input' = Match User Input
}
export interface VeoConfig {