Compare commits
2 Commits
v0.1.0_202
...
v0.3.0_202
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e09d8ec55 | |||
| 1494166861 |
@@ -59,33 +59,33 @@ const Tools: React.FC<ToolsProps> = ({ language, hasCustomKey }) => {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-4 md:p-8 space-y-6">
|
||||
<div className="flex space-x-1 bg-slate-200 p-1 rounded-xl w-fit animate-fade-in shadow-inner">
|
||||
<div className="flex space-x-1 bg-slate-200 dark:bg-slate-800 p-1 rounded-xl w-fit animate-fade-in shadow-inner">
|
||||
<button
|
||||
onClick={() => { setActiveTab('image'); setResultUrl(null); }}
|
||||
className={`flex items-center space-x-2 px-6 py-2 rounded-lg transition-all active:scale-95 ${activeTab === 'image' ? 'bg-white shadow text-blue-600' : 'text-slate-600 hover:text-slate-800'}`}
|
||||
className={`flex items-center space-x-2 px-6 py-2 rounded-lg transition-all active:scale-95 ${activeTab === 'image' ? 'bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-blue-400' : 'text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200'}`}
|
||||
>
|
||||
<ImageIcon size={18} />
|
||||
<span className="font-medium">{t.imageGen}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveTab('video'); setResultUrl(null); }}
|
||||
className={`flex items-center space-x-2 px-6 py-2 rounded-lg transition-all active:scale-95 ${activeTab === 'video' ? 'bg-white shadow text-purple-600' : 'text-slate-600 hover:text-slate-800'}`}
|
||||
className={`flex items-center space-x-2 px-6 py-2 rounded-lg transition-all active:scale-95 ${activeTab === 'video' ? 'bg-white dark:bg-slate-700 shadow text-purple-600 dark:text-purple-400' : 'text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200'}`}
|
||||
>
|
||||
<Video size={18} />
|
||||
<span className="font-medium">{t.videoGen}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-3xl shadow-xl border border-slate-100 p-6 md:p-8 animate-slide-up space-y-6">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-3xl shadow-xl border border-slate-100 dark:border-slate-800 p-6 md:p-8 animate-slide-up space-y-6">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
className="w-full p-5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:outline-none focus:bg-white transition-all resize-none min-h-[120px] text-slate-700"
|
||||
className="w-full p-5 bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:outline-none focus:bg-white dark:focus:bg-slate-900 transition-all resize-none min-h-[120px] text-slate-700 dark:text-slate-200"
|
||||
rows={4}
|
||||
placeholder={activeTab === 'image' ? t.imagePromptPlaceholder : t.videoPromptPlaceholder}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
/>
|
||||
<Sparkles className="absolute right-4 bottom-4 text-slate-300 pointer-events-none" size={20} />
|
||||
<Sparkles className="absolute right-4 bottom-4 text-slate-300 dark:text-slate-600 pointer-events-none" size={20} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-6 items-center justify-between">
|
||||
@@ -96,7 +96,7 @@ const Tools: React.FC<ToolsProps> = ({ language, hasCustomKey }) => {
|
||||
<select
|
||||
value={imageSize}
|
||||
onChange={(e) => setImageSize(e.target.value as any)}
|
||||
className="px-4 py-2 bg-slate-100 border border-transparent rounded-xl text-sm font-medium focus:bg-white focus:border-blue-200 transition-all"
|
||||
className="px-4 py-2 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-transparent rounded-xl text-sm font-medium focus:bg-white dark:focus:bg-slate-700 focus:border-blue-200 transition-all"
|
||||
>
|
||||
<option value="1K">1K (HD)</option>
|
||||
<option value="2K">2K (QHD)</option>
|
||||
@@ -109,7 +109,7 @@ const Tools: React.FC<ToolsProps> = ({ language, hasCustomKey }) => {
|
||||
<select
|
||||
value={videoRatio}
|
||||
onChange={(e) => setVideoRatio(e.target.value as any)}
|
||||
className="px-4 py-2 bg-slate-100 border border-transparent rounded-xl text-sm font-medium focus:bg-white focus:border-blue-200 transition-all"
|
||||
className="px-4 py-2 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 border border-transparent rounded-xl text-sm font-medium focus:bg-white dark:focus:bg-slate-700 focus:border-blue-200 transition-all"
|
||||
>
|
||||
<option value="16:9">{t.landscape}</option>
|
||||
<option value="9:16">{t.portrait}</option>
|
||||
@@ -121,7 +121,7 @@ const Tools: React.FC<ToolsProps> = ({ language, hasCustomKey }) => {
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={loading || !prompt.trim()}
|
||||
className="px-8 py-3 bg-blue-600 text-white font-bold rounded-2xl hover:bg-blue-700 active:scale-95 disabled:opacity-50 flex items-center space-x-2 transition-all shadow-lg shadow-blue-200"
|
||||
className="px-8 py-3 bg-blue-600 text-white font-bold rounded-2xl hover:bg-blue-700 active:scale-95 disabled:opacity-50 flex items-center space-x-2 transition-all shadow-lg shadow-blue-200 dark:shadow-none"
|
||||
>
|
||||
{loading ? <Loader2 className="animate-spin" size={20} /> : null}
|
||||
<span>{loading ? t.generating : t.generate}</span>
|
||||
@@ -129,19 +129,19 @@ const Tools: React.FC<ToolsProps> = ({ language, hasCustomKey }) => {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 text-red-600 text-sm rounded-xl border border-red-100 animate-slide-up">
|
||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm rounded-xl border border-red-100 dark:border-red-900/30 animate-slide-up">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && activeTab === 'video' && (
|
||||
<div className="text-center py-4 text-sm text-slate-500 animate-breathe">
|
||||
<div className="text-center py-4 text-sm text-slate-500 dark:text-slate-400 animate-breathe">
|
||||
{t.videoDuration}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{resultUrl && (
|
||||
<div className="mt-8 border-t border-slate-100 pt-8 animate-slide-up">
|
||||
<div className="mt-8 border-t border-slate-100 dark:border-slate-800 pt-8 animate-slide-up">
|
||||
<div className="relative rounded-2xl overflow-hidden bg-slate-900 flex justify-center items-center shadow-2xl group">
|
||||
{activeTab === 'image' ? (
|
||||
<img src={resultUrl} alt="Generated" className="max-h-[600px] w-auto object-contain transition-transform duration-500 group-hover:scale-[1.02]" />
|
||||
|
||||
64
constants.ts
64
constants.ts
@@ -56,6 +56,10 @@ export const TRANSLATIONS = {
|
||||
noHistory: "暂无历史记录。开始一段对话吧!",
|
||||
apiError: "错误:无法生成响应。请检查 API Key。",
|
||||
languageLabel: "界面语言",
|
||||
themeLabel: "主题模式",
|
||||
themeAuto: "跟随系统",
|
||||
themeLight: "浅色模式",
|
||||
themeDark: "深色模式",
|
||||
apiKeyIntro: "为了支持高质量图像生成和视频生成功能,请先选择您的 API Key。",
|
||||
selectApiKeyBtn: "选择 API Key",
|
||||
billingDocs: "了解计费文档",
|
||||
@@ -65,6 +69,17 @@ export const TRANSLATIONS = {
|
||||
older: "更早",
|
||||
transcribePrompt: "请准确转录此音频内容。",
|
||||
getStarted: "开始探索",
|
||||
installApp: "安装应用",
|
||||
installAppDesc: "将社学搭子安装到您的设备,获得原生应用般的流畅体验。",
|
||||
install: "安装",
|
||||
replyLanguageLabel: "AI 回复语言",
|
||||
replyLangSystem: "跟随系统",
|
||||
replyLangAuto: "跟随提问语言",
|
||||
toast: {
|
||||
copySuccess: "已复制到剪贴板",
|
||||
genSuccess: "生成成功",
|
||||
saveSuccess: "保存成功"
|
||||
},
|
||||
onboarding: {
|
||||
step1: "欢迎使用社学搭子!这是一个专为社会学研究者打造的数字空间。",
|
||||
step2: "你可以通过左侧的场景切换,选择从‘经典导读’到‘研究讨论’的不同模式。",
|
||||
@@ -139,6 +154,10 @@ export const TRANSLATIONS = {
|
||||
noHistory: "暫無歷史記錄。開始一段對話吧!",
|
||||
apiError: "錯誤:無法生成響應。請檢查 API Key。",
|
||||
languageLabel: "介面語言",
|
||||
themeLabel: "主題模式",
|
||||
themeAuto: "跟隨系統",
|
||||
themeLight: "淺色模式",
|
||||
themeDark: "深色模式",
|
||||
apiKeyIntro: "為了支持高質量圖像生成和視頻生成功能,請先選擇您的 API Key。",
|
||||
selectApiKeyBtn: "選擇 API Key",
|
||||
billingDocs: "了解計費文檔",
|
||||
@@ -148,6 +167,17 @@ export const TRANSLATIONS = {
|
||||
older: "更早",
|
||||
transcribePrompt: "請準確轉錄此音訊內容。",
|
||||
getStarted: "開始探索",
|
||||
installApp: "安裝應用",
|
||||
installAppDesc: "將社學搭子安裝到您的設備,獲得原生應用般的流暢體驗。",
|
||||
install: "安裝",
|
||||
replyLanguageLabel: "AI 回復語言",
|
||||
replyLangSystem: "跟隨系統",
|
||||
replyLangAuto: "跟隨提問語言",
|
||||
toast: {
|
||||
copySuccess: "已複製到剪貼簿",
|
||||
genSuccess: "生成成功",
|
||||
saveSuccess: "保存成功"
|
||||
},
|
||||
onboarding: {
|
||||
step1: "歡迎使用社學搭子!這是一個專為社會學研究者打造的數字空間。",
|
||||
step2: "你可以通過左側的場景切換,選擇從‘經典導讀’到‘研究討論’的不同模式。",
|
||||
@@ -158,10 +188,10 @@ export const TRANSLATIONS = {
|
||||
homeFeatureTitle: "探索模組",
|
||||
homeQuoteTitle: "社會學視角",
|
||||
quotes: [
|
||||
{ text: "人是懸掛在由他自己所編織的意義之網中的動物。", author: "克利福德·格爾茨" },
|
||||
{ text: "人是懸掛在由他自己所編織的意義之網中的動物。", author: "克利福德·格尔茨" },
|
||||
{ text: "想像力,這種能力可以使人看清個人生活與社會結構之間的聯繫。", author: "C·賴特·米爾斯" },
|
||||
{ text: "社會學是關於社會行動的科學,其目的是通過對行動意義的解釋來理解行動。", author: "馬克斯·韋伯" },
|
||||
{ text: "哲學家們只是用不同的方式解釋世界,而問題在於改變世界。", author: "卡爾·馬克思" }
|
||||
{ text: "哲學家們只是用不同的方式解釋世界,而問題在於改變世界。", author: "卡尔·马克思" }
|
||||
],
|
||||
scenarios: {
|
||||
general: { title: "日常答疑", desc: "解答各類社會學基礎問題", greeting: "你好!我是你的社會學學習搭子。有什麼日常學習中的疑問需要我解答嗎?" },
|
||||
@@ -222,6 +252,10 @@ export const TRANSLATIONS = {
|
||||
noHistory: "履歴がありません。",
|
||||
apiError: "エラー:応答を生成できませんでした。APIキーを確認してください。",
|
||||
languageLabel: "言語",
|
||||
themeLabel: "テーマ",
|
||||
themeAuto: "システムに従う",
|
||||
themeLight: "ライトモード",
|
||||
themeDark: "ダークモード",
|
||||
apiKeyIntro: "高品質な画像・動画生成を利用するには、まずAPIキーを選択してください。",
|
||||
selectApiKeyBtn: "APIキーを選択",
|
||||
billingDocs: "課金ドキュメントを確認",
|
||||
@@ -231,6 +265,17 @@ export const TRANSLATIONS = {
|
||||
older: "それ以前",
|
||||
transcribePrompt: "この音声を正確に書き起こしてください。",
|
||||
getStarted: "はじめる",
|
||||
installApp: "アプリをインストール",
|
||||
installAppDesc: "デバイスにインストールして、より良い体験を。",
|
||||
install: "インストール",
|
||||
replyLanguageLabel: "AI応答言語",
|
||||
replyLangSystem: "システム言語に従う",
|
||||
replyLangAuto: "入力言語に従う",
|
||||
toast: {
|
||||
copySuccess: "クリップボードにコピーしました",
|
||||
genSuccess: "生成成功",
|
||||
saveSuccess: "保存成功"
|
||||
},
|
||||
onboarding: {
|
||||
step1: "ソシオパルへようこそ!社会学研究者のためのデジタル空間です。",
|
||||
step2: "左側のメニューから、古典講読から研究アドバイザーまでシナリオを切り替えられます。",
|
||||
@@ -305,6 +350,10 @@ export const TRANSLATIONS = {
|
||||
noHistory: "No history yet.",
|
||||
apiError: "Error: Could not generate response.",
|
||||
languageLabel: "Language",
|
||||
themeLabel: "Theme",
|
||||
themeAuto: "System",
|
||||
themeLight: "Light",
|
||||
themeDark: "Dark",
|
||||
apiKeyIntro: "To support high-quality image and video generation, please select your API Key first.",
|
||||
selectApiKeyBtn: "Select API Key",
|
||||
billingDocs: "Billing Documentation",
|
||||
@@ -314,6 +363,17 @@ export const TRANSLATIONS = {
|
||||
older: "Older",
|
||||
transcribePrompt: "Please transcribe this audio exactly as spoken.",
|
||||
getStarted: "Get Started",
|
||||
installApp: "Install App",
|
||||
installAppDesc: "Install SocioPal on your device for a better experience.",
|
||||
install: "Install",
|
||||
replyLanguageLabel: "AI Reply Language",
|
||||
replyLangSystem: "System Default",
|
||||
replyLangAuto: "Match User Input",
|
||||
toast: {
|
||||
copySuccess: "Copied to clipboard",
|
||||
genSuccess: "Generation successful",
|
||||
saveSuccess: "Saved successfully"
|
||||
},
|
||||
onboarding: {
|
||||
step1: "Welcome to SocioPal! A digital space designed for sociology researchers.",
|
||||
step2: "Switch scenarios on the left to explore modes from 'Classic Readings' to 'Research Advisor'.",
|
||||
|
||||
50
index.html
50
index.html
@@ -1,18 +1,47 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<title>社学搭子 - 社会学学习工具</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
|
||||
<!-- 添加 typography 插件支持 -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
slate: {
|
||||
850: '#1e293b', // 自定义深色背景
|
||||
950: '#020617',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; overflow: hidden; }
|
||||
body { font-family: 'Inter', sans-serif; overflow: hidden; touch-action: manipulation; }
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
||||
|
||||
/* 深色模式滚动条 */
|
||||
.dark ::-webkit-scrollbar-thumb { background: #475569; }
|
||||
.dark ::-webkit-scrollbar-thumb:hover { background: #64748b; }
|
||||
|
||||
/* 隐藏滚动条但保留功能 */
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
|
||||
/* 自定义动画 */
|
||||
@keyframes slideUp {
|
||||
@@ -27,9 +56,15 @@
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
/* 波浪跳动加载动画 */
|
||||
@keyframes bounce-delay {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
.animate-slide-up { animation: slideUp 0.3s ease-out forwards; }
|
||||
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
||||
.animate-breathe { animation: breathe 2s infinite ease-in-out; }
|
||||
.animate-bounce-delay { animation: bounce-delay 1.4s infinite ease-in-out both; }
|
||||
|
||||
/* 按钮微动效 */
|
||||
.btn-hover { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); }
|
||||
@@ -47,17 +82,20 @@
|
||||
"@google/genai": "https://esm.sh/@google/genai@^1.34.0",
|
||||
"lucide-react": "https://esm.sh/lucide-react@^0.562.0",
|
||||
"react-markdown": "https://esm.sh/react-markdown@^10.1.0",
|
||||
"remark-gfm": "https://esm.sh/remark-gfm@^4.0.0",
|
||||
"vite": "https://esm.sh/vite@^7.3.0",
|
||||
"@vitejs/plugin-react": "https://esm.sh/@vitejs/plugin-react@^5.1.2",
|
||||
"express": "https://esm.sh/express@^5.2.1",
|
||||
"path": "https://esm.sh/path@^0.12.7",
|
||||
"url": "https://esm.sh/url@^0.11.4"
|
||||
"url": "https://esm.sh/url@^0.11.4",
|
||||
"vite-plugin-pwa": "https://esm.sh/vite-plugin-pwa@^1.2.0",
|
||||
"html2canvas": "https://esm.sh/html2canvas@^1.4.1"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-slate-50 text-slate-900">
|
||||
<body class="bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
10
package.json
10
package.json
@@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"name": "sociopal",
|
||||
"private": true,
|
||||
@@ -18,12 +17,15 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
"vite": "^5.0.0",
|
||||
"html2canvas": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0"
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"vite-plugin-pwa": "^0.19.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
releases/HTY1024-APP-SKG-0.2.0_20251224.zip
Normal file
BIN
releases/HTY1024-APP-SKG-0.2.0_20251224.zip
Normal file
Binary file not shown.
BIN
releases/HTY1024-APP-SKG-0.3.0_20251226.zip
Normal file
BIN
releases/HTY1024-APP-SKG-0.3.0_20251226.zip
Normal file
Binary file not shown.
@@ -19,12 +19,33 @@ const getClient = () => {
|
||||
};
|
||||
|
||||
// --- 模型定义 ---
|
||||
const MODEL_CHAT_STANDARD = "gemini-3-flash-preview";
|
||||
const MODEL_CHAT_DEEP = "gemini-3-pro-preview";
|
||||
const MODEL_CHAT_FAST = "gemini-flash-lite-latest";
|
||||
const MODEL_IMAGE_GEN = "gemini-3-pro-image-preview";
|
||||
const MODEL_VIDEO_GEN = "veo-3.1-fast-generate-preview";
|
||||
const MODEL_TTS = "gemini-2.5-flash-preview-tts";
|
||||
export const MODEL_CHAT_STANDARD = "gemini-3-flash-preview";
|
||||
export const MODEL_CHAT_DEEP = "gemini-3-pro-preview";
|
||||
export const MODEL_CHAT_FAST = "gemini-flash-lite-latest";
|
||||
export const MODEL_IMAGE_GEN = "gemini-3-pro-image-preview";
|
||||
export const MODEL_VIDEO_GEN = "veo-3.1-fast-generate-preview";
|
||||
export const MODEL_TTS = "gemini-2.5-flash-preview-tts";
|
||||
|
||||
// 获取当前模式对应的模型名称
|
||||
export const getModelNameForMode = (mode: ChatMode): string => {
|
||||
switch (mode) {
|
||||
case ChatMode.DEEP: return MODEL_CHAT_DEEP;
|
||||
case ChatMode.FAST: return MODEL_CHAT_FAST;
|
||||
case ChatMode.STANDARD:
|
||||
default:
|
||||
return MODEL_CHAT_STANDARD;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化模型名称用于展示
|
||||
export const formatModelName = (modelId: string): string => {
|
||||
if (modelId === MODEL_CHAT_STANDARD) return "Gemini 3 Flash";
|
||||
if (modelId === MODEL_CHAT_DEEP) return "Gemini 3 Pro";
|
||||
if (modelId === MODEL_CHAT_FAST) return "Gemini Flash Lite";
|
||||
if (modelId === MODEL_IMAGE_GEN) return "Gemini 3 Pro Image";
|
||||
if (modelId === MODEL_VIDEO_GEN) return "Veo 3.1";
|
||||
return modelId;
|
||||
};
|
||||
|
||||
// --- 聊天功能 ---
|
||||
|
||||
@@ -32,13 +53,13 @@ export const streamChatResponse = async (
|
||||
history: Message[],
|
||||
currentMessage: string,
|
||||
mode: ChatMode,
|
||||
language: AppLanguage,
|
||||
targetLanguage: string, // 传入目标语言代码(如 'zh-CN', 'en')或 'auto'
|
||||
scenario: ChatScenario = ChatScenario.GENERAL,
|
||||
attachments: { mimeType: string; data: string }[] = [],
|
||||
onChunk: (text: string, grounding?: any) => void
|
||||
) => {
|
||||
const ai = getClient();
|
||||
let model = MODEL_CHAT_STANDARD;
|
||||
let model = getModelNameForMode(mode);
|
||||
|
||||
// 根据场景构造系统指令
|
||||
let baseInstruction = "";
|
||||
@@ -78,20 +99,26 @@ export const streamChatResponse = async (
|
||||
break;
|
||||
}
|
||||
|
||||
// 语言指令:根据 targetLanguage 动态生成
|
||||
let languageInstruction = "";
|
||||
if (targetLanguage === 'auto') {
|
||||
languageInstruction = "Reply in the language used by the user in their latest message. If the user explicitly asks to switch languages, follow their request.";
|
||||
} else {
|
||||
// 强制使用特定语言
|
||||
languageInstruction = `You MUST reply in ${targetLanguage}. Do NOT use other languages unless explicitly asked for translation examples.`;
|
||||
}
|
||||
|
||||
let config: any = {
|
||||
systemInstruction: `${baseInstruction} Always reply in the user's preferred language: ${language}.`,
|
||||
systemInstruction: `${baseInstruction}\n\n${languageInstruction}`,
|
||||
};
|
||||
|
||||
// 根据模式配置参数
|
||||
if (mode === ChatMode.STANDARD) {
|
||||
model = MODEL_CHAT_STANDARD;
|
||||
config.tools = [{ googleSearch: {} }];
|
||||
} else if (mode === ChatMode.DEEP) {
|
||||
model = MODEL_CHAT_DEEP;
|
||||
config.thinkingConfig = { thinkingBudget: 32768 }; // Pro 模型最大思考预算
|
||||
} else if (mode === ChatMode.FAST) {
|
||||
model = MODEL_CHAT_FAST;
|
||||
}
|
||||
// Fast 模式仅切换模型,无需额外配置
|
||||
|
||||
const chat = ai.chats.create({
|
||||
model,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import { UserSettings, ChatSession, AppLanguage } from "../types";
|
||||
import { DEFAULT_LANGUAGE } from "../constants";
|
||||
|
||||
@@ -12,7 +13,7 @@ export const loadSettings = (): UserSettings => {
|
||||
if (stored) return JSON.parse(stored);
|
||||
return {
|
||||
language: DEFAULT_LANGUAGE,
|
||||
theme: 'light'
|
||||
theme: 'auto'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,4 +66,4 @@ export const clearData = () => {
|
||||
localStorage.removeItem(KEYS.SETTINGS);
|
||||
localStorage.removeItem(KEYS.SESSIONS);
|
||||
localStorage.removeItem(KEYS.CURRENT_SESSION);
|
||||
};
|
||||
};
|
||||
|
||||
3
types.ts
3
types.ts
@@ -27,6 +27,7 @@ export interface Message {
|
||||
attachments?: Attachment[];
|
||||
isThinking?: boolean;
|
||||
groundingMetadata?: GroundingMetadata;
|
||||
model?: string; // 模型名称
|
||||
}
|
||||
|
||||
export interface GroundingMetadata {
|
||||
@@ -45,7 +46,7 @@ export interface Attachment {
|
||||
|
||||
export interface UserSettings {
|
||||
language: AppLanguage;
|
||||
theme: 'light' | 'dark';
|
||||
theme: 'light' | 'dark' | 'auto';
|
||||
isOnboarded?: boolean;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { cwd } from 'node:process'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
@@ -9,7 +10,34 @@ export default defineConfig(({ mode }) => {
|
||||
const env = { ...process.env, ...loadEnv(mode, cwd(), '') };
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
|
||||
manifest: {
|
||||
name: 'SocioPal - Social Learning Tool',
|
||||
short_name: 'SocioPal',
|
||||
description: 'A comprehensive AI-powered tool for sociology learning.',
|
||||
theme_color: '#ffffff',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
icons: [
|
||||
{
|
||||
src: 'pwa-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
define: {
|
||||
// 确保 API Key 在构建时注入
|
||||
'process.env.API_KEY': JSON.stringify(env.API_KEY || '')
|
||||
|
||||
Reference in New Issue
Block a user