diff --git a/.gitignore b/.gitignore index f43bd7d..4206ae4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,9 @@ build /clawgo_test .gocache -.vscode \ No newline at end of file +.vscode + +cmd/workspace/* +channels + +*.pid \ No newline at end of file diff --git a/webui/src/components/channel/ChannelFieldRenderer.tsx b/webui/src/components/channel/ChannelFieldRenderer.tsx index 2544323..6caf08a 100644 --- a/webui/src/components/channel/ChannelFieldRenderer.tsx +++ b/webui/src/components/channel/ChannelFieldRenderer.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Check, ShieldCheck, Users, Wifi } from 'lucide-react'; -import { CheckboxCardField, FieldBlock, TextField } from '../ui/FormControls'; +import { SwitchCardField, FieldBlock, TextField } from '../ui/FormControls'; import type { ChannelField, ChannelKey } from './channelSchema'; type Translate = (key: string, options?: any) => string; @@ -15,18 +14,6 @@ type ChannelFieldRendererProps = { t: Translate; }; -function getWhatsAppBooleanIcon(fieldKey: string) { - switch (fieldKey) { - case 'enabled': - return Wifi; - case 'enable_groups': - return Users; - case 'require_mention_in_groups': - return ShieldCheck; - default: - return Check; - } -} type TagListFieldProps = { isWhatsApp: boolean; @@ -122,33 +109,9 @@ const ChannelFieldRenderer: React.FC = ({ const helper = getDescription(t, channelKey, field.key); if (field.type === 'boolean') { - if (isWhatsApp) { - const Icon = getWhatsAppBooleanIcon(field.key); - return ( - -
- -
-
-
{helper}
-
- - )} - label={label} - onChange={(checked) => setDraft((prev) => ({ ...prev, [field.key]: checked }))} - /> - ); - } - return ( - = ({ title, }) => { return ( -
+
{icon} diff --git a/webui/src/components/channel/WhatsAppQRCodePanel.tsx b/webui/src/components/channel/WhatsAppQRCodePanel.tsx index 5a23fe5..1116314 100644 --- a/webui/src/components/channel/WhatsAppQRCodePanel.tsx +++ b/webui/src/components/channel/WhatsAppQRCodePanel.tsx @@ -29,14 +29,14 @@ const WhatsAppQRCodePanel: React.FC = ({ } > {qrAvailable ? ( -
+
{t('whatsappBridgeQRCode')}
) : ( diff --git a/webui/src/components/channel/WhatsAppStatusPanel.tsx b/webui/src/components/channel/WhatsAppStatusPanel.tsx index b71f54d..7b2f78c 100644 --- a/webui/src/components/channel/WhatsAppStatusPanel.tsx +++ b/webui/src/components/channel/WhatsAppStatusPanel.tsx @@ -44,18 +44,17 @@ const WhatsAppStatusPanel: React.FC = ({ : } title={ - <> -
{t('gatewayStatus')}
-
{stateLabel}
- +
+
+
{t('gatewayStatus')}
+
{stateLabel}
+
+ + + +
} > -
-
- - - -
diff --git a/webui/src/components/chat/SubagentSidebar.tsx b/webui/src/components/chat/SubagentSidebar.tsx index 56a3d6f..4d95af3 100644 --- a/webui/src/components/chat/SubagentSidebar.tsx +++ b/webui/src/components/chat/SubagentSidebar.tsx @@ -60,7 +60,7 @@ const SubagentSidebar: React.FC = ({ subagentTaskPlaceholder, }) => { return ( -
+
{dispatchTitle}
{dispatchHint}
diff --git a/webui/src/components/chat/SubagentStreamFilters.tsx b/webui/src/components/chat/SubagentStreamFilters.tsx index 815742e..cd8a01d 100644 --- a/webui/src/components/chat/SubagentStreamFilters.tsx +++ b/webui/src/components/chat/SubagentStreamFilters.tsx @@ -19,7 +19,7 @@ const SubagentStreamFilters: React.FC = ({ selectedAgents, }) => { return ( -
+
diff --git a/webui/src/components/config/ConfigPageChrome.tsx b/webui/src/components/config/ConfigPageChrome.tsx index c7467b3..494ab2c 100644 --- a/webui/src/components/config/ConfigPageChrome.tsx +++ b/webui/src/components/config/ConfigPageChrome.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { RefreshCw, Save } from 'lucide-react'; import { Button, FixedButton } from '../ui/Button'; -import { TextField, ToolbarCheckboxField } from '../ui/FormControls'; +import { TextField, ToolbarSwitchField } from '../ui/FormControls'; import CodeBlockPanel from '../data-display/CodeBlockPanel'; import { ModalBackdrop, ModalCard, ModalHeader, ModalShell } from '../ui/ModalFrame'; @@ -66,7 +66,7 @@ export function ConfigToolbar({ - - + {t('close')}} diff --git a/webui/src/components/config/GatewayConfigSection.tsx b/webui/src/components/config/GatewayConfigSection.tsx index 1838343..504b6d2 100644 --- a/webui/src/components/config/GatewayConfigSection.tsx +++ b/webui/src/components/config/GatewayConfigSection.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Plus, Trash2 } from 'lucide-react'; import { FixedButton } from '../ui/Button'; -import { CheckboxField, PanelField, SelectField, TextField, TextareaField } from '../ui/FormControls'; +import { SwitchField, PanelField, SelectField, TextField, TextareaField } from '../ui/FormControls'; type Translate = (key: string) => string; @@ -83,7 +83,7 @@ export function GatewayP2PSection({
- onP2PFieldChange('enabled', e.target.checked)} /> @@ -161,13 +161,13 @@ export function GatewayDispatchSection({
- onDispatchFieldChange('prefer_local', e.target.checked)} /> + onDispatchFieldChange('prefer_local', e.target.checked)} /> - onDispatchFieldChange('prefer_p2p', e.target.checked)} /> + onDispatchFieldChange('prefer_p2p', e.target.checked)} /> - onDispatchFieldChange('allow_relay_fallback', e.target.checked)} /> + onDispatchFieldChange('allow_relay_fallback', e.target.checked)} />
@@ -243,7 +243,7 @@ export function GatewayArtifactsSection({ artifacts, onArtifactsFieldChange, t }
- onArtifactsFieldChange('enabled', e.target.checked)} /> + onArtifactsFieldChange('enabled', e.target.checked)} /> - onArtifactsFieldChange('prune_on_read', e.target.checked)} /> + onArtifactsFieldChange('prune_on_read', e.target.checked)} />
diff --git a/webui/src/components/config/ProviderConfigSection.tsx b/webui/src/components/config/ProviderConfigSection.tsx index 5d6d472..a86996b 100644 --- a/webui/src/components/config/ProviderConfigSection.tsx +++ b/webui/src/components/config/ProviderConfigSection.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { Download, FolderOpen, LogIn, LogOut, Plus, RefreshCw, RotateCcw, ShieldCheck, Trash2, Upload, Wallet, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { Button, FixedButton } from '../ui/Button'; -import { CheckboxField, InlineCheckboxField, PanelField, SelectField, TextField, ToolbarCheckboxField } from '../ui/FormControls'; +import { SwitchField, InlineSwitchField, PanelField, SelectField, TextField, ToolbarSwitchField } from '../ui/FormControls'; function joinClasses(...values: Array) { return values.filter(Boolean).join(' '); @@ -112,43 +113,38 @@ export function ProviderRuntimeToolbar({ t, }: ProviderRuntimeToolbarProps) { return ( -
+
{t('configProxies')}
-
Runtime filters and provider creation are split so the status controls stay attached to each other.
+
{t('providersRuntimeToolbarDesc')}
-
- - -
- Runtime - onRuntimeRefreshSecChange(Number(e.target.value || 10))} className="min-w-[124px] bg-zinc-900/70 border-zinc-700"> - - - - - - onRuntimeWindowChange(e.target.value as 'all' | '1h' | '24h' | '7d')} className="min-w-[148px] bg-zinc-900/70 border-zinc-700"> - - - - - -
- onNewProxyNameChange(e.target.value)} placeholder={t('configNewProviderName')} className="min-w-[220px] bg-zinc-900/70 border-zinc-700 xl:w-[280px]" /> - +
+ + onRuntimeRefreshSecChange(Number(e.target.value || 10))} className="min-w-[124px] !w-[124px] bg-zinc-900/70 border-zinc-700"> + + + + + + onRuntimeWindowChange(e.target.value as 'all' | '1h' | '24h' | '7d')} className="min-w-[148px] !w-[148px] bg-zinc-900/70 border-zinc-700"> + + + + + + onNewProxyNameChange(e.target.value)} placeholder={t('configNewProviderName')} className="min-w-[220px] !w-[220px] xl:!w-[280px] bg-zinc-900/70 border-zinc-700" /> + +
); @@ -179,38 +175,43 @@ export function ProviderRuntimeSummary({ toggleRuntimeSection, filterRuntimeEvents, }: ProviderRuntimeSummaryProps) { + const { t } = useTranslation(); const hits = filterRuntimeEvents(item?.recent_hits); const errors = filterRuntimeEvents(item?.recent_errors); const changes = filterRuntimeEvents(item?.recent_changes); return (
-
runtime auth: {String(item?.auth || '-')}
-
api key health: {item?.api_state?.health_score ?? 100} · failures: {item?.api_state?.failure_count ?? 0} · cooldown: {item?.api_state?.cooldown_until || '-'}
-
api key token: {item?.api_state?.token_masked || '-'}
-
last success: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}
- {Array.isArray(item?.oauth_accounts) && item.oauth_accounts.length > 0 && ( -
oauth accounts: {item.oauth_accounts.length} · {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || account?.project_id || '-').join(', ')}
- )} +
{t('metaRuntimeAuth')}: {String(item?.auth || '-')}
+ {item?.api_state ? ( +
+
{t('metaApiKeyHealth')}: {item?.api_state?.health_score ?? 100} · {t('metaFailures')}: {item?.api_state?.failure_count ?? 0} · {t('metaCooldown')}: {item?.api_state?.cooldown_until || '-'}
+
+ ) : null} +
{t('metaApiKeyToken')}: {item?.api_state?.token_masked || '-'}
+
{t('metaLastSuccess')}: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}
+ {Array.isArray(item.oauth_accounts) && item.oauth_accounts.length > 0 ? ( +
{t('metaOAuthAccounts')}: {item.oauth_accounts.length} · {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || account?.project_id || '-').join(', ')}
+ ) : null}
- + - + - + - +
-
candidate order
+
{t('providersCandidateOrder')}
{runtimeSectionOpen('candidates') && Array.isArray(item?.candidate_order) && item.candidate_order.length > 0 ? ( @@ -219,9 +220,11 @@ export function ProviderRuntimeSummary({
{idx + 1}. {candidate?.kind || '-'}
{candidate?.target || '-'}
-
status: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}
-
health: {candidate?.health_score ?? 100} · failures: {candidate?.failure_count ?? 0}
-
cooldown: {candidate?.cooldown_until || '-'}
+
+
{t('metaStatus')}: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}
+
{t('metaHealth')}: {candidate?.health_score ?? 100} · {t('metaFailures')}: {candidate?.failure_count ?? 0}
+
{t('metaCooldown')}: {candidate?.cooldown_until || '-'}
+
))}
@@ -231,27 +234,27 @@ export function ProviderRuntimeSummary({
-
recent hits
+
{t('providersRecentHits')}
{runtimeSectionOpen('hits') ? renderRuntimeEventList(hits, `${name}-hit`) :
-
}
-
recent errors
+
{t('providersRecentErrors')}
{runtimeSectionOpen('errors') ? renderRuntimeEventList(errors, `${name}-error`) :
-
}
-
recent changes
+
{t('providersRecentChanges')}
{runtimeSectionOpen('changes') ? renderRuntimeEventList(changes, `${name}-change`) :
-
} @@ -279,40 +282,44 @@ export function ProviderRuntimeDrawer({ onExportHistory, renderRuntimeEventList, }: ProviderRuntimeDrawerProps) { + const { t } = useTranslation(); return (
- - + + +
-
auth: {String(item?.auth || '-')}
-
last success: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}
- {Array.isArray(item?.oauth_accounts) && item.oauth_accounts.length > 0 && ( -
oauth accounts: {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || '-').join(', ')}
- )} -
+
{t('metaAuth')}: {String(item?.auth || '-')}
+
{t('metaLastSuccess')}: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}
+ {Array.isArray(item.oauth_accounts) && item.oauth_accounts.length > 0 ? ( +
+
{t('metaOAuthAccounts')}: {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || '-').join(', ')}
+
+ ) : null}
-
oauth accounts
+
{t('providersOAuthAccounts')}
{Array.isArray(item?.oauth_accounts) && item.oauth_accounts.length > 0 ? (
{item.oauth_accounts.map((account: any, idx: number) => (
{account?.account_label || account?.email || account?.account_id || '-'}
{account?.credential_file || '-'}
-
project: {account?.project_id || '-'}
-
device: {account?.device_id || '-'}
-
resource: {account?.resource_url || '-'}
+
+
{t('metaProject')}: {account?.project_id || '-'}
+
{t('metaDevice')}: {account?.device_id || '-'}
+
+
{t('metaResource')}: {account?.resource_url || '-'}
))}
@@ -321,16 +328,18 @@ export function ProviderRuntimeDrawer({ )}
-
candidate order
+
{t('providersCandidateOrder')}
{Array.isArray(item?.candidate_order) && item.candidate_order.length > 0 ? (
{item.candidate_order.map((candidate: any, idx: number) => (
{idx + 1}. {candidate?.kind || '-'}
{candidate?.target || '-'}
-
status: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}
-
health: {candidate?.health_score ?? 100} · failures: {candidate?.failure_count ?? 0}
-
cooldown: {candidate?.cooldown_until || '-'}
+
+
{t('metaStatus')}: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}
+
{t('metaHealth')}: {candidate?.health_score ?? 100} · {t('metaFailures')}: {candidate?.failure_count ?? 0}
+
{t('metaCooldown')}: {candidate?.cooldown_until || '-'}
+
))}
@@ -339,15 +348,15 @@ export function ProviderRuntimeDrawer({ )}
-
recent hits
+
{t('providersRecentHits')}
{renderRuntimeEventList(filterRuntimeEvents(item?.recent_hits), 'drawer-hit')}
-
recent errors
+
{t('providersRecentErrors')}
{renderRuntimeEventList(filterRuntimeEvents(item?.recent_errors), 'drawer-error')}
-
recent changes
+
{t('providersRecentChanges')}
{renderRuntimeEventList(filterRuntimeEvents(item?.recent_changes), 'drawer-change')}
@@ -391,6 +400,7 @@ export function ProviderProxyCard({ runtimeSummary, t, }: ProviderProxyCardProps) { + const { t: ti } = useTranslation(); const authMode = String(proxy?.auth || 'oauth'); const providerModels = Array.isArray(proxy?.models) ? proxy.models.map((value: any) => String(value || '').trim()).filter(Boolean) @@ -408,35 +418,35 @@ export function ProviderProxyCard({ ? null : lastQuotaError ? { - label: '额度受限', - tone: 'border-amber-500/30 bg-amber-500/10 text-amber-200', - detail: `最近一次限额命中:${String(lastQuotaError?.when || '-')}`, - } + label: t('providersQuotaLimited'), + tone: 'border-amber-500/30 bg-amber-500/10 text-amber-200', + detail: ti('providersQuotaLimitedDetail', { when: String(lastQuotaError?.when || '-') }), + } : oauthAccounts.some((account) => String(account?.cooldown_until || '').trim()) ? { - label: '冷却中', - tone: 'border-orange-500/30 bg-orange-500/10 text-orange-200', - detail: oauthAccounts - .map((account) => String(account?.cooldown_until || '').trim()) - .find(Boolean) || '-', - } + label: t('providersQuotaCooldown'), + tone: 'border-orange-500/30 bg-orange-500/10 text-orange-200', + detail: oauthAccounts + .map((account) => String(account?.cooldown_until || '').trim()) + .find(Boolean) || '-', + } : oauthAccounts.some((account) => Number(account?.health_score || 100) < 60) ? { - label: '健康偏低', - tone: 'border-rose-500/30 bg-rose-500/10 text-rose-200', - detail: `最低健康分 ${Math.min(...oauthAccounts.map((account) => Number(account?.health_score || 100)))}`, - } + label: t('providersQuotaHealthLow'), + tone: 'border-rose-500/30 bg-rose-500/10 text-rose-200', + detail: ti('providersQuotaHealthLowDetail', { score: Math.min(...oauthAccounts.map((account) => Number(account?.health_score || 100))) }), + } : connected ? { - label: '可用', - tone: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200', - detail: '当前账号可参与 OAuth 轮换。', - } + label: t('providersQuotaHealthy'), + tone: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200', + detail: t('providersQuotaHealthyDetail'), + } : { - label: '未登录', - tone: 'border-zinc-700 bg-zinc-900/50 text-zinc-300', - detail: '还没有可用的 OAuth 账号。', - }; + label: t('providersOAuthDisconnected'), + tone: 'border-zinc-700 bg-zinc-900/50 text-zinc-300', + detail: t('providersQuotaNoAccountDetail'), + }; const quotaTone = quotaState?.tone || 'border-zinc-700 bg-zinc-900/50 text-zinc-300'; const oauthStatusText = oauthAccountsLoading ? t('providersOAuthLoading') @@ -471,14 +481,14 @@ export function ProviderProxyCard({
{showOAuth - ? 'Configure connection first, then choose the OAuth provider and start login.' - : 'Pick an auth mode first. OAuth and Hybrid will open the login workflow.'} + ? t('providersOAuthConfigHint') + : t('providersAuthPickHint')}
{runtimeSummary ? ( ) : null} @@ -493,8 +503,8 @@ export function ProviderProxyCard({
1
-
Connection
-
Base URL, API key, and model routing.
+
{t('providersSectionConnection')}
+
{t('providersSectionConnectionDesc')}
@@ -517,7 +527,7 @@ export function ProviderProxyCard({
3
{t('providersOAuthSetup')}
-
Select provider, then login or import.
+
{t('providersSectionOAuthSetupDesc')}
@@ -559,13 +569,13 @@ export function ProviderProxyCard({
- + onFieldChange('oauth.cooldown_sec', Number(e.target.value || 0))} placeholder={t('providersCooldownSec')} className="w-full" /> - + onFieldChange('oauth.refresh_scan_sec', Number(e.target.value || 0))} placeholder={t('providersRefreshScanSec')} className="w-full" /> - + onFieldChange('oauth.refresh_lead_sec', Number(e.target.value || 0))} placeholder={t('providersRefreshLeadSec')} className="w-full" />
@@ -613,31 +623,31 @@ export function ProviderProxyCard({
-
登录状态
+
{t('providersOAuthLoginStatus')}
- {connected ? `已登录 ${oauthAccountCount} 个账号` : '尚未登录'} + {connected ? ti('providersLoggedInCount', { count: oauthAccountCount }) : t('providersNotLoggedIn')}
{connected - ? (oauthAccounts[0]?.account_label || oauthAccounts[0]?.email || oauthAccounts[0]?.account_id || '主账号已加载') - : '点击 OAuth 登录或导入授权文件后,这里会自动显示账号。'} + ? (oauthAccounts[0]?.account_label || oauthAccounts[0]?.email || oauthAccounts[0]?.account_id || t('providersPrimaryLoaded')) + : t('providersLoginOAuthOrImportHint')}
- {connected ? 'Connected' : 'Disconnected'} + {connected ? t('providersConnected') : t('providersDisconnected')}
-
额度状态
+
{t('providersQuotaStatus')}
{quotaState?.label || '-'}
-
{quotaState?.detail || '后端暂未提供真实余额接口,这里展示现有的 quota/cooldown/health 信号。'}
+
{quotaState?.detail || t('providersQuotaDefaultHelp')}
{lastQuotaError ? 'Quota' : connected ? 'Runtime' : 'Pending'} @@ -653,8 +663,8 @@ export function ProviderProxyCard({
2
-
Authentication
-
Choose how this provider authenticates requests.
+
{t('providersSectionAuth')}
+
{t('providersSectionAuthDesc')}
@@ -666,7 +676,7 @@ export function ProviderProxyCard({
- {showOAuth ? 'Model selection stays on this provider. Hybrid only switches credentials inside the same provider.' : ( + {showOAuth ? t('providersOAuthHybridHint') : ( <> {t('providersSwitchAuthBefore')} oauth @@ -685,34 +695,32 @@ export function ProviderProxyCard({
4
{t('providersOAuthAccounts')}
-
Imported sessions.
+
{t('providersSectionOAuthAccountsDesc')}
-
+
{oauthAccountsLoading ? t('providersOAuthLoadingHelp') : connected ? `${oauthStatusText}。${oauthStatusDetail}` : t('providersOAuthEmptyHelp')}
-
+
{connected - ? `已自动加载 ${oauthAccountCount} 个 OAuth 账号。当前主账号:${oauthAccounts[0]?.account_label || oauthAccounts[0]?.email || oauthAccounts[0]?.account_id || '-'}` - : '当前没有可用账号。可以直接点击左侧 OAuth 登录,或者导入 auth.json。'} + ? ti('providersAutoLoadedCount', { count: oauthAccountCount, primary: oauthAccounts[0]?.account_label || oauthAccounts[0]?.email || oauthAccounts[0]?.account_id || '-' }) + : t('providersNoAccountsAvailableHint')}
{oauthAccountsLoading ? (
{t('providersOAuthLoading')}
@@ -725,17 +733,16 @@ export function ProviderProxyCard({
{account?.email || account?.account_id || account?.credential_file}
-
- {String(account?.cooldown_until || '').trim() ? '冷却中' : Number(account?.health_score || 100) < 60 ? '受限' : '在线'} +
+ {String(account?.cooldown_until || '').trim() ? t('providersAccountCooldown') : Number(account?.health_score || 100) < 60 ? t('providersAccountLimited') : t('providersAccountOnline')}
-
label: {account?.account_label || account?.email || account?.account_id || '-'}
+
{t('metaLabel')}: {account?.account_label || account?.email || account?.account_id || '-'}
{account?.credential_file}
{(account?.balance_label || account?.plan_type) ? (
@@ -748,9 +755,13 @@ export function ProviderProxyCard({ {t('providersSubscriptionUntil')}: {account?.subscription_active_until}
) : null} -
project: {account?.project_id || '-'} · device: {account?.device_id || '-'}
-
proxy: {account?.network_proxy || '-'}
-
expire: {account?.expire || '-'} · cooldown: {account?.cooldown_until || '-'}
+
+
{t('metaProject')}: {account?.project_id || '-'} · {t('metaDevice')}: {account?.device_id || '-'}
+
+
{t('metaProxy')}: {account?.network_proxy || '-'}
+
+
{t('metaExpire')}: {account?.expire || '-'} · {t('metaCooldown')}: {account?.cooldown_until || '-'}
+
{t('providersQuotaStatus')}: {String(account?.cooldown_until || '').trim() ? `${t('providersQuotaCooldown')} · ${account?.cooldown_until || '-'}` @@ -760,16 +771,18 @@ export function ProviderProxyCard({ ? `${t('providersQuotaLimited')} · ${String(lastQuotaError?.when || '-')}` : t('providersQuotaHealthy')}
-
health: {Number(account?.health_score || 100)} · failures: {Number(account?.failure_count || 0)} · last failure: {account?.last_failure || '-'}
+
+
{t('metaHealth')}: {Number(account?.health_score || 100)} · {t('metaFailures')}: {Number(account?.failure_count || 0)} · {t('metaLastFailure')}: {account?.last_failure || '-'}
+
- onRefreshOAuthAccount(String(account?.credential_file || ''))} variant="neutral" radius="lg" label="Refresh"> + onRefreshOAuthAccount(String(account?.credential_file || ''))} variant="neutral" radius="lg" label={t('refresh')}> - onClearOAuthCooldown(String(account?.credential_file || ''))} variant="neutral" radius="lg" label="Clear Cooldown"> + onClearOAuthCooldown(String(account?.credential_file || ''))} variant="neutral" radius="lg" label={t('providersClearCooldown')}> - onDeleteOAuthAccount(String(account?.credential_file || ''))} variant="danger" radius="lg" label="Logout"> + onDeleteOAuthAccount(String(account?.credential_file || ''))} variant="danger" radius="lg" label={t('providersLogout')}>
@@ -785,15 +798,15 @@ export function ProviderProxyCard({
5
-
Advanced
-
Low-frequency runtime settings.
+
{t('providersSectionAdvanced')}
+
{t('providersSectionAdvancedDesc')}
- setRuntimeOpen((v) => !v)} >
-
Runtime
-
Health, candidate order, recent hits and errors.
+
{t('providersRuntimeTitle')}
+
{t('providersRuntimeDesc')}
-
{runtimeOpen ? 'Collapse' : 'Expand'}
+
{runtimeOpen ? t('collapse') : t('expand')}
{runtimeOpen ?
{runtimeSummary}
: null}
diff --git a/webui/src/components/data-display/EmptyState.tsx b/webui/src/components/data-display/EmptyState.tsx index a43c927..476109d 100644 --- a/webui/src/components/data-display/EmptyState.tsx +++ b/webui/src/components/data-display/EmptyState.tsx @@ -31,7 +31,7 @@ const EmptyState: React.FC = ({ 'text-sm text-zinc-500', centered && 'text-center', padded && 'p-4', - panel && 'brand-card border border-zinc-800/80 rounded-[30px]', + panel && 'brand-card border border-zinc-800/80 rounded-2xl', dashed && 'border-dashed', (icon || title) && 'flex flex-col items-center justify-center gap-2', className, diff --git a/webui/src/components/data-display/MetricPanel.tsx b/webui/src/components/data-display/MetricPanel.tsx index e2a113d..827b3c4 100644 --- a/webui/src/components/data-display/MetricPanel.tsx +++ b/webui/src/components/data-display/MetricPanel.tsx @@ -14,7 +14,7 @@ type MetricPanelProps = { valueClassName?: string; }; -const BASE_CLASS_NAME = 'brand-card hover-lift glow-effect ui-border-subtle rounded-[28px] border p-5 min-h-[148px]'; +const BASE_CLASS_NAME = 'brand-card hover-lift glow-effect ui-border-subtle rounded-2xl border p-4 min-h-[120px]'; const MetricPanel: React.FC = ({ className, diff --git a/webui/src/components/data-display/StatCard.tsx b/webui/src/components/data-display/StatCard.tsx index 9788cb0..a8435a5 100644 --- a/webui/src/components/data-display/StatCard.tsx +++ b/webui/src/components/data-display/StatCard.tsx @@ -12,7 +12,7 @@ const StatCard: React.FC = ({ title, value, icon }) => ( whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} transition={{ type: "spring", stiffness: 400, damping: 25 }} - className="brand-card hover-lift h-full min-h-[124px] border border-zinc-800 p-6 flex items-center gap-4 cursor-pointer" + className="brand-card hover-lift h-full min-h-[96px] border border-zinc-800 p-5 flex items-center gap-4 cursor-pointer" >
{icon} diff --git a/webui/src/components/ekg/EKGDistributionCard.tsx b/webui/src/components/ekg/EKGDistributionCard.tsx index ac011b7..bf52353 100644 --- a/webui/src/components/ekg/EKGDistributionCard.tsx +++ b/webui/src/components/ekg/EKGDistributionCard.tsx @@ -15,7 +15,7 @@ const EKGDistributionCard: React.FC = ({ const maxValue = entries.length > 0 ? Math.max(...entries.map(([, value]) => value)) : 0; return ( -
+
{title}
{entries.length === 0 ? ( diff --git a/webui/src/components/ekg/EKGRankingCard.tsx b/webui/src/components/ekg/EKGRankingCard.tsx index e20cdea..06a8180 100644 --- a/webui/src/components/ekg/EKGRankingCard.tsx +++ b/webui/src/components/ekg/EKGRankingCard.tsx @@ -14,7 +14,7 @@ const EKGRankingCard: React.FC = ({ valueMode, }) => { return ( -
+
{title}
{items.length === 0 ? ( diff --git a/webui/src/components/layout/ListPanel.tsx b/webui/src/components/layout/ListPanel.tsx index ff9e775..fca0e42 100644 --- a/webui/src/components/layout/ListPanel.tsx +++ b/webui/src/components/layout/ListPanel.tsx @@ -16,7 +16,7 @@ const ListPanel: React.FC = ({ header, }) => { return ( -
+
{header} {children}
diff --git a/webui/src/components/layout/SectionPanel.tsx b/webui/src/components/layout/SectionPanel.tsx index 93e704c..0c8398c 100644 --- a/webui/src/components/layout/SectionPanel.tsx +++ b/webui/src/components/layout/SectionPanel.tsx @@ -39,14 +39,14 @@ const SectionPanel: React.FC = ({ variants={containerVariants} initial="hidden" animate="visible" - className={joinClasses('brand-card glass-panel rounded-[30px] border border-zinc-800/80 p-6 glow-effect', className)} + className={joinClasses('brand-card glass-panel rounded-2xl border border-zinc-800/80 p-5 glow-effect', className)} >
{title || subtitle || actions || icon ? (
{title ? ( diff --git a/webui/src/components/mcp/MCPServerCard.tsx b/webui/src/components/mcp/MCPServerCard.tsx index d448c17..4777f94 100644 --- a/webui/src/components/mcp/MCPServerCard.tsx +++ b/webui/src/components/mcp/MCPServerCard.tsx @@ -62,21 +62,21 @@ const MCPServerCard: React.FC = ({
{String(server?.package || '-')} {Array.isArray(server?.args) ? server.args.length : 0} diff --git a/webui/src/components/mcp/MCPServerEditor.tsx b/webui/src/components/mcp/MCPServerEditor.tsx index f684da7..1a5c0e5 100644 --- a/webui/src/components/mcp/MCPServerEditor.tsx +++ b/webui/src/components/mcp/MCPServerEditor.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Package, Wrench } from 'lucide-react'; import { Button, FixedButton } from '../ui/Button'; -import { CheckboxCardField, SelectField, TextField } from '../ui/FormControls'; +import { SwitchCardField, SelectField, TextField } from '../ui/FormControls'; import NoticePanel from '../layout/NoticePanel'; type MCPDraftServer = { @@ -79,7 +79,7 @@ const MCPServerEditor: React.FC = ({
enabled
- = ({ {draft.transport === 'stdio' ? (
-
+
-
Args
-
{t('configMCPArgsEnterHint')}
+
{t('mcpArgs')}
+
{t('configMCPArgsHint', { defaultValue: 'Press Enter to add arguments' })}
- +
diff --git a/webui/src/components/subagentProfiles/ProfileEditorPanel.tsx b/webui/src/components/subagentProfiles/ProfileEditorPanel.tsx index 4d5f1de..e7d19e0 100644 --- a/webui/src/components/subagentProfiles/ProfileEditorPanel.tsx +++ b/webui/src/components/subagentProfiles/ProfileEditorPanel.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Save, Trash2 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button, FixedButton } from '../ui/Button'; -import { CheckboxCardField, FieldBlock, SelectField, TextField, TextareaField } from '../ui/FormControls'; +import { SwitchCardField, FieldBlock, SelectField, TextField, TextareaField } from '../ui/FormControls'; import type { SubagentProfile, ToolAllowlistGroup } from './profileDraft'; import { parseAllowlist } from './profileDraft'; @@ -67,16 +67,17 @@ const ProfileEditorPanel: React.FC = ({ const allowlistText = (draft.tool_allowlist || []).join(', '); const statusEnabled = (draft.status || 'active') === 'active'; const notifyPolicyOptions = [ - { value: 'final_only', label: '仅最终结果', help: '只在任务完成后通知主代理。' }, - { value: 'internal_only', label: '仅内部事件', help: '只回传中间过程,不单独强调最终结果。' }, - { value: 'milestone', label: '关键节点', help: '到达关键阶段时通知主代理。' }, - { value: 'on_blocked', label: '遇阻才通知', help: '只有卡住、需要介入时才通知主代理。' }, - { value: 'always', label: '始终通知', help: '过程和结果都会尽量通知主代理。' }, + { value: 'final_only', label: t('profileNotifyFinalOnlyLabel'), help: t('profileNotifyFinalOnlyHelp') }, + { value: 'internal_only', label: t('profileNotifyInternalOnlyLabel'), help: t('profileNotifyInternalOnlyHelp') }, + { value: 'milestone', label: t('profileNotifyMilestoneLabel'), help: t('profileNotifyMilestoneHelp') }, + { value: 'on_blocked', label: t('profileNotifyOnBlockedLabel'), help: t('profileNotifyOnBlockedHelp') }, + { value: 'always', label: t('profileNotifyAlwaysLabel'), help: t('profileNotifyAlwaysHelp') }, ]; const notifyPolicy = notifyPolicyOptions.find((option) => option.value === (draft.notify_main_policy || 'final_only')) || notifyPolicyOptions[0]; return ( -
+
+
= ({ onChange={(e) => onChange({ ...draft, agent_id: e.target.value })} dense className="w-full disabled:opacity-60" - placeholder="coder" + placeholder={t('profileIdPlaceholder')} /> @@ -94,7 +95,7 @@ const ProfileEditorPanel: React.FC = ({ onChange={(e) => onChange({ ...draft, name: e.target.value })} dense className="w-full" - placeholder="Code Agent" + placeholder={t('profileNamePlaceholder')} /> @@ -103,22 +104,21 @@ const ProfileEditorPanel: React.FC = ({ onChange={(e) => onChange({ ...draft, role: e.target.value })} dense className="w-full" - placeholder="coding" + placeholder={t('profileRolePlaceholder')} /> - onChange({ ...draft, status: checked ? 'active' : 'disabled' })} /> = ({ onChange={(e) => onChange({ ...draft, system_prompt_file: e.target.value.replace(/\\/g, '/') })} dense className={`w-full ${promptPathInvalid ? 'border-rose-400/70 focus:border-rose-300' : ''}`} - placeholder="agents/coder/AGENT.md" + placeholder={t('profilePromptFilePlaceholder')} />
{promptPathHint}
@@ -148,7 +148,7 @@ const ProfileEditorPanel: React.FC = ({ onChange={(e) => onChange({ ...draft, memory_namespace: e.target.value })} dense className="w-full" - placeholder="coder" + placeholder={t('profileNamespacePlaceholder')} /> @@ -157,7 +157,7 @@ const ProfileEditorPanel: React.FC = ({ onChange={(e) => onChange({ ...draft, tool_allowlist: parseAllowlist(e.target.value) })} dense className="w-full" - placeholder="read_file, list_files, memory_search" + placeholder={t('profileAllowlistPlaceholder')} />
{toolAllowlistHint}
{groups.length > 0 ? ( @@ -170,7 +170,7 @@ const ProfileEditorPanel: React.FC = ({
) : null} - + onPromptContentChange(e.target.value)} @@ -226,7 +226,8 @@ const ProfileEditorPanel: React.FC = ({ />
-
+
+
diff --git a/webui/src/pages/Config.tsx b/webui/src/pages/Config.tsx index 192918c..09f7d88 100644 --- a/webui/src/pages/Config.tsx +++ b/webui/src/pages/Config.tsx @@ -100,7 +100,7 @@ const Config: React.FC = () => { t={t} /> -
+
{!showRaw ? (
{ {cron.map((j) => { const schedule = formatSchedule(j, t); return ( -
+

{j.name || j.id}

@@ -344,15 +344,15 @@ const Cron: React.FC = () => {
- setCronForm({ ...cronForm, deliver: checked })} /> - setCronForm({ ...cronForm, enabled: checked })} /> diff --git a/webui/src/pages/Dashboard.tsx b/webui/src/pages/Dashboard.tsx index 4f12644..8b56357 100644 --- a/webui/src/pages/Dashboard.tsx +++ b/webui/src/pages/Dashboard.tsx @@ -62,7 +62,7 @@ const Dashboard: React.FC = () => { }, [nodeAlerts]); return ( -
+
{ )} -
+
} className="min-h-[340px] h-full">
{recentTasks.length === 0 ? ( diff --git a/webui/src/pages/LogCodes.tsx b/webui/src/pages/LogCodes.tsx index 5b5daee..f4ea0f5 100644 --- a/webui/src/pages/LogCodes.tsx +++ b/webui/src/pages/LogCodes.tsx @@ -56,7 +56,7 @@ const LogCodes: React.FC = () => { } /> -
+
diff --git a/webui/src/pages/Logs.tsx b/webui/src/pages/Logs.tsx index c2cd3ce..6966943 100644 --- a/webui/src/pages/Logs.tsx +++ b/webui/src/pages/Logs.tsx @@ -95,18 +95,20 @@ const Logs: React.FC = () => { }; return ( -
+
-
- {isStreaming ? t('live') : t('paused')} + title={ +
+ {t('logs')} +
+
+ {isStreaming ? t('live') : t('paused')} +
} + titleClassName="ui-text-primary flex items-center gap-3" actions={ - {
{skills.map(s => ( -
+
diff --git a/webui/src/pages/SubagentProfiles.tsx b/webui/src/pages/SubagentProfiles.tsx index abdc021..16ef48d 100644 --- a/webui/src/pages/SubagentProfiles.tsx +++ b/webui/src/pages/SubagentProfiles.tsx @@ -238,7 +238,7 @@ const SubagentProfiles: React.FC = () => {
{ promptPlaceholder={t('agentPromptContentPlaceholder')} promptPathHint={promptPathHint} promptPathInvalid={!!promptPathErrorKey} - roleLabel="Role" + roleLabel={t('profileRoleLabel')} saving={saving} statusLabel={t('status')} - toolAllowlistHint={<>skill_exec is inherited automatically and does not need to be listed here.} + toolAllowlistHint={<>skill_exec {t('profileToolAllowlistInheritHint')}} toolAllowlistLabel={t('toolAllowlist')} maxRetriesLabel={t('maxRetries')} retryBackoffLabel={t('retryBackoffMs')} - maxTaskCharsLabel="Max Task Chars" - maxResultCharsLabel="Max Result Chars" + maxTaskCharsLabel={t('profileMaxTaskCharsLabel')} + maxResultCharsLabel={t('profileMaxResultCharsLabel')} />
diff --git a/webui/src/pages/TaskAudit.tsx b/webui/src/pages/TaskAudit.tsx index d9cb6a9..2e166b1 100644 --- a/webui/src/pages/TaskAudit.tsx +++ b/webui/src/pages/TaskAudit.tsx @@ -111,15 +111,15 @@ const TaskAudit: React.FC = () => { title={t('taskAudit')} titleClassName="text-xl md:text-2xl font-semibold" actions={( - - setSourceFilter(e.target.value)}> + + setSourceFilter(e.target.value)}> - setStatusFilter(e.target.value)}> + setStatusFilter(e.target.value)}>