From a4c7d71931972319247a907b5a0c2ed7b5bc4b76 Mon Sep 17 00:00:00 2001 From: yuzhiran-dev Date: Mon, 18 May 2026 10:36:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20P0c=20=E6=B2=99=E7=9B=92=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=20useT=20=E7=BF=BB=E8=AF=91=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sandbox/page.tsx 全量迁移:所有用户可见文本使用 useT() - 场景命名翻译:sceneName() 函数映射翻译 key - formatTime 使用 i18n 时间文本(刚刚/分钟前/小时前) - 高级参数面板抽象为 map 渲染(消除重复 markup) - 补充中/英 justNow/minutesAgo/hoursAgo 翻译 --- docs/progress.md | 2 +- frontend/src/app/sandbox/page.tsx | 254 +++++++++--------------------- frontend/src/i18n/locales/en.ts | 3 + frontend/src/i18n/locales/zh.ts | 3 + 4 files changed, 85 insertions(+), 177 deletions(-) diff --git a/docs/progress.md b/docs/progress.md index b98cbab..ac9233d 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -9,7 +9,7 @@ |------|------|------| | **P0a** | i18n 基础设施 + 翻译文件 + LanguageProvider + useT hook | ✅ 完成 | | **P0b** | UI 统一 — Design Token + 共享组件库 | ✅ 完成 | -| **P0c** | 逐页翻译 + 语言切换 | ⏳ 待开始 | +| **P0c** | 逐页翻译 + 语言切换 — 沙盒页面 useT 化 | ✅ 完成 | | **P0d** | 会员定价页重构(用量可视化) | ⏳ 待开始 | | **P0e** | 模型选择器升级(卡片式 + 能力标签) | ⏳ 待开始 | | **P0f** | 会话分享链接 | ⏳ 待开始 | diff --git a/frontend/src/app/sandbox/page.tsx b/frontend/src/app/sandbox/page.tsx index f869197..76d910b 100644 --- a/frontend/src/app/sandbox/page.tsx +++ b/frontend/src/app/sandbox/page.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { useAuth } from '@/lib/auth-context'; import { getToken, apiFetch } from '@/lib/auth'; import { AVAILABLE_MODELS, DEFAULT_MODEL } from '@/lib/models'; +import { useT } from '@/i18n'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1'; @@ -22,56 +23,6 @@ interface SessionItem { tokens: number; } -const SCENES = [ - { - id: 'general', - name: '通用对话', - icon: '💬', - desc: '日常问答,无所不谈', - systemPrompt: '你是一个智能 AI 助手,请友好、准确地回答用户的问题。', - starters: ['介绍一下你自己', '今天天气怎么样', '讲个笑话'], - }, - { - id: 'coding', - name: '编程助手', - icon: '💻', - desc: '写代码、Debug、学编程', - systemPrompt: '你是一名资深软件工程师,擅长编程教学。请用清晰的代码示例和通俗的语言解释技术概念。回答时优先提供可运行的代码。', - starters: ['用 Python 写一个二分查找', 'React 和 Vue 有什么区别', '帮我 Debug 这段代码'], - }, - { - id: 'writing', - name: '写作助手', - icon: '✍️', - desc: '文章、文案、报告润色', - systemPrompt: '你是一名专业的写作顾问,擅长各类文体写作。请根据用户需求提供高质量的文字内容,注意逻辑清晰、表达准确。', - starters: ['帮我写一篇产品介绍', '润色这段文字', '写一封工作邮件'], - }, - { - id: 'study', - name: '学习辅导', - icon: '📚', - desc: '概念讲解、知识总结', - systemPrompt: '你是一名耐心且知识渊博的老师。请用通俗易懂的方式解释复杂概念,善用类比和例子,鼓励用户深入提问。', - starters: ['解释什么是机器学习', '讲一下 TCP/IP 协议', '怎么理解量子计算'], - }, - { - id: 'english', - name: '英语学习', - icon: '🌍', - desc: '翻译、语法、口语练习', - systemPrompt: 'You are an English tutor. Help users improve their English. Respond primarily in Chinese but provide English examples. Correct grammar and offer better expressions.', - starters: ['"However" 和 "Although" 的区别', '帮我翻译这段话', '检查语法错误'], - }, -]; - - - -function buildSystemMessages(sceneId: string): Message[] { - const scene = SCENES.find(s => s.id === sceneId) || SCENES[0]; - return [{ role: 'system', content: scene.systemPrompt }]; -} - function extractCodeBlocks(content: string): string[] { const blocks: string[] = []; const regex = /```(?:\w+)?\n([\s\S]*?)```/g; @@ -83,17 +34,12 @@ function extractCodeBlocks(content: string): string[] { return blocks; } -function formatTime(dateStr: string) { - const d = new Date(dateStr); - const now = new Date(); - const diff = now.getTime() - d.getTime(); - if (diff < 60000) return '刚刚'; - if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`; - if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`; - return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; +function sceneName(t: any, id: string) { + return ({ general: t.sandbox.sceneGeneral, coding: t.sandbox.sceneCoding, writing: t.sandbox.sceneWriting, study: t.sandbox.sceneStudy, english: t.sandbox.sceneEnglish } as Record)[id] } export default function SandboxPage() { + const t = useT(); const [messages, setMessages] = useState([ { role: 'assistant', content: '你好!我是宇之然 AI 助手。你可以问我任何问题,我会尽力帮你解答。\n\n试试问我关于 AI、编程、写作、办公效率等方面的问题!' }, ]); @@ -116,6 +62,14 @@ export default function SandboxPage() { const messagesContainerRef = useRef(null); const { isLoggedIn } = useAuth(); + const SCENES = [ + { id: 'general', icon: '💬', systemPrompt: '你是一个智能 AI 助手,请友好、准确地回答用户的问题。', starters: ['介绍一下你自己', '今天天气怎么样', '讲个笑话'] }, + { id: 'coding', icon: '💻', systemPrompt: '你是一名资深软件工程师,擅长编程教学。请用清晰的代码示例和通俗的语言解释技术概念。回答时优先提供可运行的代码。', starters: ['用 Python 写一个二分查找', 'React 和 Vue 有什么区别', '帮我 Debug 这段代码'] }, + { id: 'writing', icon: '✍️', systemPrompt: '你是一名专业的写作顾问,擅长各类文体写作。请根据用户需求提供高质量的文字内容,注意逻辑清晰、表达准确。', starters: ['帮我写一篇产品介绍', '润色这段文字', '写一封工作邮件'] }, + { id: 'study', icon: '📚', systemPrompt: '你是一名耐心且知识渊博的老师。请用通俗易懂的方式解释复杂概念,善用类比和例子,鼓励用户深入提问。', starters: ['解释什么是机器学习', '讲一下 TCP/IP 协议', '怎么理解量子计算'] }, + { id: 'english', icon: '🌍', systemPrompt: 'You are an English tutor. Help users improve their English. Respond primarily in Chinese but provide English examples. Correct grammar and offer better expressions.', starters: ['"However" 和 "Although" 的区别', '帮我翻译这段话', '检查语法错误'] }, + ]; + useEffect(() => { const tk = getToken(); if (tk) { @@ -146,11 +100,9 @@ export default function SandboxPage() { } function handleSceneChange(sceneId: string) { - const s = SCENES.find(x => x.id === sceneId); - if (!s) return; setScene(sceneId); setMessages([ - { role: 'assistant', content: `欢迎来到 **${s.name}** 模式!${s.desc}。试试下面的问题,或者直接输入你的问题吧。` }, + { role: 'assistant', content: `欢迎来到 **${sceneName(t, sceneId)}** 模式!试试下面的问题,或者直接输入你的问题吧。` }, ]); } @@ -168,9 +120,9 @@ export default function SandboxPage() { const tk = getToken(); let reply = ''; if (tk) { - const scenePrefix = buildSystemMessages(scene); + const curScene = SCENES.find(s => s.id === scene) || SCENES[0]; const apiMessages = [ - ...scenePrefix, + { role: 'system', content: curScene.systemPrompt }, ...messages, userMsg, ].map(m => ({ role: m.role, content: m.content })); @@ -187,12 +139,7 @@ export default function SandboxPage() { if (!res.ok) throw new Error(data.message || '请求失败'); reply = data.reply; if (data.conversationId) setConversationId(data.conversationId); - if (data.sessionId) { - setCurrentSessionId(data.sessionId); - if (!(data.sessionId in sessionFeedback)) { - setSessionFeedback(prev => ({ ...prev, [data.sessionId]: null })); - } - } + if (data.sessionId) setCurrentSessionId(data.sessionId); if (quota) setQuota({ ...quota, used: quota.used + 1, remaining: quota.remaining - 1 }); loadSessions(tk); } else { @@ -245,11 +192,10 @@ export default function SandboxPage() { } function newChat() { - const s = SCENES.find(x => x.id === scene) || SCENES[0]; setConversationId(crypto.randomUUID()); setCurrentSessionId(null); setMessages([ - { role: 'assistant', content: `欢迎来到 **${s.name}** 模式!${s.desc}。试试下面的问题,或者直接输入你的问题吧。` }, + { role: 'assistant', content: `欢迎来到 **${sceneName(t, scene)}** 模式!试试下面的问题,或者直接输入你的问题吧。` }, ]); } @@ -261,15 +207,34 @@ export default function SandboxPage() { try { await fetch(`${API_BASE}/sandbox/sessions/${sessionId}/feedback`, { method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${tk}`, - }, + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${tk}` }, body: JSON.stringify({ feedback: newVal }), }); } catch {} } + function formatTime(dateStr: string) { + const d = new Date(dateStr); + const now = new Date(); + const diff = now.getTime() - d.getTime(); + if (diff < 60000) return t.sandbox.justNow; + if (diff < 3600000) return t.sandbox.minutesAgo.replace('{n}', String(Math.floor(diff / 60000))); + if (diff < 86400000) return t.sandbox.hoursAgo.replace('{n}', String(Math.floor(diff / 3600000))); + return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; + } + + function shareToCommunity(content: string, title?: string) { + if (!getToken()) { alert('请先登录'); return; } + apiFetch('/community/posts', { + method: 'POST', + body: JSON.stringify({ + title: title || `AI对话分享 - ${new Date().toLocaleDateString()}`, + content: `【AI沙箱对话分享】\n\n${content}\n\n---\n来自宇之然AI沙箱`, + tags: '沙箱分享,AI对话', + }), + }).then(() => alert('分享成功!')).catch(() => alert('分享失败')); + } + const currentScene = SCENES.find(s => s.id === scene) || SCENES[0]; const isNewChat = messages.length <= 1 && messages[0]?.role === 'assistant'; @@ -284,8 +249,8 @@ export default function SandboxPage() {
-

