mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 04:23:28 +00:00
Fix remaining hardcoded Chinese UI strings for English locale
This commit is contained in:
@@ -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<boolean> {
|
||||
}
|
||||
|
||||
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")}
|
||||
</button>
|
||||
{copyState === "failed" && (
|
||||
<span className="text-[10px] text-amber-300">复制失败</span>
|
||||
<span className="text-[10px] text-amber-300">{t("common.copyFailed")}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap break-words">{errorText}</span>
|
||||
|
||||
+24
-31
@@ -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<DailyItem[]>([]);
|
||||
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 (
|
||||
<main className="min-h-screen p-4 md:p-8 max-w-3xl mx-auto">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">📅 今日播报板</h1>
|
||||
<h1 className="text-2xl font-bold">📅 {t('daily.title')}</h1>
|
||||
<p className="text-sm text-[var(--text-muted)] mt-1">{today}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -96,42 +92,40 @@ function DailyBoard() {
|
||||
>
|
||||
{speaking ? (
|
||||
<>
|
||||
<span>⏹️</span> 停止
|
||||
<span>⏹️</span> {t('daily.stop')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>🎤</span> 播报
|
||||
<span>🎤</span> {t('daily.report')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计 */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
|
||||
<div className="text-2xl font-bold text-green-400">{completedItems.length}</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">已完成</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">{t('daily.completed')}</div>
|
||||
</div>
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-400">{inProgressItems.length}</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">进行中</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">{t('daily.inProgress')}</div>
|
||||
</div>
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
|
||||
<div className="text-2xl font-bold text-gray-400">
|
||||
{items.length - completedItems.length - inProgressItems.length}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">待开始</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">{t('daily.pending')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 播报列表 */}
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-[var(--text-muted)]">加载中...</div>
|
||||
<div className="p-8 text-center text-[var(--text-muted)]">{t('common.loading')}</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="p-8 text-center text-[var(--text-muted)]">
|
||||
今日暂无记录
|
||||
{t('daily.noRecords')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--border)]">
|
||||
@@ -143,13 +137,13 @@ function DailyBoard() {
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg mt-0.5">
|
||||
{item.status === 'completed' ? '✅' :
|
||||
{item.status === 'completed' ? '✅' :
|
||||
item.status === 'in-progress' ? '🔄' : '⏳'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`text-sm ${
|
||||
item.status === 'completed'
|
||||
? 'text-[var(--text-muted)] line-through'
|
||||
item.status === 'completed'
|
||||
? 'text-[var(--text-muted)] line-through'
|
||||
: ''
|
||||
}`}>
|
||||
{item.content}
|
||||
@@ -164,10 +158,9 @@ function DailyBoard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 提示 */}
|
||||
{speaking && (
|
||||
<div className="mt-4 p-3 rounded-lg bg-blue-500/10 border border-blue-500/30 text-xs text-blue-400">
|
||||
🎤 正在语音播报今日完成事项...
|
||||
🎤 {t('daily.speakingHint')}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
+13
-13
@@ -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 ? (
|
||||
<span
|
||||
className={compact ? "text-yellow-400 text-xs cursor-pointer" : "text-yellow-400 text-sm cursor-pointer"}
|
||||
title="Telegram 連線異常,建議重啟"
|
||||
title={t("gateway.telegramStall")}
|
||||
onClick={handleDetailClick}
|
||||
>⚠️</span>
|
||||
) : (
|
||||
@@ -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")}`}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -280,7 +280,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
|
||||
{showDetail && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 rounded-lg bg-[var(--card)] border border-[var(--border)] shadow-xl text-xs w-72 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b border-[var(--border)] flex items-center justify-between">
|
||||
<span className="font-semibold text-[var(--text)]">Gateway 狀態</span>
|
||||
<span className="font-semibold text-[var(--text)]">{t("gateway.statusTitle")}</span>
|
||||
<button onClick={() => setShowDetail(false)} className="text-[var(--text-muted)] hover:text-[var(--text)] cursor-pointer">✕</button>
|
||||
</div>
|
||||
|
||||
@@ -289,7 +289,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[var(--text-muted)]">Process:</span>
|
||||
<span className={health?.ok ? "text-green-400" : "text-red-400"}>
|
||||
{health?.ok ? "✅ 運作中" : "❌ 無回應"}
|
||||
{health?.ok ? t("gateway.processRunning") : t("gateway.processDown")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -298,10 +298,10 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-[var(--text-muted)] shrink-0">Telegram:</span>
|
||||
<span className="text-yellow-400">
|
||||
⚠️ Polling 異常
|
||||
⚠️ {t("gateway.telegramPolling")}
|
||||
{logResult.lastStallAt && (
|
||||
<span className="text-[var(--text-muted)] ml-1">
|
||||
({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" })})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -312,7 +312,7 @@ export function GatewayStatus({ compact = false, className = "", hideIconOnMobil
|
||||
{logResult && logResult.issues.includes("subagent_timeout") && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[var(--text-muted)]">Subagent:</span>
|
||||
<span className="text-orange-400">⚠️ 有 timeout 記錄</span>
|
||||
<span className="text-orange-400">⚠️ {t("gateway.subagentTimeout")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -268,7 +268,7 @@ export default function SkillsPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{t("skills.title")}</h1>
|
||||
<p className="text-[var(--text-muted)] text-sm mt-1">
|
||||
共 {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})
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
|
||||
+65
-41
@@ -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<AgentState['state'], string> = {
|
||||
working: t('agent.status.working'),
|
||||
online: t('agent.status.online'),
|
||||
idle: t('agent.status.idle'),
|
||||
offline: t('agent.status.offline'),
|
||||
};
|
||||
const stateEmojis: Record<AgentState['state'], string> = {
|
||||
working: '🔥',
|
||||
online: '🟢',
|
||||
idle: '😴',
|
||||
offline: '⚫',
|
||||
};
|
||||
const stateColors: Record<AgentState['state'], string> = {
|
||||
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 (
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 hover:border-[var(--accent)]/50 transition-all">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-3 h-3 rounded-full ${config.color}`} />
|
||||
<span className="text-2xl">{config.emoji}</span>
|
||||
<div className={`w-3 h-3 rounded-full ${color}`} />
|
||||
<span className="text-2xl">{emoji}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-sm truncate">{agent.agentId}</h3>
|
||||
<p className="text-xs text-[var(--text-muted)]">{config.label}</p>
|
||||
<p className="text-xs text-[var(--text-muted)]">{label}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">
|
||||
最后活跃: {lastActiveText}
|
||||
{t('workspace.lastActive')} {lastActiveText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillCard({ skill }: { skill: Skill }) {
|
||||
function SkillCard({ skill, t }: { skill: Skill; t: (key: string) => string }) {
|
||||
return (
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-3 hover:border-[var(--accent)]/50 transition-all">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<h4 className="font-medium text-sm truncate">{skill.name}</h4>
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium ${
|
||||
skill.status === 'running'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
skill.status === 'running'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{skill.status === 'running' ? '运行中' : '已禁用'}
|
||||
{skill.status === 'running' ? t('workspace.skillRunning') : t('workspace.skillDisabled')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-muted)] line-clamp-2">
|
||||
{skill.description || '无描述'}
|
||||
{skill.description || t('skills.noDesc')}
|
||||
</p>
|
||||
<p className="text-[10px] text-[var(--accent)] mt-2">
|
||||
v{skill.version}
|
||||
@@ -70,6 +96,7 @@ function SkillCard({ skill }: { skill: Skill }) {
|
||||
}
|
||||
|
||||
export default function WorkspacePage() {
|
||||
const { t, locale } = useI18n();
|
||||
const [agents, setAgents] = useState<AgentState[]>([]);
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
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 (
|
||||
<main className="min-h-screen p-4 md:p-8 max-w-7xl mx-auto">
|
||||
{/* 头部 */}
|
||||
<div className="flex flex-col gap-4 mb-8 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">🤖 Agent 工作台</h1>
|
||||
<h1 className="text-2xl font-bold">🤖 {t('nav.workspace')}</h1>
|
||||
<p className="text-sm text-[var(--text-muted)] mt-1">
|
||||
实时监控所有 Agent 状态 · {lastRefresh.toLocaleTimeString('zh-CN')} 更新
|
||||
{t('workspace.subtitle').replace('{time}', refreshTime)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
|
||||
<span className="text-[var(--text-muted)]">在线:</span>
|
||||
<span className="text-[var(--text-muted)]">{t('workspace.online')}</span>
|
||||
<span className="font-semibold ml-1">{onlineCount}/{agents.length}</span>
|
||||
</div>
|
||||
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
|
||||
<span className="text-[var(--text-muted)]">工作中:</span>
|
||||
<span className="text-[var(--text-muted)]">{t('workspace.workingCount')}</span>
|
||||
<span className="font-semibold ml-1 text-green-400">{workingCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent 网格 */}
|
||||
<section className="mb-8">
|
||||
<h2 className="text-lg font-semibold mb-4">📊 Agent 状态</h2>
|
||||
<h2 className="text-lg font-semibold mb-4">📊 {t('workspace.agentStatus')}</h2>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)]">加载中...</div>
|
||||
<div className="text-center py-8 text-[var(--text-muted)]">{t('common.loading')}</div>
|
||||
) : agents.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
|
||||
暂无 Agent 数据
|
||||
{t('workspace.noAgents')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard key={agent.agentId} agent={agent} />
|
||||
<AgentCard key={agent.agentId} agent={agent} t={t} locale={locale} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 技能列表 */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">📦 已安装技能</h2>
|
||||
<h2 className="text-lg font-semibold">📦 {t('workspace.installedSkills')}</h2>
|
||||
<span className="text-xs text-[var(--text-muted)]">
|
||||
共 {skills.length} 个技能
|
||||
{t('workspace.totalSkills').replace('{count}', String(skills.length))}
|
||||
</span>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)]">加载中...</div>
|
||||
<div className="text-center py-8 text-[var(--text-muted)]">{t('common.loading')}</div>
|
||||
) : skills.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
|
||||
暂无技能
|
||||
{t('workspace.noSkills')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{skills.map((skill) => (
|
||||
<SkillCard key={skill.name} skill={skill} />
|
||||
<SkillCard key={skill.name} skill={skill} t={t} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+168
@@ -13,6 +13,8 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
// nav sidebar
|
||||
"nav.overview": "總覽",
|
||||
"nav.agents": "機器人",
|
||||
"nav.workspace": "Agent 工作台",
|
||||
"nav.daily": "今日播報",
|
||||
"nav.models": "模型列表",
|
||||
"nav.monitor": "監控",
|
||||
"nav.sessions": "會話列表",
|
||||
@@ -276,6 +278,45 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"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<Locale, Record<string, string>> = {
|
||||
"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<Locale, Record<string, string>> = {
|
||||
"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<Locale, Record<string, string>> = {
|
||||
"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<Locale, Record<string, string>> = {
|
||||
"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<Locale, Record<string, string>> = {
|
||||
"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 (
|
||||
|
||||
+3
-1
@@ -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 (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="px-2 py-1.5 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm hover:border-[var(--accent)] transition cursor-pointer"
|
||||
title={theme === "dark" ? "切换到亮色模式" : "切换到暗色模式"}
|
||||
title={theme === "dark" ? t("theme.switchToLight") : t("theme.switchToDark")}
|
||||
>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user