diff --git a/app/components/agent-card.tsx b/app/components/agent-card.tsx index 71045de..a0f8075 100644 --- a/app/components/agent-card.tsx +++ b/app/components/agent-card.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { buildGatewayUrl } from "@/lib/gateway-url"; import { getPlatformDisplayName } from "@/lib/platforms"; +import { useI18n } from "@/lib/i18n"; export interface AgentPlatform { name: string; @@ -102,6 +103,7 @@ async function copyText(text: string): Promise { } function ErrorStatusWithCopy({ error, className }: { error?: string; className?: string }) { + const { t } = useI18n(); const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">("idle"); const errorText = (error || "Unknown error").trim() || "Unknown error"; @@ -130,10 +132,10 @@ function ErrorStatusWithCopy({ error, className }: { error?: string; className?: onClick={onCopy} className="rounded border border-[var(--border)] px-1.5 py-0.5 text-[10px] text-[var(--text-muted)] hover:text-[var(--text)] hover:border-[var(--accent)]" > - {copyState === "copied" ? "已复制" : "复制"} + {copyState === "copied" ? t("common.copied") : t("common.copy")} {copyState === "failed" && ( - 复制失败 + {t("common.copyFailed")} )} {errorText} diff --git a/app/daily/page.tsx b/app/daily/page.tsx index 1c0f4bf..b04259b 100644 --- a/app/daily/page.tsx +++ b/app/daily/page.tsx @@ -2,6 +2,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { useI18n, localeToBcp47 } from '@/lib/i18n'; interface DailyItem { time: string; @@ -10,6 +11,7 @@ interface DailyItem { } function DailyBoard() { + const { t, locale } = useI18n(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [speaking, setSpeaking] = useState(false); @@ -33,14 +35,16 @@ function DailyBoard() { const completedItems = items.filter(i => i.status === 'completed'); const inProgressItems = items.filter(i => i.status === 'in-progress'); - + const reportText = completedItems.length > 0 - ? `今日共完成 ${completedItems.length} 项任务。${completedItems.map(i => i.content).join('。')}` - : '今日暂无完成记录'; + ? t('daily.reportCompleted') + .replace('{count}', String(completedItems.length)) + .replace('{items}', completedItems.map(i => i.content).join(locale === 'en' ? '. ' : '。')) + : t('daily.reportEmpty'); const speak = () => { if (!('speechSynthesis' in window)) { - alert('您的浏览器不支持语音合成'); + alert(t('daily.noSpeechSupport')); return; } @@ -51,25 +55,18 @@ function DailyBoard() { } const u = new SpeechSynthesisUtterance(reportText); - u.lang = 'zh-CN'; + u.lang = localeToBcp47(locale); u.rate = 1.0; - + u.onstart = () => setSpeaking(true); u.onend = () => setSpeaking(false); u.onerror = () => setSpeaking(false); - + setUtterance(u); window.speechSynthesis.speak(u); }; - const stop = () => { - if ('speechSynthesis' in window) { - window.speechSynthesis.cancel(); - } - setSpeaking(false); - }; - - const today = new Date().toLocaleDateString('zh-CN', { + const today = new Date().toLocaleDateString(localeToBcp47(locale), { year: 'numeric', month: 'long', day: 'numeric', @@ -78,10 +75,9 @@ function DailyBoard() { return (
- {/* 头部 */}
-

📅 今日播报板

+

📅 {t('daily.title')}

{today}

@@ -96,42 +92,40 @@ function DailyBoard() { > {speaking ? ( <> - ⏹️ 停止 + ⏹️ {t('daily.stop')} ) : ( <> - 🎤 播报 + 🎤 {t('daily.report')} )}
- {/* 统计 */}
{completedItems.length}
-
已完成
+
{t('daily.completed')}
{inProgressItems.length}
-
进行中
+
{t('daily.inProgress')}
{items.length - completedItems.length - inProgressItems.length}
-
待开始
+
{t('daily.pending')}
- {/* 播报列表 */}
{loading ? ( -
加载中...
+
{t('common.loading')}
) : items.length === 0 ? (
- 今日暂无记录 + {t('daily.noRecords')}
) : (
@@ -143,13 +137,13 @@ function DailyBoard() { }`} > - {item.status === 'completed' ? '✅' : + {item.status === 'completed' ? '✅' : item.status === 'in-progress' ? '🔄' : '⏳'}

{item.content} @@ -164,10 +158,9 @@ function DailyBoard() { )}

- {/* 提示 */} {speaking && (
- 🎤 正在语音播报今日完成事项... + 🎤 {t('daily.speakingHint')}
)}
diff --git a/app/gateway-status.tsx b/app/gateway-status.tsx index 4fe42b3..90045fd 100644 --- a/app/gateway-status.tsx +++ b/app/gateway-status.tsx @@ -133,21 +133,21 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil const res = await fetch("/api/gateway-restart", { method: "POST" }); const data = await res.json(); if (data.ok) { - setRestartMsg("✅ 重啟指令已送出,稍後自動重新檢查…"); + setRestartMsg(t("gateway.restartSent")); setTimeout(() => { checkHealth(); setRestartMsg(null); setShowDetail(false); }, 4000); } else { - setRestartMsg(`❌ 重啟失敗:${data.error || "未知錯誤"}`); + setRestartMsg(t("gateway.restartFailed").replace("{error}", data.error || t("common.unknownError"))); } } catch (err: any) { - setRestartMsg(`❌ 重啟失敗:${err.message}`); + setRestartMsg(t("gateway.restartFailed").replace("{error}", err.message)); } finally { setRestarting(false); } - }, [restarting, checkHealth]); + }, [restarting, checkHealth, t]); const handleRestore = useCallback(async (filename: string) => { if (restoring) return; @@ -252,7 +252,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil ) : showWarning ? ( ⚠️ ) : ( @@ -270,9 +270,9 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil className={`inline-flex items-center gap-1 rounded-full border font-medium transition-colors ${ compact ? "px-1.5 py-0.5 text-[10px]" : "px-2 py-0.5 text-xs" } bg-orange-500/20 text-orange-300 border-orange-500/40 hover:bg-orange-500/35 cursor-pointer`} - title="查看問題並重啟 Gateway" + title={t("gateway.viewAndRestart")} > - 🔄{!compact && " 重啟"} + 🔄{!compact && ` ${t("gateway.restart")}`} )} @@ -280,7 +280,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil {showDetail && (
- Gateway 狀態 + {t("gateway.statusTitle")}
@@ -289,7 +289,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
Process: - {health?.ok ? "✅ 運作中" : "❌ 無回應"} + {health?.ok ? t("gateway.processRunning") : t("gateway.processDown")}
@@ -298,10 +298,10 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
Telegram: - ⚠️ Polling 異常 + ⚠️ {t("gateway.telegramPolling")} {logResult.lastStallAt && ( - ({new Date(logResult.lastStallAt).toLocaleTimeString("zh-TW", { hour: "2-digit", minute: "2-digit" })}) + ({new Date(logResult.lastStallAt).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}) )} @@ -312,7 +312,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil {logResult && logResult.issues.includes("subagent_timeout") && (
Subagent: - ⚠️ 有 timeout 記錄 + ⚠️ {t("gateway.subagentTimeout")}
)} @@ -483,7 +483,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil disabled={restarting} className="w-full py-1.5 rounded-lg bg-orange-500/20 text-orange-300 border border-orange-500/40 hover:bg-orange-500/35 transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer font-medium" > - {restarting ? "⏳ 重啟中…" : "🔄 重啟 Gateway"} + {restarting ? t("gateway.restarting") : t("gateway.restartGateway")}
diff --git a/app/skills/page.tsx b/app/skills/page.tsx index f689d48..931f599 100644 --- a/app/skills/page.tsx +++ b/app/skills/page.tsx @@ -268,7 +268,7 @@ export default function SkillsPage() {

{t("skills.title")}

- 共 {skills.length} {t("skills.count")}({t("skills.builtin")} {builtinCount} / {t("skills.extension")} {extensionCount} / {t("skills.custom")} {customCount} / {t("skills.workspace")} {workspaceCount}) + {t("skills.totalPrefix")} {skills.length} {t("skills.count")}({t("skills.builtin")} {builtinCount} / {t("skills.extension")} {extensionCount} / {t("skills.custom")} {customCount} / {t("skills.workspace")} {workspaceCount})

diff --git a/app/workspace/page.tsx b/app/workspace/page.tsx index 843c56a..9eaa838 100644 --- a/app/workspace/page.tsx +++ b/app/workspace/page.tsx @@ -2,6 +2,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { useI18n, localeToBcp47, type Locale } from '@/lib/i18n'; interface AgentState { agentId: string; @@ -16,51 +17,76 @@ interface Skill { status: string; } -const STATE_CONFIG = { - working: { label: '工作中', emoji: '🔥', color: 'bg-green-500' }, - online: { label: '在线', emoji: '🟢', color: 'bg-blue-500' }, - idle: { label: '空闲', emoji: '😴', color: 'bg-yellow-500' }, - offline: { label: '离线', emoji: '⚫', color: 'bg-gray-500' }, -}; +function AgentCard({ + agent, + t, + locale, +}: { + agent: AgentState; + t: (key: string) => string; + locale: Locale; +}) { + const stateLabels: Record = { + working: t('agent.status.working'), + online: t('agent.status.online'), + idle: t('agent.status.idle'), + offline: t('agent.status.offline'), + }; + const stateEmojis: Record = { + working: '🔥', + online: '🟢', + idle: '😴', + offline: '⚫', + }; + const stateColors: Record = { + working: 'bg-green-500', + online: 'bg-blue-500', + idle: 'bg-yellow-500', + offline: 'bg-gray-500', + }; -function AgentCard({ agent }: { agent: AgentState }) { - const config = STATE_CONFIG[agent.state] || STATE_CONFIG.offline; + const label = stateLabels[agent.state] || stateLabels.offline; + const emoji = stateEmojis[agent.state] || stateEmojis.offline; + const color = stateColors[agent.state] || stateColors.offline; const lastActiveText = agent.lastActive - ? new Date(agent.lastActive).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) - : '无记录'; + ? new Date(agent.lastActive).toLocaleTimeString(localeToBcp47(locale), { + hour: '2-digit', + minute: '2-digit', + }) + : t('workspace.noRecord'); return (
-
- {config.emoji} +
+ {emoji}

{agent.agentId}

-

{config.label}

+

{label}

- 最后活跃: {lastActiveText} + {t('workspace.lastActive')} {lastActiveText}
); } -function SkillCard({ skill }: { skill: Skill }) { +function SkillCard({ skill, t }: { skill: Skill; t: (key: string) => string }) { return (

{skill.name}

- {skill.status === 'running' ? '运行中' : '已禁用'} + {skill.status === 'running' ? t('workspace.skillRunning') : t('workspace.skillDisabled')}

- {skill.description || '无描述'} + {skill.description || t('skills.noDesc')}

v{skill.version} @@ -70,6 +96,7 @@ function SkillCard({ skill }: { skill: Skill }) { } export default function WorkspacePage() { + const { t, locale } = useI18n(); const [agents, setAgents] = useState([]); const [skills, setSkills] = useState([]); const [loading, setLoading] = useState(true); @@ -84,17 +111,16 @@ export default function WorkspacePage() { ]); const agentData = await agentRes.json(); const skillsData = await skillsRes.json(); - - // Handle both old and new skill format + if (Array.isArray(skillsData.skills)) { - setSkills(skillsData.skills.map((s: any) => ({ - name: s.name || s.id || '未知', + setSkills(skillsData.skills.map((s: { name?: string; id?: string; description?: string; version?: string }) => ({ + name: s.name || s.id || 'unknown', description: s.description || '', version: s.version || 'unknown', status: 'running', }))); } - + setAgents(agentData.statuses || []); } catch (error) { console.error('Failed to fetch data:', error); @@ -105,71 +131,69 @@ export default function WorkspacePage() { } fetchData(); - const interval = setInterval(fetchData, 15000); // 15秒刷新 + const interval = setInterval(fetchData, 15000); return () => clearInterval(interval); }, []); const workingCount = agents.filter(a => a.state === 'working').length; const onlineCount = agents.filter(a => a.state === 'online' || a.state === 'working').length; + const refreshTime = lastRefresh.toLocaleTimeString(localeToBcp47(locale)); return (

- {/* 头部 */}
-

🤖 Agent 工作台

+

🤖 {t('nav.workspace')}

- 实时监控所有 Agent 状态 · {lastRefresh.toLocaleTimeString('zh-CN')} 更新 + {t('workspace.subtitle').replace('{time}', refreshTime)}

- 在线: + {t('workspace.online')} {onlineCount}/{agents.length}
- 工作中: + {t('workspace.workingCount')} {workingCount}
- {/* Agent 网格 */}
-

📊 Agent 状态

+

📊 {t('workspace.agentStatus')}

{loading ? ( -
加载中...
+
{t('common.loading')}
) : agents.length === 0 ? (
- 暂无 Agent 数据 + {t('workspace.noAgents')}
) : (
{agents.map((agent) => ( - + ))}
)}
- {/* 技能列表 */}
-

📦 已安装技能

+

📦 {t('workspace.installedSkills')}

- 共 {skills.length} 个技能 + {t('workspace.totalSkills').replace('{count}', String(skills.length))}
{loading ? ( -
加载中...
+
{t('common.loading')}
) : skills.length === 0 ? (
- 暂无技能 + {t('workspace.noSkills')}
) : (
{skills.map((skill) => ( - + ))}
)} diff --git a/lib/i18n.tsx b/lib/i18n.tsx index 3752891..bb00f49 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -13,6 +13,8 @@ const translations: Record> = { // nav sidebar "nav.overview": "總覽", "nav.agents": "機器人", + "nav.workspace": "Agent 工作台", + "nav.daily": "今日播報", "nav.models": "模型列表", "nav.monitor": "監控", "nav.sessions": "會話列表", @@ -276,6 +278,45 @@ const translations: Record> = { "skills.contentTitle": "SKILL.md 內容", "skills.loadingContent": "正在載入技能內容...", "skills.contentLoadFailed": "技能內容載入失敗", + "skills.totalPrefix": "共", + + // workspace page + "workspace.subtitle": "即時監控所有 Agent 狀態 · {time} 更新", + "workspace.online": "在線:", + "workspace.workingCount": "工作中:", + "workspace.agentStatus": "Agent 狀態", + "workspace.noAgents": "暫無 Agent 資料", + "workspace.installedSkills": "已安裝技能", + "workspace.totalSkills": "共 {count} 個技能", + "workspace.noSkills": "暫無技能", + "workspace.lastActive": "最後活躍:", + "workspace.noRecord": "無記錄", + "workspace.skillRunning": "執行中", + "workspace.skillDisabled": "已停用", + "workspace.unknown": "未知", + + // daily report page + "daily.title": "今日播報板", + "daily.report": "播報", + "daily.stop": "停止", + "daily.completed": "已完成", + "daily.inProgress": "進行中", + "daily.pending": "待開始", + "daily.noRecords": "今日暫無記錄", + "daily.speakingHint": "正在語音播報今日完成事項...", + "daily.noSpeechSupport": "您的瀏覽器不支援語音合成", + "daily.reportCompleted": "今日共完成 {count} 項任務。{items}", + "daily.reportEmpty": "今日暫無完成記錄", + + // theme + "theme.switchToLight": "切換到亮色模式", + "theme.switchToDark": "切換到暗色模式", + + // copy + "common.copy": "複製", + "common.copied": "已複製", + "common.copyFailed": "複製失敗", + "common.unknownError": "未知錯誤", // gateway status "gateway.healthy": "Gateway 運作正常", @@ -300,6 +341,18 @@ const translations: Record> = { "gateway.reloadNow": "立即重新整理", "gateway.reloadHint": "重新整理後,可點選機器人卡片上的「測試」確認是否正常運作", "gateway.configCorruptHint": "設定檔可能損毀,請使用上方 Gateway 面板還原備份", + "gateway.restart": "重啟", + "gateway.restartGateway": "🔄 重啟 Gateway", + "gateway.restarting": "⏳ 重啟中…", + "gateway.restartSent": "✅ 重啟指令已送出,稍後自動重新檢查…", + "gateway.restartFailed": "❌ 重啟失敗:{error}", + "gateway.viewAndRestart": "查看問題並重啟 Gateway", + "gateway.telegramStall": "Telegram 連線異常,建議重啟", + "gateway.statusTitle": "Gateway 狀態", + "gateway.processRunning": "✅ 運作中", + "gateway.processDown": "❌ 無回應", + "gateway.telegramPolling": "⚠️ Polling 異常", + "gateway.subagentTimeout": "⚠️ 有 timeout 記錄", // pixel office "pixelOffice.title": "OpenClaw Agents 辦公室", @@ -621,6 +674,45 @@ const translations: Record> = { "skills.contentTitle": "SKILL.md 内容", "skills.loadingContent": "正在加载技能内容...", "skills.contentLoadFailed": "技能内容加载失败", + "skills.totalPrefix": "共", + + // workspace page + "workspace.subtitle": "实时监控所有 Agent 状态 · {time} 更新", + "workspace.online": "在线:", + "workspace.workingCount": "工作中:", + "workspace.agentStatus": "Agent 状态", + "workspace.noAgents": "暂无 Agent 数据", + "workspace.installedSkills": "已安装技能", + "workspace.totalSkills": "共 {count} 个技能", + "workspace.noSkills": "暂无技能", + "workspace.lastActive": "最后活跃:", + "workspace.noRecord": "无记录", + "workspace.skillRunning": "运行中", + "workspace.skillDisabled": "已禁用", + "workspace.unknown": "未知", + + // daily report page + "daily.title": "今日播报板", + "daily.report": "播报", + "daily.stop": "停止", + "daily.completed": "已完成", + "daily.inProgress": "进行中", + "daily.pending": "待开始", + "daily.noRecords": "今日暂无记录", + "daily.speakingHint": "正在语音播报今日完成事项...", + "daily.noSpeechSupport": "您的浏览器不支持语音合成", + "daily.reportCompleted": "今日共完成 {count} 项任务。{items}", + "daily.reportEmpty": "今日暂无完成记录", + + // theme + "theme.switchToLight": "切换到亮色模式", + "theme.switchToDark": "切换到暗色模式", + + // copy + "common.copy": "复制", + "common.copied": "已复制", + "common.copyFailed": "复制失败", + "common.unknownError": "未知错误", // gateway status "gateway.healthy": "Gateway 运行正常", @@ -645,6 +737,18 @@ const translations: Record> = { "gateway.reloadNow": "立即刷新", "gateway.reloadHint": "刷新后,可点击机器人卡片上的「测试」确认是否正常运作", "gateway.configCorruptHint": "配置文件可能损坏,请使用上方 Gateway 面板还原备份", + "gateway.restart": "重启", + "gateway.restartGateway": "🔄 重启 Gateway", + "gateway.restarting": "⏳ 重启中…", + "gateway.restartSent": "✅ 重启指令已送出,稍后自动重新检查…", + "gateway.restartFailed": "❌ 重启失败:{error}", + "gateway.viewAndRestart": "查看问题并重启 Gateway", + "gateway.telegramStall": "Telegram 连接异常,建议重启", + "gateway.statusTitle": "Gateway 状态", + "gateway.processRunning": "✅ 运行中", + "gateway.processDown": "❌ 无响应", + "gateway.telegramPolling": "⚠️ Polling 异常", + "gateway.subagentTimeout": "⚠️ 有 timeout 记录", // pixel office "pixelOffice.title": "OpenClaw Agents办公室", @@ -970,6 +1074,45 @@ const translations: Record> = { "skills.contentTitle": "SKILL.md", "skills.loadingContent": "Loading skill content...", "skills.contentLoadFailed": "Failed to load skill content", + "skills.totalPrefix": "Total", + + // workspace page + "workspace.subtitle": "Real-time monitoring of all agent statuses · updated {time}", + "workspace.online": "Online:", + "workspace.workingCount": "Working:", + "workspace.agentStatus": "Agent Status", + "workspace.noAgents": "No agent data", + "workspace.installedSkills": "Installed Skills", + "workspace.totalSkills": "{count} skills total", + "workspace.noSkills": "No skills", + "workspace.lastActive": "Last active:", + "workspace.noRecord": "No record", + "workspace.skillRunning": "Running", + "workspace.skillDisabled": "Disabled", + "workspace.unknown": "Unknown", + + // daily report page + "daily.title": "Daily Report Board", + "daily.report": "Report", + "daily.stop": "Stop", + "daily.completed": "Completed", + "daily.inProgress": "In Progress", + "daily.pending": "Pending", + "daily.noRecords": "No records today", + "daily.speakingHint": "Speaking today's completed items...", + "daily.noSpeechSupport": "Your browser does not support speech synthesis", + "daily.reportCompleted": "Completed {count} tasks today. {items}", + "daily.reportEmpty": "No completed records today", + + // theme + "theme.switchToLight": "Switch to light mode", + "theme.switchToDark": "Switch to dark mode", + + // copy + "common.copy": "Copy", + "common.copied": "Copied", + "common.copyFailed": "Copy failed", + "common.unknownError": "Unknown error", // gateway status "gateway.healthy": "Gateway is running", @@ -994,6 +1137,18 @@ const translations: Record> = { "gateway.reloadNow": "Refresh now", "gateway.reloadHint": "After refresh, click the Test button on each bot card to verify it's working.", "gateway.configCorruptHint": "Config may be corrupt. Use the Gateway panel above to restore a backup.", + "gateway.restart": "Restart", + "gateway.restartGateway": "🔄 Restart Gateway", + "gateway.restarting": "⏳ Restarting…", + "gateway.restartSent": "✅ Restart command sent, rechecking shortly…", + "gateway.restartFailed": "❌ Restart failed: {error}", + "gateway.viewAndRestart": "View issues and restart Gateway", + "gateway.telegramStall": "Telegram connection issue, restart recommended", + "gateway.statusTitle": "Gateway Status", + "gateway.processRunning": "✅ Running", + "gateway.processDown": "❌ Not responding", + "gateway.telegramPolling": "⚠️ Polling issue", + "gateway.subagentTimeout": "⚠️ Timeout records", // pixel office "pixelOffice.title": "OpenClaw Agents Office", @@ -1386,6 +1541,19 @@ export function useI18n() { return useContext(I18nContext); } +export function localeToBcp47(locale: Locale): string { + switch (locale) { + case "en": + return "en-US"; + case "zh-TW": + return "zh-TW"; + case "vi": + return "vi-VN"; + default: + return "zh-CN"; + } +} + export function LanguageSwitcher() { const { locale, setLocale } = useI18n(); return ( diff --git a/lib/theme.tsx b/lib/theme.tsx index d11534c..9acdaa5 100644 --- a/lib/theme.tsx +++ b/lib/theme.tsx @@ -1,6 +1,7 @@ "use client"; import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from "react"; +import { useI18n } from "@/lib/i18n"; export type Theme = "dark" | "light"; @@ -50,11 +51,12 @@ export function useTheme() { export function ThemeSwitcher() { const { theme, toggleTheme } = useTheme(); + const { t } = useI18n(); return (