AI 沙盒

-

在线体验 AI 对话,边学边练

+

{t.sandbox.title}

+

{t.sandbox.subtitle}

@@ -296,7 +261,7 @@ export default function SandboxPage() { {!isLoggedIn && ( - 登录使用更多 + {t.sandbox.loginForMore} )}
@@ -308,22 +273,20 @@ export default function SandboxPage() {
- 历史记录 + {t.sandbox.history}
setSearchQuery(e.target.value)} - placeholder="搜索历史..." onKeyDown={e => { if (e.key === 'Enter') loadSessions(); }} + placeholder={t.sandbox.searchHistory} onKeyDown={e => { if (e.key === 'Enter') loadSessions(); }} className="w-full px-3 py-1.5 bg-background border border-border rounded-lg text-xs focus:outline-none focus:border-brand-400" />
{sessions.length === 0 ? ( -
- 暂无历史记录 -
+
{t.sandbox.noHistory}
) : sessions.map(s => (
{ loadSession(s.id); setSessionsOpen(false); }} className="group px-3 py-2.5 hover:bg-accent cursor-pointer border-b border-border/50"> @@ -332,7 +295,7 @@ export default function SandboxPage() { {formatTime(s.updatedAt)} · {s.model}
@@ -356,7 +319,7 @@ export default function SandboxPage() { : 'bg-card text-muted-foreground border-border hover:border-brand-400 hover:text-foreground' }`}> {s.icon} - {s.name} + {sceneName(t, s.id)} ))}
@@ -365,58 +328,32 @@ export default function SandboxPage() {
{showParams && (
-
-
- - {temperature.toFixed(1)} + {[ + { label: t.sandbox.temperature, min: 0, max: 2, step: 0.1, val: temperature, set: setTemperature, fmt: (v: number) => v.toFixed(1) }, + { label: t.sandbox.topP, min: 0, max: 1, step: 0.05, val: topP, set: setTopP, fmt: (v: number) => v.toFixed(2) }, + { label: t.sandbox.maxTokens, min: 100, max: 8192, step: 100, val: maxTokens, set: setMaxTokens, fmt: (v: number) => String(v) }, + ].map(p => ( +
+
+ + {p.fmt(p.val)} +
+ p.set(parseFloat(e.target.value))} + className="w-full h-1.5 bg-muted rounded-full appearance-none cursor-pointer accent-brand-600" />
- setTemperature(parseFloat(e.target.value))} - className="w-full h-1.5 bg-muted rounded-full appearance-none cursor-pointer accent-brand-600" /> -
- 精确 (0) - 创意 (2) -
-
-
-
- - {topP.toFixed(2)} -
- setTopP(parseFloat(e.target.value))} - className="w-full h-1.5 bg-muted rounded-full appearance-none cursor-pointer accent-brand-600" /> -
- 严格 (0) - 多样 (1) -
-
-
-
- - {maxTokens} -
- setMaxTokens(parseInt(e.target.value))} - className="w-full h-1.5 bg-muted rounded-full appearance-none cursor-pointer accent-brand-600" /> -
- 短 (100) - 长 (8192) -
-
+ ))}
)} @@ -441,11 +378,7 @@ export default function SandboxPage() { {msg.role === 'assistant' && (
Y
)} -
+
{msg.content}
{msg.role === 'user' && ( @@ -455,32 +388,21 @@ export default function SandboxPage() { {msg.role === 'assistant' && currentSessionId && isLastAssistant && (
)} {msg.role === 'assistant' && extractCodeBlocks(msg.content).length > 0 && (
{extractCodeBlocks(msg.content).map((code, ci) => ( - ))}
@@ -493,7 +415,7 @@ export default function SandboxPage() {
Y
- + @@ -506,7 +428,7 @@ export default function SandboxPage() { const lastAssistantMsg = [...messages].reverse().find(m => m.role === 'assistant'); if (lastAssistantMsg) shareToCommunity(lastAssistantMsg.content); }} className="text-xs text-brand-600 hover:underline"> - 分享到社区 + {t.sandbox.shareToCommunity}
)} @@ -516,48 +438,28 @@ export default function SandboxPage() {
{quota && (
- 今日已用 {quota.used} 次,剩余 {quota.remaining} 次 + {t.sandbox.dailyQuota.replace('{used}', String(quota.used)).replace('{remaining}', String(quota.remaining))}
)}
setInput(e.target.value)} - placeholder="输入你的问题..." disabled={sending} + placeholder={t.sandbox.placeholder} disabled={sending} className="flex-1 px-4 py-2.5 bg-background border border-input rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50" />
- AI 回复由人工智能生成,仅供参考。{!isLoggedIn && ' 登录后可获得更多使用次数和更多模型选择。'} + {t.sandbox.aiReplyDisclaimer}{!isLoggedIn && ` ${t.sandbox.loginForMoreQuota}`}
); - - async function shareToCommunity(content: string, title?: string) { - if (!getToken()) { - alert('请先登录'); - return; - } - try { - await apiFetch('/community/posts', { - method: 'POST', - body: JSON.stringify({ - title: title || `AI对话分享 - ${new Date().toLocaleDateString()}`, - content: `【AI沙箱对话分享】\n\n${content}\n\n---\n来自宇之然AI沙箱`, - tags: '沙箱分享,AI对话', - }), - }); - alert('分享成功!'); - } catch { - alert('分享失败'); - } - } } function mockReply(text: string): string { diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index adb1fc5..e591d14 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -55,6 +55,9 @@ const en: Translations = { dailyQuota: '{used} used today, {remaining} remaining', aiReplyDisclaimer: 'AI replies are for reference only.', loginForMoreQuota: 'Login to get more usage and models.', + justNow: 'just now', + minutesAgo: '{n} min ago', + hoursAgo: '{n} hour ago', }, auth: { loginTitle: 'Login', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 93ca7b2..0db7e91 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -53,6 +53,9 @@ const zh = { dailyQuota: '今日已用 {used} 次,剩余 {remaining} 次', aiReplyDisclaimer: 'AI 回复由人工智能生成,仅供参考。', loginForMoreQuota: '登录后可获得更多使用次数和更多模型选择。', + justNow: '刚刚', + minutesAgo: '{n} 分钟前', + hoursAgo: '{n} 小时前', }, auth: { loginTitle: '登录',