Compare commits
1 Commits
v0.2.0_202
...
v0.3.0_202
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc88f7d6a |
445
App.tsx
445
App.tsx
@@ -5,7 +5,8 @@ import {
|
||||
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
|
||||
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';
|
||||
@@ -21,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",
|
||||
@@ -47,6 +48,13 @@ 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 = [
|
||||
{
|
||||
@@ -65,6 +73,7 @@ const SIDEBAR_GROUPS = [
|
||||
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' },
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -77,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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -104,9 +126,13 @@ 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);
|
||||
|
||||
@@ -120,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);
|
||||
|
||||
@@ -145,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(() => {
|
||||
@@ -174,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);
|
||||
@@ -253,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);
|
||||
@@ -277,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;
|
||||
}
|
||||
@@ -293,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(),
|
||||
@@ -332,7 +537,7 @@ export default function App() {
|
||||
role: MessageRole.MODEL,
|
||||
timestamp: Date.now(),
|
||||
text: '',
|
||||
isThinking: isThinkingMode && !isCreativeModule(currentModule)
|
||||
isThinking: isThinkingMode && !isCustomViewModule(currentModule)
|
||||
}]);
|
||||
|
||||
try {
|
||||
@@ -362,7 +567,15 @@ export default function App() {
|
||||
});
|
||||
|
||||
// Pass isThinkingMode to the service
|
||||
const stream = await geminiRef.current!.generateText(inputText, currentModule, historyParts, attachments, isThinkingMode);
|
||||
const stream = await geminiRef.current!.generateText(
|
||||
inputText,
|
||||
currentModule,
|
||||
historyParts,
|
||||
settings.language,
|
||||
attachments,
|
||||
isThinkingMode,
|
||||
settings.aiResponseLanguage
|
||||
);
|
||||
|
||||
let fullText = '';
|
||||
let sources: any[] = [];
|
||||
@@ -424,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);
|
||||
}
|
||||
@@ -432,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 ---
|
||||
|
||||
@@ -510,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);
|
||||
|
||||
@@ -629,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" />
|
||||
@@ -667,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>
|
||||
@@ -921,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" />}
|
||||
|
||||
@@ -940,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>
|
||||
@@ -963,7 +1292,8 @@ 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">
|
||||
@@ -987,7 +1317,7 @@ export default function App() {
|
||||
</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>
|
||||
@@ -998,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">
|
||||
|
||||
@@ -1030,6 +1360,20 @@ export default function App() {
|
||||
>
|
||||
<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}
|
||||
@@ -1062,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">
|
||||
@@ -1071,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..."
|
||||
/>
|
||||
|
||||
15
index.html
15
index.html
@@ -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>
|
||||
BIN
releases/HTY1024-APP-CKG-0.3.0_20251226.zip
Normal file
BIN
releases/HTY1024-APP-CKG-0.3.0_20251226.zip
Normal file
Binary file not shown.
@@ -34,8 +34,10 @@ export class GeminiService {
|
||||
prompt: string,
|
||||
module: AppModule,
|
||||
history: {role: string, parts: any[]}[],
|
||||
language: string,
|
||||
media?: { data: string, mimeType: string }[],
|
||||
enableThinking: boolean = false
|
||||
enableThinking: boolean = false,
|
||||
responseLangMode: 'system' | 'input' = 'system'
|
||||
) {
|
||||
const ai = this.getClient();
|
||||
let model = MODEL_CHAT_PRO;
|
||||
@@ -65,16 +67,57 @@ export class GeminiService {
|
||||
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
|
||||
@@ -140,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,
|
||||
@@ -203,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() || "";
|
||||
}
|
||||
}
|
||||
148
services/i18n.ts
148
services/i18n.ts
@@ -17,6 +17,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'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',
|
||||
@@ -29,6 +30,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'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',
|
||||
@@ -76,6 +78,9 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'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.',
|
||||
@@ -123,6 +128,38 @@ 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': '比特智者',
|
||||
@@ -140,6 +177,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'module.network': '计算机网络',
|
||||
'module.ai_lab': '人工智能',
|
||||
'module.research': '学术搜索',
|
||||
'module.sql': 'SQL 工具箱',
|
||||
'module.vision': '视觉实验室',
|
||||
'module.studio': '视频工作室',
|
||||
'module.audio': '音频实验室',
|
||||
@@ -152,6 +190,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'desc.network': '协议、安全与分布式系统',
|
||||
'desc.ai_lab': '机器学习、深度学习与神经网络',
|
||||
'desc.research': '基于网络的学术研究',
|
||||
'desc.sql': 'SQL 格式化、转换与处理',
|
||||
'desc.vision': '图像分析与生成',
|
||||
'desc.studio': 'AI 视频生成工作室',
|
||||
'desc.audio': '语音转文字与文字转语音',
|
||||
@@ -197,6 +236,9 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'action.new_chat': '新会话',
|
||||
'action.install': '安装应用',
|
||||
'action.toggle_think': '深度思考模式',
|
||||
'action.lang_mode': 'AI 语言',
|
||||
'action.lang_system': '跟随系统',
|
||||
'action.lang_input': '跟随输入',
|
||||
|
||||
'history.title': '历史记录',
|
||||
'history.empty': '暂无该模块的历史记录',
|
||||
@@ -244,6 +286,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': '语音生成失败。',
|
||||
},
|
||||
'ja': {
|
||||
'app.name': 'BitSage',
|
||||
@@ -260,6 +334,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'module.network': 'コンピュータネットワーク',
|
||||
'module.ai_lab': '人工知能ラボ',
|
||||
'module.research': '学術検索',
|
||||
'module.sql': 'SQLツール',
|
||||
'module.vision': 'ビジョンラボ',
|
||||
'module.studio': 'ビデオスタジオ',
|
||||
'module.audio': 'オーディオラボ',
|
||||
@@ -272,6 +347,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'desc.network': 'プロトコル、セキュリティ、分散システム',
|
||||
'desc.ai_lab': '機械学習、深層学習、ニューラルネットワーク',
|
||||
'desc.research': 'Webに基づく学術研究',
|
||||
'desc.sql': 'SQLのフォーマット、変換、操作',
|
||||
'desc.vision': '画像分析と生成',
|
||||
'desc.studio': 'AIビデオ生成スタジオ',
|
||||
'desc.audio': '音声認識と音声合成',
|
||||
@@ -317,6 +393,9 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'action.new_chat': '新しいチャット',
|
||||
'action.install': 'アプリをインストール',
|
||||
'action.toggle_think': '深い思考モード',
|
||||
'action.lang_mode': 'AI 言語',
|
||||
'action.lang_system': 'システム言語',
|
||||
'action.lang_input': '入力に合わせる',
|
||||
|
||||
'history.title': '履歴',
|
||||
'history.empty': 'このモジュールの履歴はありません。',
|
||||
@@ -364,6 +443,38 @@ 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': '比特智者',
|
||||
@@ -380,6 +491,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'module.network': '計算機網路',
|
||||
'module.ai_lab': '人工智慧',
|
||||
'module.research': '學術搜尋',
|
||||
'module.sql': 'SQL 工具箱',
|
||||
'module.vision': '視覺實驗室',
|
||||
'module.studio': '影片工作室',
|
||||
'module.audio': '音訊實驗室',
|
||||
@@ -392,6 +504,7 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'desc.network': '通訊協定、資安與分散式系統',
|
||||
'desc.ai_lab': '機器學習、深度學習與神經網路',
|
||||
'desc.research': '基於網路的學術研究',
|
||||
'desc.sql': 'SQL 格式化、轉換與處理',
|
||||
'desc.vision': '圖像分析與生成',
|
||||
'desc.studio': 'AI 影片生成工作室',
|
||||
'desc.audio': '語音轉文字與文字轉語音',
|
||||
@@ -437,6 +550,9 @@ const translations: Record<Language, Record<string, string>> = {
|
||||
'action.new_chat': '新對話',
|
||||
'action.install': '安裝應用',
|
||||
'action.toggle_think': '深度思考模式',
|
||||
'action.lang_mode': 'AI 語言',
|
||||
'action.lang_system': '跟隨系統',
|
||||
'action.lang_input': '跟隨輸入',
|
||||
|
||||
'history.title': '歷史記錄',
|
||||
'history.empty': '暫無該模組的歷史記錄',
|
||||
@@ -484,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
31
sw.js
Normal 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);
|
||||
})
|
||||
);
|
||||
});
|
||||
2
types.ts
2
types.ts
@@ -10,6 +10,7 @@ export enum AppModule {
|
||||
|
||||
// Tools
|
||||
RESEARCH = 'research', // Search Grounding
|
||||
SQL = 'sql', // SQL Tools
|
||||
|
||||
// Creative
|
||||
VISION = 'vision', // Image Gen & Analysis
|
||||
@@ -50,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 {
|
||||
|
||||
Reference in New Issue
Block a user