Update UI components from checkboxes to switches and introduce a recursive configuration component.

This commit is contained in:
lpf
2026-03-12 12:42:09 +08:00
parent 679cae2df0
commit f0a1e9c941
37 changed files with 661 additions and 411 deletions

7
.gitignore vendored
View File

@@ -21,4 +21,9 @@ build
/clawgo_test
.gocache
.vscode
.vscode
cmd/workspace/*
channels
*.pid

View File

@@ -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<ChannelFieldRendererProps> = ({
const helper = getDescription(t, channelKey, field.key);
if (field.type === 'boolean') {
if (isWhatsApp) {
const Icon = getWhatsAppBooleanIcon(field.key);
return (
<CheckboxCardField
key={field.key}
className="ui-boolean-card-detailed"
checked={!!value}
help={(
<div className="ui-boolean-head">
<div className={`ui-pill ${value ? 'ui-pill-success' : 'ui-pill-neutral'} flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl border`}>
<Icon className="h-4 w-4" />
</div>
<div className="min-w-0">
<div className="ui-form-help">{helper}</div>
</div>
</div>
)}
label={label}
onChange={(checked) => setDraft((prev) => ({ ...prev, [field.key]: checked }))}
/>
);
}
return (
<CheckboxCardField
<SwitchCardField
key={field.key}
className="ui-boolean-card"
checked={!!value}
help={helper}
label={label}

View File

@@ -14,7 +14,7 @@ const ChannelSectionCard: React.FC<ChannelSectionCardProps> = ({
title,
}) => {
return (
<section className="brand-card ui-panel rounded-[30px] p-6 space-y-5">
<section className="brand-card ui-panel rounded-2xl p-5 space-y-4">
<div className="ui-section-header">
<div className="ui-subpanel flex h-11 w-11 shrink-0 items-center justify-center">
{icon}

View File

@@ -29,14 +29,14 @@ const WhatsAppQRCodePanel: React.FC<WhatsAppQRCodePanelProps> = ({
}
>
{qrAvailable ? (
<div className="ui-soft-panel rounded-[28px] p-5">
<div className="ui-soft-panel rounded-2xl p-5">
<img src={qrImageURL} alt={t('whatsappBridgeQRCode')} className="mx-auto block w-full max-w-[360px]" />
</div>
) : (
<EmptyState
centered
dashed
className="ui-soft-panel rounded-[28px] p-8"
className="ui-soft-panel rounded-2xl p-8"
icon={
<div className="ui-subpanel ui-surface-strong mx-auto flex h-16 w-16 items-center justify-center rounded-3xl">
<Smartphone className="ui-icon-muted h-7 w-7" />

View File

@@ -44,18 +44,17 @@ const WhatsAppStatusPanel: React.FC<WhatsAppStatusPanelProps> = ({
<ChannelSectionCard
icon={status?.connected ? <Wifi className="ui-icon-success h-[18px] w-[18px]" /> : <WifiOff className="ui-icon-warning h-[18px] w-[18px]" />}
title={
<>
<div className="ui-text-muted text-xs uppercase tracking-[0.28em]">{t('gatewayStatus')}</div>
<div className="ui-text-primary mt-1 text-2xl font-semibold">{stateLabel}</div>
</>
<div className="flex items-center justify-between gap-3">
<div>
<div className="ui-text-muted text-xs uppercase tracking-[0.28em]">{t('gatewayStatus')}</div>
<div className="ui-text-primary mt-1 text-2xl font-semibold">{stateLabel}</div>
</div>
<FixedButton onClick={onLogout} variant="danger" label={t('logout')}>
<LogOut className="h-4 w-4" />
</FixedButton>
</div>
}
>
<div className="flex items-start justify-between gap-4">
<div />
<FixedButton onClick={onLogout} variant="danger" label={t('logout')}>
<LogOut className="h-4 w-4" />
</FixedButton>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<InfoTile label={t('whatsappBridgeURL')} className="break-all">

View File

@@ -60,7 +60,7 @@ const SubagentSidebar: React.FC<SubagentSidebarProps> = ({
subagentTaskPlaceholder,
}) => {
return (
<div className="ui-surface-strong ui-border-subtle w-full xl:w-[320px] xl:shrink-0 border-b xl:border-b-0 xl:border-r p-4 flex flex-col gap-4 max-h-[46vh] xl:max-h-none overflow-y-auto">
<div className="bg-zinc-50/50 dark:bg-zinc-900/40 ui-border-subtle w-full xl:w-[320px] xl:shrink-0 border-b xl:border-b-0 xl:border-r p-4 flex flex-col gap-4 max-h-[46vh] xl:max-h-none overflow-y-auto">
<div>
<div className="ui-text-muted text-xs uppercase tracking-wider mb-1">{dispatchTitle}</div>
<div className="ui-text-secondary text-sm">{dispatchHint}</div>

View File

@@ -19,7 +19,7 @@ const SubagentStreamFilters: React.FC<SubagentStreamFiltersProps> = ({
selectedAgents,
}) => {
return (
<div className="ui-surface-strong ui-border-subtle px-4 py-3 border-b flex flex-wrap gap-2">
<div className="bg-zinc-50/50 dark:bg-zinc-900/40 ui-border-subtle px-4 py-3 border-b flex flex-wrap gap-2">
<Button onClick={onReset} variant={selectedAgents.length === 0 ? 'primary' : 'neutral'} size="xs" radius="full">
{allAgentsLabel}
</Button>

View File

@@ -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({
<Button onClick={onToggleBasicMode} size="sm">
{basicMode ? t('configBasicMode') : t('configAdvancedMode')}
</Button>
<ToolbarCheckboxField
<ToolbarSwitchField
checked={hotOnly}
help={t('configHotOnlyHint', { defaultValue: 'Only show fields that support hot reload.' })}
label={t('configHotOnly')}
@@ -126,7 +126,7 @@ export function ConfigDiffModal({ diffRows, onClose, t }: ConfigDiffModalProps)
return (
<ModalShell>
<ModalBackdrop />
<ModalCard className="max-h-[85vh] max-w-4xl rounded-[30px]">
<ModalCard className="max-h-[85vh] max-w-4xl rounded-2xl">
<ModalHeader
title={t('configDiffPreviewCount', { count: diffRows.length })}
actions={<Button onClick={onClose} size="xs" radius="xl">{t('close')}</Button>}

View File

@@ -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({
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<PanelField label={t('enable')}>
<CheckboxField
<SwitchField
checked={Boolean(p2p?.enabled)}
onChange={(e) => onP2PFieldChange('enabled', e.target.checked)}
/>
@@ -161,13 +161,13 @@ export function GatewayDispatchSection({
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<PanelField label={t('configNodeDispatchPreferLocal')}>
<CheckboxField checked={Boolean(dispatch?.prefer_local)} onChange={(e) => onDispatchFieldChange('prefer_local', e.target.checked)} />
<SwitchField checked={Boolean(dispatch?.prefer_local)} onChange={(e) => onDispatchFieldChange('prefer_local', e.target.checked)} />
</PanelField>
<PanelField label={t('configNodeDispatchPreferP2P')}>
<CheckboxField checked={Boolean(dispatch?.prefer_p2p ?? true)} onChange={(e) => onDispatchFieldChange('prefer_p2p', e.target.checked)} />
<SwitchField checked={Boolean(dispatch?.prefer_p2p ?? true)} onChange={(e) => onDispatchFieldChange('prefer_p2p', e.target.checked)} />
</PanelField>
<PanelField label={t('configNodeDispatchAllowRelay')}>
<CheckboxField checked={Boolean(dispatch?.allow_relay_fallback ?? true)} onChange={(e) => onDispatchFieldChange('allow_relay_fallback', e.target.checked)} />
<SwitchField checked={Boolean(dispatch?.allow_relay_fallback ?? true)} onChange={(e) => onDispatchFieldChange('allow_relay_fallback', e.target.checked)} />
</PanelField>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
@@ -243,7 +243,7 @@ export function GatewayArtifactsSection({ artifacts, onArtifactsFieldChange, t }
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<PanelField label={t('enable')}>
<CheckboxField checked={Boolean(artifacts?.enabled)} onChange={(e) => onArtifactsFieldChange('enabled', e.target.checked)} />
<SwitchField checked={Boolean(artifacts?.enabled)} onChange={(e) => onArtifactsFieldChange('enabled', e.target.checked)} />
</PanelField>
<PanelField label={t('configNodeArtifactsKeepLatest')}>
<TextField
@@ -256,7 +256,7 @@ export function GatewayArtifactsSection({ artifacts, onArtifactsFieldChange, t }
/>
</PanelField>
<PanelField label={t('configNodeArtifactsPruneOnRead')}>
<CheckboxField checked={Boolean(artifacts?.prune_on_read ?? true)} onChange={(e) => onArtifactsFieldChange('prune_on_read', e.target.checked)} />
<SwitchField checked={Boolean(artifacts?.prune_on_read ?? true)} onChange={(e) => onArtifactsFieldChange('prune_on_read', e.target.checked)} />
</PanelField>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">

View File

@@ -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<string | undefined | false>) {
return values.filter(Boolean).join(' ');
@@ -112,43 +113,38 @@ export function ProviderRuntimeToolbar({
t,
}: ProviderRuntimeToolbarProps) {
return (
<div className="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_auto] gap-3 items-center">
<div className="flex flex-col xl:flex-row items-start xl:items-center justify-between gap-3">
<div className="space-y-1">
<div className="text-sm font-semibold text-zinc-200">{t('configProxies')}</div>
<div className="text-[11px] text-zinc-500">Runtime filters and provider creation are split so the status controls stay attached to each other.</div>
<div className="text-[11px] text-zinc-500">{t('providersRuntimeToolbarDesc')}</div>
</div>
<div className="flex flex-wrap items-center justify-end gap-2 xl:min-w-[1040px]">
<Button onClick={onRefreshRuntime} size="xs" radius="lg" variant="neutral" gap="2" noShrink>
<RefreshCw className="w-4 h-4" />
{t('providersRefreshRuntime')}
</Button>
<ToolbarCheckboxField
checked={runtimeAutoRefresh}
className="shrink-0"
help="Refresh runtime stats on the selected interval."
label={t('providersAutoRefresh')}
onChange={onRuntimeAutoRefreshChange}
/>
<div className="flex flex-wrap items-center gap-2 rounded-xl border border-zinc-800 bg-zinc-950/25 px-2 py-2">
<span className="px-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-zinc-500">Runtime</span>
<SelectField dense value={String(runtimeRefreshSec)} onChange={(e) => onRuntimeRefreshSecChange(Number(e.target.value || 10))} className="min-w-[124px] bg-zinc-900/70 border-zinc-700">
<option value="2">2s</option>
<option value="5">5s</option>
<option value="10">10s</option>
<option value="30">30s</option>
</SelectField>
<SelectField dense value={runtimeWindow} onChange={(e) => onRuntimeWindowChange(e.target.value as 'all' | '1h' | '24h' | '7d')} className="min-w-[148px] bg-zinc-900/70 border-zinc-700">
<option value="1h">{t('providersRuntime1h')}</option>
<option value="24h">{t('providersRuntime24h')}</option>
<option value="7d">{t('providersRuntime7d')}</option>
<option value="all">{t('providersRuntimeAll')}</option>
</SelectField>
</div>
<TextField dense value={newProxyName} onChange={(e) => onNewProxyNameChange(e.target.value)} placeholder={t('configNewProviderName')} className="min-w-[220px] bg-zinc-900/70 border-zinc-700 xl:w-[280px]" />
<Button onClick={onAddProxy} variant="primary" size="xs" radius="lg" gap="2" noShrink>
<Plus className="w-4 h-4" />
{t('add')}
</Button>
<div className="flex flex-wrap items-center xl:justify-end gap-2 shrink-0">
<ToolbarSwitchField
checked={runtimeAutoRefresh}
className="shrink-0"
label={t('providersAutoRefresh')}
onChange={onRuntimeAutoRefreshChange}
/>
<SelectField dense value={String(runtimeRefreshSec)} onChange={(e) => onRuntimeRefreshSecChange(Number(e.target.value || 10))} className="min-w-[124px] !w-[124px] bg-zinc-900/70 border-zinc-700">
<option value="2">2s</option>
<option value="5">5s</option>
<option value="10">10s</option>
<option value="30">30s</option>
</SelectField>
<SelectField dense value={runtimeWindow} onChange={(e) => onRuntimeWindowChange(e.target.value as 'all' | '1h' | '24h' | '7d')} className="min-w-[148px] !w-[148px] bg-zinc-900/70 border-zinc-700">
<option value="1h">{t('providersRuntime1h')}</option>
<option value="24h">{t('providersRuntime24h')}</option>
<option value="7d">{t('providersRuntime7d')}</option>
<option value="all">{t('providersRuntimeAll')}</option>
</SelectField>
<TextField dense value={newProxyName} onChange={(e) => onNewProxyNameChange(e.target.value)} placeholder={t('configNewProviderName')} className="min-w-[220px] !w-[220px] xl:!w-[280px] bg-zinc-900/70 border-zinc-700" />
<Button onClick={onRefreshRuntime} size="xs" radius="lg" variant="neutral" gap="2" noShrink>
<RefreshCw className="w-4 h-4" />
</Button>
<Button onClick={onAddProxy} variant="primary" size="xs" radius="lg" gap="2" noShrink>
<Plus className="w-4 h-4" />
{t('add')}
</Button>
</div>
</div>
);
@@ -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 (
<div className="md:col-span-7 rounded-lg border border-zinc-800 bg-zinc-950/40 p-2 text-[11px] text-zinc-400">
<div>runtime auth: {String(item?.auth || '-')}</div>
<div>api key health: {item?.api_state?.health_score ?? 100} · failures: {item?.api_state?.failure_count ?? 0} · cooldown: {item?.api_state?.cooldown_until || '-'}</div>
<div>api key token: {item?.api_state?.token_masked || '-'}</div>
<div>last success: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}</div>
{Array.isArray(item?.oauth_accounts) && item.oauth_accounts.length > 0 && (
<div>oauth accounts: {item.oauth_accounts.length} · {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || account?.project_id || '-').join(', ')}</div>
)}
<div>{t('metaRuntimeAuth')}: {String(item?.auth || '-')}</div>
{item?.api_state ? (
<div className="mt-2 text-xs text-zinc-500">
<div>{t('metaApiKeyHealth')}: {item?.api_state?.health_score ?? 100} · {t('metaFailures')}: {item?.api_state?.failure_count ?? 0} · {t('metaCooldown')}: {item?.api_state?.cooldown_until || '-'}</div>
</div>
) : null}
<div>{t('metaApiKeyToken')}: {item?.api_state?.token_masked || '-'}</div>
<div>{t('metaLastSuccess')}: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}</div>
{Array.isArray(item.oauth_accounts) && item.oauth_accounts.length > 0 ? (
<div>{t('metaOAuthAccounts')}: {item.oauth_accounts.length} · {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || account?.project_id || '-').join(', ')}</div>
) : null}
<div className="mt-1 flex items-center gap-2">
<FixedButton onClick={onClearApiCooldown} variant="neutral" radius="lg" label="Clear API Cooldown">
<FixedButton onClick={onClearApiCooldown} variant="neutral" radius="lg" label={t('providersClearingAPICooldown')}>
<RotateCcw className="w-4 h-4" />
</FixedButton>
<FixedButton onClick={onClearHistory} variant="neutral" radius="lg" label="Clear History">
<FixedButton onClick={onClearHistory} variant="neutral" radius="lg" label={t('providersClearHistory')}>
<Trash2 className="w-4 h-4" />
</FixedButton>
<FixedButton onClick={onExportHistory} variant="neutral" radius="lg" label="Export History">
<FixedButton onClick={onExportHistory} variant="neutral" radius="lg" label={t('providersExportHistory')}>
<Download className="w-4 h-4" />
</FixedButton>
<FixedButton onClick={onOpenHistory} variant="neutral" radius="lg" label="Open History">
<FixedButton onClick={onOpenHistory} variant="neutral" radius="lg" label={t('providersOpenHistory')}>
<FolderOpen className="w-4 h-4" />
</FixedButton>
</div>
<div className="mt-2">
<div className="mb-1 flex items-center justify-between gap-2">
<div className="text-zinc-500">candidate order</div>
<div className="text-zinc-500">{t('providersCandidateOrder')}</div>
<Button onClick={() => toggleRuntimeSection('candidates')} size="xs" radius="lg" variant="neutral">
{runtimeSectionOpen('candidates') ? 'Collapse' : 'Expand'}
{runtimeSectionOpen('candidates') ? t('collapse') : t('expand')}
</Button>
</div>
{runtimeSectionOpen('candidates') && Array.isArray(item?.candidate_order) && item.candidate_order.length > 0 ? (
@@ -219,9 +220,11 @@ export function ProviderRuntimeSummary({
<div key={`${candidate?.kind || 'candidate'}-${candidate?.target || idx}`} className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-2">
<div className="text-zinc-200">{idx + 1}. {candidate?.kind || '-'}</div>
<div className="truncate text-zinc-400">{candidate?.target || '-'}</div>
<div className="text-zinc-500">status: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}</div>
<div className="text-zinc-500">health: {candidate?.health_score ?? 100} · failures: {candidate?.failure_count ?? 0}</div>
<div className="text-zinc-500">cooldown: {candidate?.cooldown_until || '-'}</div>
<div className="mt-2 ml-4 px-3 py-2 border-l-2 border-zinc-800 text-xs text-zinc-400 space-y-1">
<div className="text-zinc-500">{t('metaStatus')}: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}</div>
<div className="text-zinc-500">{t('metaHealth')}: {candidate?.health_score ?? 100} · {t('metaFailures')}: {candidate?.failure_count ?? 0}</div>
<div className="text-zinc-500">{t('metaCooldown')}: {candidate?.cooldown_until || '-'}</div>
</div>
</div>
))}
</div>
@@ -231,27 +234,27 @@ export function ProviderRuntimeSummary({
</div>
<div className="mt-2">
<div className="mb-1 flex items-center justify-between gap-2">
<div className="text-zinc-500">recent hits</div>
<div className="text-zinc-500">{t('providersRecentHits')}</div>
<Button onClick={() => toggleRuntimeSection('hits')} size="xs" radius="lg" variant="neutral">
{runtimeSectionOpen('hits') ? 'Collapse' : 'Expand'}
{runtimeSectionOpen('hits') ? t('collapse') : t('expand')}
</Button>
</div>
{runtimeSectionOpen('hits') ? renderRuntimeEventList(hits, `${name}-hit`) : <div className="text-zinc-500">-</div>}
</div>
<div className="mt-2">
<div className="mb-1 flex items-center justify-between gap-2">
<div className="text-zinc-500">recent errors</div>
<div className="text-zinc-500">{t('providersRecentErrors')}</div>
<Button onClick={() => toggleRuntimeSection('errors')} size="xs" radius="lg" variant="neutral">
{runtimeSectionOpen('errors') ? 'Collapse' : 'Expand'}
{runtimeSectionOpen('errors') ? t('collapse') : t('expand')}
</Button>
</div>
{runtimeSectionOpen('errors') ? renderRuntimeEventList(errors, `${name}-error`) : <div className="text-zinc-500">-</div>}
</div>
<div className="mt-2">
<div className="mb-1 flex items-center justify-between gap-2">
<div className="text-zinc-500">recent changes</div>
<div className="text-zinc-500">{t('providersRecentChanges')}</div>
<Button onClick={() => toggleRuntimeSection('changes')} size="xs" radius="lg" variant="neutral">
{runtimeSectionOpen('changes') ? 'Collapse' : 'Expand'}
{runtimeSectionOpen('changes') ? t('collapse') : t('expand')}
</Button>
</div>
{runtimeSectionOpen('changes') ? renderRuntimeEventList(changes, `${name}-change`) : <div className="text-zinc-500">-</div>}
@@ -279,40 +282,44 @@ export function ProviderRuntimeDrawer({
onExportHistory,
renderRuntimeEventList,
}: ProviderRuntimeDrawerProps) {
const { t } = useTranslation();
return (
<div className="fixed inset-0 z-[110] flex justify-end">
<button className="absolute inset-0 bg-black/40" onClick={onClose} />
<div className="relative h-full w-full max-w-2xl border-l border-zinc-800 bg-zinc-950 shadow-2xl">
<div className="flex items-center justify-between border-b border-zinc-800 px-4 py-3">
<div>
<div className="text-sm font-semibold text-zinc-100">Provider Runtime History</div>
<div className="text-sm font-semibold text-zinc-100">{t('providersRuntimeDrawerTitle')}</div>
<div className="text-xs text-zinc-500">{name}</div>
</div>
<div className="flex items-center gap-2">
<Button onClick={onExportHistory} size="xs" radius="lg" variant="neutral">Export</Button>
<Button onClick={onClearHistory} size="xs" radius="lg" variant="neutral">Clear</Button>
<Button onClick={onClose} size="xs" radius="lg" variant="neutral">Close</Button>
<Button onClick={onExportHistory} size="xs" radius="lg" variant="neutral">{t('export')}</Button>
<Button onClick={onClearHistory} size="xs" radius="lg" variant="neutral">{t('clear')}</Button>
<Button onClick={onClose} size="xs" radius="lg" variant="neutral">{t('close')}</Button>
</div>
</div>
<div className="h-[calc(100%-57px)] overflow-y-auto p-4 space-y-4 text-xs text-zinc-300">
<div className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-1">
<div>auth: {String(item?.auth || '-')}</div>
<div>last success: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}</div>
{Array.isArray(item?.oauth_accounts) && item.oauth_accounts.length > 0 && (
<div>oauth accounts: {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || '-').join(', ')}</div>
)}
</div>
<div>{t('metaAuth')}: {String(item?.auth || '-')}</div>
<div>{t('metaLastSuccess')}: {item?.last_success ? `${item.last_success.when || '-'} ${item.last_success.kind || '-'} ${item.last_success.target || '-'}` : '-'}</div>
{Array.isArray(item.oauth_accounts) && item.oauth_accounts.length > 0 ? (
<div className="mt-1 text-xs text-zinc-500">
<div>{t('metaOAuthAccounts')}: {item.oauth_accounts.map((account: any) => account?.account_label || account?.email || account?.account_id || '-').join(', ')}</div>
</div>
) : null}</div>
<div className="space-y-2">
<div className="text-zinc-500">oauth accounts</div>
<div className="text-zinc-500">{t('providersOAuthAccounts')}</div>
{Array.isArray(item?.oauth_accounts) && item.oauth_accounts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{item.oauth_accounts.map((account: any, idx: number) => (
<div key={`drawer-account-${account?.credential_file || idx}`} className="rounded-lg border border-zinc-800 bg-zinc-900/30 px-3 py-2">
<div className="text-zinc-100">{account?.account_label || account?.email || account?.account_id || '-'}</div>
<div className="truncate text-zinc-500">{account?.credential_file || '-'}</div>
<div className="text-zinc-500">project: {account?.project_id || '-'}</div>
<div className="text-zinc-500">device: {account?.device_id || '-'}</div>
<div className="truncate text-zinc-500">resource: {account?.resource_url || '-'}</div>
<div className="mt-2 ml-4 px-3 py-2 border-l-2 border-zinc-800 text-xs text-zinc-400 space-y-1">
<div className="text-zinc-500">{t('metaProject')}: {account?.project_id || '-'}</div>
<div className="text-zinc-500">{t('metaDevice')}: {account?.device_id || '-'}</div>
</div>
<div className="truncate text-zinc-500">{t('metaResource')}: {account?.resource_url || '-'}</div>
</div>
))}
</div>
@@ -321,16 +328,18 @@ export function ProviderRuntimeDrawer({
)}
</div>
<div className="space-y-2">
<div className="text-zinc-500">candidate order</div>
<div className="text-zinc-500">{t('providersCandidateOrder')}</div>
{Array.isArray(item?.candidate_order) && item.candidate_order.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{item.candidate_order.map((candidate: any, idx: number) => (
<div key={`drawer-candidate-${idx}`} className="rounded-lg border border-zinc-800 bg-zinc-900/30 px-3 py-2">
<div className="text-zinc-100">{idx + 1}. {candidate?.kind || '-'}</div>
<div className="truncate text-zinc-400">{candidate?.target || '-'}</div>
<div className="text-zinc-500">status: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}</div>
<div className="text-zinc-500">health: {candidate?.health_score ?? 100} · failures: {candidate?.failure_count ?? 0}</div>
<div className="text-zinc-500">cooldown: {candidate?.cooldown_until || '-'}</div>
<div className="mt-2 ml-4 px-3 py-2 border-l-2 border-zinc-800 text-xs text-zinc-400 space-y-1">
<div className="text-zinc-500">{t('metaStatus')}: {candidate?.status || (candidate?.available ? 'ready' : 'skip')}</div>
<div className="text-zinc-500">{t('metaHealth')}: {candidate?.health_score ?? 100} · {t('metaFailures')}: {candidate?.failure_count ?? 0}</div>
<div className="text-zinc-500">{t('metaCooldown')}: {candidate?.cooldown_until || '-'}</div>
</div>
</div>
))}
</div>
@@ -339,15 +348,15 @@ export function ProviderRuntimeDrawer({
)}
</div>
<div className="space-y-2">
<div className="text-zinc-500">recent hits</div>
<div className="text-zinc-500">{t('providersRecentHits')}</div>
{renderRuntimeEventList(filterRuntimeEvents(item?.recent_hits), 'drawer-hit')}
</div>
<div className="space-y-2">
<div className="text-zinc-500">recent errors</div>
<div className="text-zinc-500">{t('providersRecentErrors')}</div>
{renderRuntimeEventList(filterRuntimeEvents(item?.recent_errors), 'drawer-error')}
</div>
<div className="space-y-2">
<div className="text-zinc-500">recent changes</div>
<div className="text-zinc-500">{t('providersRecentChanges')}</div>
{renderRuntimeEventList(filterRuntimeEvents(item?.recent_changes), 'drawer-change')}
</div>
</div>
@@ -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({
</div>
<div className="text-[11px] text-zinc-500">
{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')}
</div>
</div>
<div className="flex items-center gap-2">
{runtimeSummary ? (
<Button onClick={() => setRuntimeOpen((v) => !v)} variant="neutral" size="xs" radius="lg">
{runtimeOpen ? 'Hide Runtime' : 'Show Runtime'}
{runtimeOpen ? t('hide') : t('show')}
</Button>
) : null}
<FixedButton onClick={onRemove} variant="danger" radius="lg" label={t('delete')}>
@@ -493,8 +503,8 @@ export function ProviderProxyCard({
<div className="flex items-center gap-3">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-amber-500/15 text-[11px] font-semibold text-amber-300">1</div>
<div className="min-w-0 flex items-center gap-2">
<div className="text-sm font-medium text-zinc-100">Connection</div>
<div className="truncate text-[11px] text-zinc-500">Base URL, API key, and model routing.</div>
<div className="text-sm font-medium text-zinc-100">{t('providersSectionConnection')}</div>
<div className="truncate text-[11px] text-zinc-500">{t('providersSectionConnectionDesc')}</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
@@ -517,7 +527,7 @@ export function ProviderProxyCard({
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-500/15 text-[11px] font-semibold text-emerald-300">3</div>
<div className="min-w-0 flex items-center gap-2">
<div className="text-sm font-medium text-zinc-100">{t('providersOAuthSetup')}</div>
<div className="truncate text-[11px] text-zinc-500">Select provider, then login or import.</div>
<div className="truncate text-[11px] text-zinc-500">{t('providersSectionOAuthSetupDesc')}</div>
</div>
</div>
<div className="flex items-center gap-2">
@@ -559,13 +569,13 @@ export function ProviderProxyCard({
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<PanelField label={t('providersCooldownSec')} help="Quota / rate-limit cooldown" dense>
<PanelField label={t('providersCooldownSec')} help={t('providersCooldownSecHelp')} dense>
<ProxyTextField value={String(proxy?.oauth?.cooldown_sec || '')} onChange={(e) => onFieldChange('oauth.cooldown_sec', Number(e.target.value || 0))} placeholder={t('providersCooldownSec')} className="w-full" />
</PanelField>
<PanelField label={t('providersRefreshScanSec')} help="Background scan interval" dense>
<PanelField label={t('providersRefreshScanSec')} help={t('providersRefreshScanSecHelp')} dense>
<ProxyTextField value={String(proxy?.oauth?.refresh_scan_sec || '')} onChange={(e) => onFieldChange('oauth.refresh_scan_sec', Number(e.target.value || 0))} placeholder={t('providersRefreshScanSec')} className="w-full" />
</PanelField>
<PanelField label={t('providersRefreshLeadSec')} help="Refresh before expiry" dense>
<PanelField label={t('providersRefreshLeadSec')} help={t('providersRefreshLeadSecHelp')} dense>
<ProxyTextField value={String(proxy?.oauth?.refresh_lead_sec || '')} onChange={(e) => onFieldChange('oauth.refresh_lead_sec', Number(e.target.value || 0))} placeholder={t('providersRefreshLeadSec')} className="w-full" />
</PanelField>
</div>
@@ -613,31 +623,31 @@ export function ProviderProxyCard({
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/25 p-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[11px] uppercase tracking-[0.18em] text-zinc-500"></div>
<div className="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{t('providersOAuthLoginStatus')}</div>
<div className="mt-2 flex items-center gap-2 text-sm font-medium text-zinc-100">
<ShieldCheck className={`h-4 w-4 ${connected ? 'text-emerald-300' : 'text-zinc-500'}`} />
{connected ? `已登录 ${oauthAccountCount} 个账号` : '尚未登录'}
{connected ? ti('providersLoggedInCount', { count: oauthAccountCount }) : t('providersNotLoggedIn')}
</div>
<div className="mt-2 text-[11px] text-zinc-500">
{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')}
</div>
</div>
<div className={`rounded-full border px-2.5 py-1 text-[11px] ${connected ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200' : 'border-zinc-700 bg-zinc-900/50 text-zinc-400'}`}>
{connected ? 'Connected' : 'Disconnected'}
{connected ? t('providersConnected') : t('providersDisconnected')}
</div>
</div>
</div>
<div className="rounded-2xl border border-zinc-800 bg-zinc-950/25 p-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[11px] uppercase tracking-[0.18em] text-zinc-500"></div>
<div className="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{t('providersQuotaStatus')}</div>
<div className="mt-2 flex items-center gap-2 text-sm font-medium text-zinc-100">
<Wallet className="h-4 w-4 text-amber-300" />
{quotaState?.label || '-'}
</div>
<div className="mt-2 text-[11px] text-zinc-500">{quotaState?.detail || '后端暂未提供真实余额接口,这里展示现有的 quota/cooldown/health 信号。'}</div>
<div className="mt-2 text-[11px] text-zinc-500">{quotaState?.detail || t('providersQuotaDefaultHelp')}</div>
</div>
<div className={`rounded-full border px-2.5 py-1 text-[11px] ${quotaState?.tone || 'border-zinc-700 bg-zinc-900/50 text-zinc-300'}`}>
{lastQuotaError ? 'Quota' : connected ? 'Runtime' : 'Pending'}
@@ -653,8 +663,8 @@ export function ProviderProxyCard({
<div className="flex items-center gap-3">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-sky-500/15 text-[11px] font-semibold text-sky-300">2</div>
<div className="min-w-0 flex items-center gap-2">
<div className="text-sm font-medium text-zinc-100">Authentication</div>
<div className="truncate text-[11px] text-zinc-500">Choose how this provider authenticates requests.</div>
<div className="text-sm font-medium text-zinc-100">{t('providersSectionAuth')}</div>
<div className="truncate text-[11px] text-zinc-500">{t('providersSectionAuthDesc')}</div>
</div>
</div>
<PanelField label={t('providersAuthMode')} help={t('providersAuthModeHelp')} dense>
@@ -666,7 +676,7 @@ export function ProviderProxyCard({
</ProxySelectField>
</PanelField>
<div className="rounded-xl border border-dashed border-zinc-800 bg-zinc-950/30 px-3 py-2 text-[11px] text-zinc-500">
{showOAuth ? 'Model selection stays on this provider. Hybrid only switches credentials inside the same provider.' : (
{showOAuth ? t('providersOAuthHybridHint') : (
<>
{t('providersSwitchAuthBefore')}
<span className="font-mono text-zinc-300">oauth</span>
@@ -685,34 +695,32 @@ export function ProviderProxyCard({
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-fuchsia-500/15 text-[11px] font-semibold text-fuchsia-300">4</div>
<div className="min-w-0 flex items-center gap-2">
<div className="text-sm font-medium text-zinc-100">{t('providersOAuthAccounts')}</div>
<div className="truncate text-[11px] text-zinc-500">Imported sessions.</div>
<div className="truncate text-[11px] text-zinc-500">{t('providersSectionOAuthAccountsDesc')}</div>
</div>
</div>
<FixedButton onClick={onLoadOAuthAccounts} variant="neutral" radius="lg" label={t('providersRefreshList')}>
<RefreshCw className={`w-4 h-4${oauthAccountsLoading ? ' animate-spin' : ''}`} />
</FixedButton>
</div>
<div className={`rounded-xl border px-3 py-2 text-[11px] ${
oauthAccountsLoading
? 'border-sky-500/25 bg-sky-500/10 text-sky-100'
: connected
? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-100'
: 'border-zinc-800 bg-zinc-950/30 text-zinc-400'
}`}>
<div className={`rounded-xl border px-3 py-2 text-[11px] ${oauthAccountsLoading
? 'border-sky-500/25 bg-sky-500/10 text-sky-100'
: connected
? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-100'
: 'border-zinc-800 bg-zinc-950/30 text-zinc-400'
}`}>
{oauthAccountsLoading
? t('providersOAuthLoadingHelp')
: connected
? `${oauthStatusText}${oauthStatusDetail}`
: t('providersOAuthEmptyHelp')}
</div>
<div className={`hidden rounded-xl border px-3 py-2 text-[11px] ${
connected
? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-100'
: 'border-zinc-800 bg-zinc-950/30 text-zinc-400'
}`}>
<div className={`hidden rounded-xl border px-3 py-2 text-[11px] ${connected
? 'border-emerald-500/25 bg-emerald-500/10 text-emerald-100'
: 'border-zinc-800 bg-zinc-950/30 text-zinc-400'
}`}>
{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')}
</div>
{oauthAccountsLoading ? (
<div className="text-zinc-500">{t('providersOAuthLoading')}</div>
@@ -725,17 +733,16 @@ export function ProviderProxyCard({
<div className="min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="text-zinc-200 truncate">{account?.email || account?.account_id || account?.credential_file}</div>
<div className={`shrink-0 rounded-full border px-2 py-0.5 text-[10px] ${
String(account?.cooldown_until || '').trim()
? 'border-orange-500/30 bg-orange-500/10 text-orange-200'
: Number(account?.health_score || 100) < 60
? 'border-rose-500/30 bg-rose-500/10 text-rose-200'
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200'
}`}>
{String(account?.cooldown_until || '').trim() ? '冷却中' : Number(account?.health_score || 100) < 60 ? '受限' : '在线'}
<div className={`shrink-0 rounded-full border px-2 py-0.5 text-[10px] ${String(account?.cooldown_until || '').trim()
? 'border-orange-500/30 bg-orange-500/10 text-orange-200'
: Number(account?.health_score || 100) < 60
? 'border-rose-500/30 bg-rose-500/10 text-rose-200'
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-200'
}`}>
{String(account?.cooldown_until || '').trim() ? t('providersAccountCooldown') : Number(account?.health_score || 100) < 60 ? t('providersAccountLimited') : t('providersAccountOnline')}
</div>
</div>
<div className="text-zinc-500 text-[11px]">label: {account?.account_label || account?.email || account?.account_id || '-'}</div>
<div className="text-zinc-500 text-[11px]">{t('metaLabel')}: {account?.account_label || account?.email || account?.account_id || '-'}</div>
<div className="text-zinc-500 truncate text-[11px]">{account?.credential_file}</div>
{(account?.balance_label || account?.plan_type) ? (
<div className="text-zinc-500 text-[11px]">
@@ -748,9 +755,13 @@ export function ProviderProxyCard({
{t('providersSubscriptionUntil')}: {account?.subscription_active_until}
</div>
) : null}
<div className="text-zinc-500 text-[11px]">project: {account?.project_id || '-'} · device: {account?.device_id || '-'}</div>
<div className="text-zinc-500 truncate text-[11px]">proxy: {account?.network_proxy || '-'}</div>
<div className="text-zinc-500 text-[11px]">expire: {account?.expire || '-'} · cooldown: {account?.cooldown_until || '-'}</div>
<div className="flex items-start justify-between gap-4 mt-1.5">
<div className="text-zinc-500 text-[11px]">{t('metaProject')}: {account?.project_id || '-'} · {t('metaDevice')}: {account?.device_id || '-'}</div>
</div>
<div className="text-zinc-500 truncate text-[11px]">{t('metaProxy')}: {account?.network_proxy || '-'}</div>
<div className="flex items-start justify-between gap-4 mt-1.5">
<div className="text-zinc-500 text-[11px]">{t('metaExpire')}: {account?.expire || '-'} · {t('metaCooldown')}: {account?.cooldown_until || '-'}</div>
</div>
<div className="text-zinc-500 text-[11px]">
{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')}
</div>
<div className="text-zinc-500 text-[11px]">health: {Number(account?.health_score || 100)} · failures: {Number(account?.failure_count || 0)} · last failure: {account?.last_failure || '-'}</div>
<div className="flex items-start justify-between gap-4 mt-1.5">
<div className="text-zinc-500 text-[11px]">{t('metaHealth')}: {Number(account?.health_score || 100)} · {t('metaFailures')}: {Number(account?.failure_count || 0)} · {t('metaLastFailure')}: {account?.last_failure || '-'}</div>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
<FixedButton onClick={() => onRefreshOAuthAccount(String(account?.credential_file || ''))} variant="neutral" radius="lg" label="Refresh">
<FixedButton onClick={() => onRefreshOAuthAccount(String(account?.credential_file || ''))} variant="neutral" radius="lg" label={t('refresh')}>
<RefreshCw className="w-4 h-4" />
</FixedButton>
<FixedButton onClick={() => onClearOAuthCooldown(String(account?.credential_file || ''))} variant="neutral" radius="lg" label="Clear Cooldown">
<FixedButton onClick={() => onClearOAuthCooldown(String(account?.credential_file || ''))} variant="neutral" radius="lg" label={t('providersClearCooldown')}>
<RotateCcw className="w-4 h-4" />
</FixedButton>
<FixedButton onClick={() => onDeleteOAuthAccount(String(account?.credential_file || ''))} variant="danger" radius="lg" label="Logout">
<FixedButton onClick={() => onDeleteOAuthAccount(String(account?.credential_file || ''))} variant="danger" radius="lg" label={t('providersLogout')}>
<LogOut className="w-4 h-4" />
</FixedButton>
</div>
@@ -785,15 +798,15 @@ export function ProviderProxyCard({
<div className="flex items-center gap-3">
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-[11px] font-semibold text-zinc-200">5</div>
<div className="min-w-0 flex items-center gap-2">
<div className="text-sm font-medium text-zinc-100">Advanced</div>
<div className="truncate text-[11px] text-zinc-500">Low-frequency runtime settings.</div>
<div className="text-sm font-medium text-zinc-100">{t('providersSectionAdvanced')}</div>
<div className="truncate text-[11px] text-zinc-500">{t('providersSectionAdvancedDesc')}</div>
</div>
</div>
<Button onClick={() => setAdvancedOpen((v) => !v)} size="xs" radius="lg" variant="neutral">
{advancedOpen ? 'Hide' : 'Show'}
{advancedOpen ? t('hide') : t('show')}
</Button>
</div>
<InlineCheckboxField
<InlineSwitchField
checked={Boolean(proxy?.runtime_persist)}
help={t('providersRuntimePersistHelp')}
label={t('providersRuntimePersist')}
@@ -821,10 +834,10 @@ export function ProviderProxyCard({
onClick={() => setRuntimeOpen((v) => !v)}
>
<div>
<div className="text-sm font-medium text-zinc-200">Runtime</div>
<div className="text-[11px] text-zinc-500">Health, candidate order, recent hits and errors.</div>
<div className="text-sm font-medium text-zinc-200">{t('providersRuntimeTitle')}</div>
<div className="text-[11px] text-zinc-500">{t('providersRuntimeDesc')}</div>
</div>
<div className="text-[11px] text-zinc-400">{runtimeOpen ? 'Collapse' : 'Expand'}</div>
<div className="text-[11px] text-zinc-400">{runtimeOpen ? t('collapse') : t('expand')}</div>
</button>
{runtimeOpen ? <div className="border-t border-zinc-800 p-3">{runtimeSummary}</div> : null}
</div>

View File

@@ -31,7 +31,7 @@ const EmptyState: React.FC<EmptyStateProps> = ({
'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,

View File

@@ -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<MetricPanelProps> = ({
className,

View File

@@ -12,7 +12,7 @@ const StatCard: React.FC<StatCardProps> = ({ 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"
>
<div className="card-icon-shell w-12 h-12 rounded-2xl bg-zinc-800/50 flex items-center justify-center border border-zinc-700/50 relative z-[1]">
{icon}

View File

@@ -15,7 +15,7 @@ const EKGDistributionCard: React.FC<EKGDistributionCardProps> = ({
const maxValue = entries.length > 0 ? Math.max(...entries.map(([, value]) => value)) : 0;
return (
<div className="brand-card ui-border-subtle rounded-[28px] border p-5">
<div className="brand-card ui-border-subtle rounded-2xl border p-5">
<div className="ui-text-secondary mb-4 text-sm font-medium">{title}</div>
<div className="space-y-3">
{entries.length === 0 ? (

View File

@@ -14,7 +14,7 @@ const EKGRankingCard: React.FC<EKGRankingCardProps> = ({
valueMode,
}) => {
return (
<div className="brand-card ui-border-subtle rounded-[28px] border p-5">
<div className="brand-card ui-border-subtle rounded-2xl border p-5">
<div className="ui-text-secondary mb-4 text-sm font-medium">{title}</div>
<div className="space-y-2">
{items.length === 0 ? (

View File

@@ -16,7 +16,7 @@ const ListPanel: React.FC<ListPanelProps> = ({
header,
}) => {
return (
<div className={joinClasses('brand-card ui-panel rounded-[28px] overflow-hidden flex flex-col min-h-0', className)}>
<div className={joinClasses('brand-card ui-panel rounded-2xl overflow-hidden flex flex-col min-h-0', className)}>
{header}
{children}
</div>

View File

@@ -39,14 +39,14 @@ const SectionPanel: React.FC<SectionPanelProps> = ({
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)}
>
<div className="relative z-10">
{title || subtitle || actions || icon ? (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className={joinClasses('mb-5 flex items-center justify-between gap-3 flex-wrap', headerClassName)}
className={joinClasses('mb-4 flex items-center justify-between gap-3 flex-wrap', headerClassName)}
>
<div>
{title ? (

View File

@@ -62,21 +62,21 @@ const MCPServerCard: React.FC<MCPServerCardProps> = ({
<div className="grid grid-cols-2 gap-3 text-xs">
<InfoTile
label="package"
label={t('mcpPackage')}
className="ui-code-panel px-3 py-2"
contentClassName="mt-1 break-all text-zinc-200 text-sm"
>
{String(server?.package || '-')}
</InfoTile>
<InfoTile
label="args"
label={t('mcpArgs')}
className="ui-code-panel px-3 py-2"
contentClassName="mt-1 text-zinc-200 text-sm"
>
{Array.isArray(server?.args) ? server.args.length : 0}
</InfoTile>
<InfoTile
label="permission"
label={t('mcpPermission')}
className="ui-code-panel px-3 py-2"
contentClassName="mt-1 text-zinc-200 text-sm"
>

View File

@@ -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<MCPServerEditorProps> = ({
</label>
<div className="space-y-2">
<div className="text-xs text-zinc-400">enabled</div>
<CheckboxCardField
<SwitchCardField
checked={draft.enabled}
className="min-h-[76px]"
label={t('enable')}
@@ -123,14 +123,15 @@ const MCPServerEditor: React.FC<MCPServerEditorProps> = ({
{draft.transport === 'stdio' ? (
<div className="ui-soft-panel rounded-[24px] p-4 space-y-3">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div>
<div className="text-sm font-medium text-zinc-200">Args</div>
<div className="text-xs text-zinc-500">{t('configMCPArgsEnterHint')}</div>
<div className="text-sm font-medium text-zinc-200">{t('mcpArgs')}</div>
<div className="text-xs text-zinc-500">{t('configMCPArgsHint', { defaultValue: 'Press Enter to add arguments' })}</div>
</div>
<FixedButton onClick={installDraftPackage} variant="success" label={t('install')}>
<Button onClick={installDraftPackage} variant="success" size="sm" radius="lg" gap="1">
<Package className="w-4 h-4" />
</FixedButton>
{t('install')}
</Button>
</div>
<div className="flex max-h-28 flex-wrap gap-2 overflow-y-auto pr-1">

View File

@@ -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<ProfileEditorPanelProps> = ({
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 (
<div className="brand-card ui-border-subtle rounded-[28px] border p-4 space-y-3">
<div className="brand-card ui-border-subtle rounded-2xl border p-4 flex flex-col min-h-0 h-full">
<div className="space-y-3 flex-1 min-h-0 overflow-y-auto overflow-x-hidden pr-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<FieldBlock label={idLabel}>
<TextField
@@ -85,7 +86,7 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
onChange={(e) => onChange({ ...draft, agent_id: e.target.value })}
dense
className="w-full disabled:opacity-60"
placeholder="coder"
placeholder={t('profileIdPlaceholder')}
/>
</FieldBlock>
<FieldBlock label={nameLabel}>
@@ -94,7 +95,7 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
onChange={(e) => onChange({ ...draft, name: e.target.value })}
dense
className="w-full"
placeholder="Code Agent"
placeholder={t('profileNamePlaceholder')}
/>
</FieldBlock>
<FieldBlock label={roleLabel}>
@@ -103,22 +104,21 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
onChange={(e) => onChange({ ...draft, role: e.target.value })}
dense
className="w-full"
placeholder="coding"
placeholder={t('profileRolePlaceholder')}
/>
</FieldBlock>
<FieldBlock
label={statusLabel}
>
<CheckboxCardField
<SwitchCardField
checked={statusEnabled}
className="min-h-[76px]"
help={statusEnabled ? '已启用,允许接收任务。' : '已停用,不会接收新任务。'}
label={statusEnabled ? '启用' : '停用'}
help={statusEnabled ? t('profileStatusEnabledHelp') : t('profileStatusDisabledHelp')}
label={statusEnabled ? t('profileStatusEnabledLabel') : t('profileStatusDisabledLabel')}
onChange={(checked) => onChange({ ...draft, status: checked ? 'active' : 'disabled' })}
/>
</FieldBlock>
<FieldBlock
label="通知主代理"
label={t('profileNotifyMain')}
help={notifyPolicy.help}
>
<SelectField
@@ -138,7 +138,7 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
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')}
/>
<div className={`mt-1 text-[11px] ${promptPathInvalid ? 'text-rose-300' : 'ui-text-muted'}`}>{promptPathHint}</div>
</FieldBlock>
@@ -148,7 +148,7 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
onChange={(e) => onChange({ ...draft, memory_namespace: e.target.value })}
dense
className="w-full"
placeholder="coder"
placeholder={t('profileNamespacePlaceholder')}
/>
</FieldBlock>
<FieldBlock className="md:col-span-2" label={toolAllowlistLabel}>
@@ -157,7 +157,7 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
onChange={(e) => onChange({ ...draft, tool_allowlist: parseAllowlist(e.target.value) })}
dense
className="w-full"
placeholder="read_file, list_files, memory_search"
placeholder={t('profileAllowlistPlaceholder')}
/>
<div className="ui-text-muted mt-1 text-[11px]">{toolAllowlistHint}</div>
{groups.length > 0 ? (
@@ -170,7 +170,7 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
</div>
) : null}
</FieldBlock>
<FieldBlock className="md:col-span-2" label="system_prompt_file content" meta={promptMeta}>
<FieldBlock className="md:col-span-2" label={t('profilePromptFileContentLabel')} meta={promptMeta}>
<TextareaField
value={promptContent}
onChange={(e) => onPromptContentChange(e.target.value)}
@@ -226,7 +226,8 @@ const ProfileEditorPanel: React.FC<ProfileEditorPanelProps> = ({
/>
</FieldBlock>
</div>
<div className="flex items-center gap-2">
</div>
<div className="flex items-center gap-2 pt-4 mt-auto">
<Button onClick={onSave} disabled={saving} variant="primary" size="sm" radius="lg" gap="1">
<Save className="w-4 h-4" />
{isExisting ? t('update') : t('create')}

View File

@@ -0,0 +1,35 @@
import React, { useMemo, useState } from 'react';
import { Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { FixedButton } from './Button';
import { SwitchCardField, SelectField, TextField, TextareaField } from './FormControls';
interface RecursiveConfigProps {
data: any;
labels: Record<string, string>;
path?: string;
onChange: (path: string, val: any) => void;
hotPaths?: string[];
onlyHot?: boolean;
}
const isPrimitive = (v: any) => ['string', 'number', 'boolean'].includes(typeof v) || v === null;
const isPathHot = (currentPath: string, hotPaths: string[]) => {
if (!hotPaths.length) return true;
return hotPaths.some((hp) => {
const p = String(hp || '').replace(/\.\*$/, '');
if (!p) return false;
return currentPath === p || currentPath.startsWith(`${p}.`) || p.startsWith(`${currentPath}.`);
});
};
const PrimitiveArrayEditor: React.FC<{
value: any[];
path: string;
onChange: (next: any[]) => void;
}> = ({ value, path, onChange }) => {
const { t } = useTranslation();
const [draft, setDraft] = useState('');
const [selected, setSelected] = useState('');
const suggestions = useMemo(() => {

View File

@@ -21,7 +21,7 @@ type TextareaFieldProps = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>
className?: string;
};
type CheckboxFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'className'> & {
type SwitchFieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'className'> & {
className?: string;
};
@@ -37,7 +37,7 @@ type PanelFieldProps = FieldBlockProps & {
dense?: boolean;
};
type CheckboxCardFieldProps = {
type SwitchCardFieldProps = {
checked: boolean;
className?: string;
help?: React.ReactNode;
@@ -45,7 +45,7 @@ type CheckboxCardFieldProps = {
onChange: (checked: boolean) => void;
};
type ToolbarCheckboxFieldProps = {
type ToolbarSwitchFieldProps = {
checked: boolean;
className?: string;
help?: React.ReactNode;
@@ -53,7 +53,7 @@ type ToolbarCheckboxFieldProps = {
onChange: (checked: boolean) => void;
};
type InlineCheckboxFieldProps = {
type InlineSwitchFieldProps = {
checked: boolean;
className?: string;
help?: React.ReactNode;
@@ -104,62 +104,67 @@ export function TextareaField({ dense = false, monospace = false, className, ...
);
}
export function CheckboxField({ className, ...props }: CheckboxFieldProps) {
return <input {...props} type="checkbox" className={joinClasses('ui-checkbox transition-all duration-200 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 outline-none hover:border-zinc-500 w-4 h-4 shrink-0', className)} />;
}
export function CheckboxCardField({
checked,
className,
help,
label,
onChange,
}: CheckboxCardFieldProps) {
export function SwitchField({ className, ...props }: SwitchFieldProps) {
return (
<label className={joinClasses('ui-toggle-card ui-checkbox-field p-4 rounded-2xl cursor-pointer transition-all duration-200 hover:bg-zinc-800/50 border hover:border-zinc-700 border-zinc-800/60 flex flex-row items-start gap-4', className)}>
<div className="space-y-1.5 flex-1">
<div className="ui-text-primary text-sm font-semibold">{label}</div>
{help ? <div className="ui-form-help text-xs text-zinc-400 whitespace-pre-wrap">{help}</div> : null}
</div>
<div className="pt-0.5 flex shrink-0">
<CheckboxField checked={checked} onChange={(e) => onChange(e.target.checked)} />
</div>
<label className={joinClasses('ui-switch shrink-0', className)}>
<input {...props} type="checkbox" className="ui-switch-input" />
<span className="ui-switch-track">
<span className="ui-switch-knob" />
</span>
</label>
);
}
export function ToolbarCheckboxField({
export function SwitchCardField({
checked,
className,
help,
label,
onChange,
}: ToolbarCheckboxFieldProps) {
}: SwitchCardFieldProps) {
return (
<label className={joinClasses('flex items-center justify-between gap-3 py-2 cursor-pointer', className)}>
<div className="min-w-0 space-y-0.5">
<div className="ui-form-label text-sm font-semibold">{label}</div>
{help ? <div className="ui-form-help text-xs leading-snug whitespace-pre-wrap">{help}</div> : null}
</div>
<SwitchField checked={checked} onChange={(e) => onChange(e.target.checked)} />
</label>
);
}
export function ToolbarSwitchField({
checked,
className,
help,
label,
onChange,
}: ToolbarSwitchFieldProps) {
return (
<label className={joinClasses('ui-toolbar-checkbox cursor-pointer transition-colors duration-200 hover:bg-zinc-800/50', className)}>
<div className="min-w-0 space-y-0.5">
<div className="ui-text-primary text-xs font-semibold leading-tight">{label}</div>
{help ? <div className="ui-form-help text-[10px] leading-tight">{help}</div> : null}
</div>
<CheckboxField checked={checked} onChange={(e) => onChange(e.target.checked)} />
<SwitchField checked={checked} onChange={(e) => onChange(e.target.checked)} />
</label>
);
}
export function InlineCheckboxField({
export function InlineSwitchField({
checked,
className,
help,
label,
onChange,
}: InlineCheckboxFieldProps) {
}: InlineSwitchFieldProps) {
return (
<label className={joinClasses('flex items-center justify-between gap-3 rounded-xl border border-zinc-800/60 bg-zinc-900/40 px-3 py-3 cursor-pointer transition-all duration-200 hover:bg-zinc-800/60 hover:border-zinc-700 shadow-sm', className)}>
<div className="min-w-0 space-y-1">
<div className="ui-text-primary text-sm font-semibold leading-tight">{label}</div>
<label className={joinClasses('flex items-center justify-between gap-3 rounded-xl border border-zinc-800/60 bg-zinc-900/40 px-3 py-2.5 cursor-pointer transition-all duration-200 hover:bg-zinc-800/60 hover:border-zinc-700 shadow-sm', className)}>
<div className="min-w-0 space-y-0.5">
<div className="ui-text-primary text-sm font-semibold leading-snug">{label}</div>
{help ? <div className="ui-form-help text-xs text-zinc-400 leading-snug whitespace-pre-wrap">{help}</div> : null}
</div>
<CheckboxField checked={checked} onChange={(e) => onChange(e.target.checked)} />
<SwitchField checked={checked} onChange={(e) => onChange(e.target.checked)} />
</label>
);
}
@@ -170,7 +175,7 @@ export function FieldBlock({ label, help, meta, className, children }: FieldBloc
{(label || help || meta) && (
<div className="flex flex-col gap-1.5 mb-1.5">
<div className="flex items-center justify-between gap-3">
{label ? <div className="ui-form-label text-sm font-semibold text-zinc-200">{label}</div> : null}
{label ? <div className="ui-form-label text-sm font-semibold">{label}</div> : null}
{meta ? <div className="ui-form-help shrink-0 text-xs font-medium text-zinc-500">{meta}</div> : null}
</div>
{help ? <div className="ui-form-help text-xs text-zinc-400 leading-relaxed whitespace-pre-wrap">{help}</div> : null}

View File

@@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
import { Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { FixedButton } from './Button';
import { CheckboxCardField, SelectField, TextField, TextareaField } from './FormControls';
import { SwitchCardField, SelectField, TextField, TextareaField } from './FormControls';
interface RecursiveConfigProps {
data: any;
@@ -176,7 +176,7 @@ const RecursiveConfig: React.FC<RecursiveConfigProps> = ({ data, labels, path =
return (
<div key={currentPath} className="space-y-2">
{typeof value === 'boolean' ? (
<CheckboxCardField
<SwitchCardField
checked={value}
help={<span className="font-mono text-[10px]">{currentPath}</span>}
label={label}

View File

@@ -570,6 +570,8 @@ const resources = {
configDeleteProviderConfirmTitle: 'Delete Provider',
configDeleteProviderConfirmMessage: 'Delete provider "{{name}}" from current config?',
cronExpressionPlaceholder: '*/5 * * * *',
cronDeliverHint: 'Send the message through the selected channel.',
cronEnabledHint: 'Enable this cron job immediately after saving.',
recipientId: 'recipient id',
languageZh: '中文',
languageEn: 'English',
@@ -577,6 +579,96 @@ const resources = {
themeDark: 'Night',
expand: 'Expand',
collapse: 'Collapse',
show: 'Show',
hide: 'Hide',
profileNotifyMain: 'Notify Main Agent',
profileNotifyFinalOnlyLabel: 'Final Only',
profileNotifyFinalOnlyHelp: 'Notify main agent only after the task completes.',
profileNotifyInternalOnlyLabel: 'Internal Only',
profileNotifyInternalOnlyHelp: 'Relay intermediate progress only, without emphasizing the final result.',
profileNotifyMilestoneLabel: 'Milestone',
profileNotifyMilestoneHelp: 'Notify main agent when a key milestone is reached.',
profileNotifyOnBlockedLabel: 'On Blocked',
profileNotifyOnBlockedHelp: 'Notify main agent only when blocked and needs intervention.',
profileNotifyAlwaysLabel: 'Always',
profileNotifyAlwaysHelp: 'Notify main agent on both progress and results.',
profileStatusEnabledLabel: 'Enabled',
profileStatusDisabledLabel: 'Disabled',
profileStatusEnabledHelp: 'Enabled, accepting tasks.',
profileStatusDisabledHelp: 'Disabled, will not accept new tasks.',
profileEmptyLabel: 'No subagent profiles.',
profileRoleLabel: 'Role',
profileMaxTaskCharsLabel: 'Max Task Chars',
profileMaxResultCharsLabel: 'Max Result Chars',
profilePromptFileContentLabel: 'system_prompt_file content',
profilePromptFilePlaceholder: 'agents/coder/AGENT.md',
profileIdPlaceholder: 'coder',
profileNamePlaceholder: 'Code Agent',
profileRolePlaceholder: 'coding',
profileNamespacePlaceholder: 'coder',
profileAllowlistPlaceholder: 'read_file, list_files, memory_search',
profileToolAllowlistInheritHint: 'is inherited automatically and does not need to be listed here.',
mcpArgs: 'Args',
mcpPackage: 'Package',
mcpPermission: 'Permission',
providersSectionConnection: 'Connection',
providersSectionConnectionDesc: 'Base URL, API key, and model routing.',
providersSectionAuth: 'Authentication',
providersSectionAuthDesc: 'Choose how this provider authenticates requests.',
providersSectionOAuthSetupDesc: 'Select provider, then login or import.',
providersSectionOAuthAccountsDesc: 'Imported sessions.',
providersSectionAdvanced: 'Advanced',
metaApiKeyHealth: 'api key health',
metaFailures: 'failures',
metaCooldown: 'cooldown',
metaOAuthAccounts: 'oauth accounts',
metaStatus: 'status',
metaProject: 'project',
metaDevice: 'device',
metaExpire: 'expire',
metaHealth: 'health',
metaLastFailure: 'last failure',
metaRuntimeAuth: 'runtime auth',
metaAuth: 'auth',
metaApiKeyToken: 'api key token',
metaLastSuccess: 'last success',
metaResource: 'resource',
metaLabel: 'label',
metaProxy: 'proxy',
providersSectionAdvancedDesc: 'Low-frequency runtime settings.',
providersOAuthHybridHint: 'Model selection stays on this provider. Hybrid only switches credentials inside the same provider.',
providersOAuthConfigHint: 'Configure connection first, then choose the OAuth provider and start login.',
providersAuthPickHint: 'Pick an auth mode first. OAuth and Hybrid will open the login workflow.',
providersRuntimeTitle: 'Runtime',
providersRuntimeDesc: 'Health, candidate order, recent hits and errors.',
providersRuntimeToolbarDesc: 'Runtime filters and provider creation are split so the status controls stay attached to each other.',
providersRuntimeDrawerTitle: 'Provider Runtime History',
providersAccountCooldown: 'Cooldown',
providersAccountLimited: 'Limited',
providersAccountOnline: 'Online',
providersPrimaryLoaded: 'Primary loaded',
providersLoginOAuthOrImportHint: 'Click OAuth Login or import an auth.json, then accounts will appear here.',
providersLoggedInCount: 'Logged in {{count}} account(s)',
providersNotLoggedIn: 'Not logged in',
providersAutoLoadedCount: 'Auto-loaded {{count}} OAuth account(s). Primary: {{primary}}',
providersNoAccountsAvailableHint: 'No available accounts. Click OAuth Login on the left, or import an auth.json.',
providersQuotaDefaultHelp: 'Backend has no real balance API yet. This shows quota, cooldown, and health signals.',
providersQuotaLimitedDetail: 'Last quota hit: {{when}}',
providersQuotaHealthLowDetail: 'Lowest health score {{score}}',
providersQuotaHealthyDetail: 'Current account can join OAuth rotation.',
providersQuotaNoAccountDetail: 'No OAuth accounts available yet.',
providersClearCooldown: 'Clear Cooldown',
providersExportHistory: 'Export History',
providersOpenHistory: 'Open History',
providersClearHistory: 'Clear History',
providersCooldownSecHelp: 'Quota / rate-limit cooldown',
providersRefreshScanSecHelp: 'Background scan interval',
providersRefreshLeadSecHelp: 'Refresh before expiry',
providersLogout: 'Logout',
providersCandidateOrder: 'candidate order',
providersRecentHits: 'recent hits',
providersRecentErrors: 'recent errors',
providersRecentChanges: 'recent changes',
configRoot: '(root)',
configCommaSeparatedHint: ', a, b',
configLabels: {
@@ -890,6 +982,35 @@ const resources = {
remote: '远端',
},
clearFocus: '清除聚焦',
zoomIn: '放大',
zoomOut: '缩小',
fitView: '适配视图',
noLiveTasks: '当前没有运行中的任务',
remoteTasksUnavailable: '远端任务详情尚未被镜像。',
savePromptFile: '保存 AGENT.md',
promptFileSaved: 'Prompt 文件已保存。',
promptFileReady: 'AGENT.md 就绪',
promptFileMissing: 'AGENT.md 缺失',
promptFileRelativePathHint: '请使用工作目录相对路径,例如 agents/coder/AGENT.md。',
promptFileRelativePathOnly: 'system_prompt_file 必须是工作目录相对路径,不能是绝对路径。',
promptFileWorkspaceOnly: 'system_prompt_file 必须在工作目录范围内。',
newProfile: '新建档案',
spawn: '派生',
dispatch: '调度',
toolAllowlist: '工具白名单',
memoryNamespace: '记忆命名空间',
subagentDeleteConfirmTitle: '删除子代理档案',
subagentDeleteConfirmMessage: '确认永久删除子代理档案 "{{id}}" 吗?',
sidebarWorkspace: '工作区',
sidebarAgentsKnowledge: '代理与知识',
sidebarInfraNetwork: '基础设施与网络',
sidebarSystemSettings: '系统设置',
sidebarMonitoringAudit: '监控与审计',
ekg: 'EKG',
ekgEscalations: '升级',
ekgSourceStats: '来源统计',
ekgChannelStats: '通道统计',
ekgTopProvidersWorkload: 'Top Providers业务负载',
ekgTopProvidersAll: 'Top Providers全量',
ekgTopErrsigWorkload: 'Top 错误签名(业务负载)',
ekgTopErrsigAll: 'Top 错误签名(全量)',
@@ -1297,6 +1418,8 @@ const resources = {
configDeleteProviderConfirmTitle: '删除 Provider',
configDeleteProviderConfirmMessage: '确认从当前配置中删除 provider “{{name}}”吗?',
cronExpressionPlaceholder: '*/5 * * * *',
cronDeliverHint: '通过指定的通道投递消息。',
cronEnabledHint: '保存后立即启用此定时任务。',
recipientId: '接收者 ID',
languageZh: '中文',
languageEn: 'English',
@@ -1304,6 +1427,96 @@ const resources = {
themeDark: '黑夜',
expand: '展开',
collapse: '收起',
show: '显示',
hide: '隐藏',
profileNotifyMain: '通知主代理',
profileNotifyFinalOnlyLabel: '仅最终结果',
profileNotifyFinalOnlyHelp: '只在任务完成后通知主代理。',
profileNotifyInternalOnlyLabel: '仅内部事件',
profileNotifyInternalOnlyHelp: '只回传中间过程,不单独强调最终结果。',
profileNotifyMilestoneLabel: '关键节点',
profileNotifyMilestoneHelp: '到达关键阶段时通知主代理。',
profileNotifyOnBlockedLabel: '遇阻才通知',
profileNotifyOnBlockedHelp: '只有卡住、需要介入时才通知主代理。',
profileNotifyAlwaysLabel: '始终通知',
profileNotifyAlwaysHelp: '过程和结果都会尽量通知主代理。',
profileStatusEnabledLabel: '启用',
profileStatusDisabledLabel: '停用',
profileStatusEnabledHelp: '已启用,允许接收任务。',
profileStatusDisabledHelp: '已停用,不会接收新任务。',
profileEmptyLabel: '暂无子代理档案。',
profileRoleLabel: '默认角色',
profileMaxTaskCharsLabel: '最大任务字符数',
profileMaxResultCharsLabel: '最大回复字符数',
profilePromptFileContentLabel: 'system_prompt_file 内容',
profilePromptFilePlaceholder: 'agents/coder/AGENT.md',
profileIdPlaceholder: 'coder',
profileNamePlaceholder: 'Code Agent',
profileRolePlaceholder: 'coding',
profileNamespacePlaceholder: 'coder',
profileAllowlistPlaceholder: 'read_file, list_files, memory_search',
profileToolAllowlistInheritHint: '技能自动继承,无需在此列出。',
mcpArgs: '运行参数配置 (Args)',
mcpPackage: 'NPM 包及参数',
mcpPermission: '授权配置',
providersSectionConnection: '连接配置',
providersSectionConnectionDesc: 'API 基础地址、密钥和模型路由。',
providersSectionAuth: '认证方式',
providersSectionAuthDesc: '选择该 provider 的请求认证方式。',
providersSectionOAuthSetupDesc: '选择服务商,然后登录或导入。',
providersSectionOAuthAccountsDesc: '已导入的登录会话。',
providersSectionAdvanced: '高级设置',
metaApiKeyHealth: 'API 健康度',
metaFailures: '失败次数',
metaCooldown: '冷却至',
metaOAuthAccounts: 'OAuth 账号',
metaStatus: '状态',
metaProject: '项目 ID',
metaDevice: '设备 ID',
metaExpire: '过期时间',
metaHealth: '健康度',
metaLastFailure: '最近失败',
metaRuntimeAuth: '运行时鉴权',
metaAuth: '授权模式',
metaApiKeyToken: '凭证 (Token)',
metaLastSuccess: '最近成功',
metaResource: '关联资源',
metaLabel: '账号标签',
metaProxy: '网络代理',
providersSectionAdvancedDesc: '低频运行时配置项。',
providersOAuthHybridHint: '模型选择仍在当前 provider 内。Hybrid 模式仅在同一 provider 内切换凭证。',
providersOAuthConfigHint: '先配置连接信息,然后选择 OAuth 服务商并发起登录。',
providersAuthPickHint: '先选一个认证模式。OAuth 和 Hybrid 会打开登录流程。',
providersRuntimeTitle: '运行态',
providersRuntimeDesc: '健康状态、候选排序、最近命中和错误。',
providersRuntimeToolbarDesc: '运行态筛选和 provider 创建分开展示,以保持状态控件紧凑。',
providersRuntimeDrawerTitle: 'Provider 运行态历史',
providersAccountCooldown: '冷却中',
providersAccountLimited: '受限',
providersAccountOnline: '在线',
providersPrimaryLoaded: '主账号已加载',
providersLoginOAuthOrImportHint: '点击 OAuth 登录或导入授权文件后,这里会自动显示账号。',
providersLoggedInCount: '已登录 {{count}} 个账号',
providersNotLoggedIn: '尚未登录',
providersAutoLoadedCount: '已自动加载 {{count}} 个 OAuth 账号。当前主账号:{{primary}}',
providersNoAccountsAvailableHint: '当前没有可用账号。可以直接点击左侧 OAuth 登录,或者导入 auth.json。',
providersQuotaDefaultHelp: '后端暂未提供真实余额接口,这里展示现有的 quota/cooldown/health 信号。',
providersQuotaLimitedDetail: '最近一次限额命中:{{when}}',
providersQuotaHealthLowDetail: '最低健康分 {{score}}',
providersQuotaHealthyDetail: '当前账号可参与 OAuth 轮换。',
providersQuotaNoAccountDetail: '还没有可用的 OAuth 账号。',
providersClearCooldown: '清除冷却',
providersExportHistory: '导出历史',
providersOpenHistory: '打开历史',
providersClearHistory: '清除历史',
providersCooldownSecHelp: 'Quota / 限流冷却',
providersRefreshScanSecHelp: '后台扫描间隔',
providersRefreshLeadSecHelp: '到期前提前刷新',
providersLogout: '退出登录',
providersCandidateOrder: '候选排序',
providersRecentHits: '最近命中',
providersRecentErrors: '最近错误',
providersRecentChanges: '最近变更',
configRoot: '(根)',
configCommaSeparatedHint: ',例如 a,b',
configLabels: {

View File

@@ -931,7 +931,7 @@ html.theme-dark .brand-button {
}
.ui-form-label {
color: var(--color-zinc-700);
color: #000;
font-size: 0.875rem;
font-weight: 600;
}
@@ -951,27 +951,26 @@ html.theme-dark .brand-button {
.ui-boolean-head {
display: grid;
grid-template-columns: 40px minmax(0, 1fr);
column-gap: 0.85rem;
grid-template-columns: 32px minmax(0, 1fr);
column-gap: 0.65rem;
align-items: center;
}
.ui-boolean-card {
min-height: 92px;
padding: 1rem 1.1rem;
min-height: 56px;
padding: 0.75rem 0.85rem;
}
.ui-boolean-card-detailed {
min-height: 132px;
padding: 1.1rem 1.15rem;
min-height: 80px;
padding: 0.75rem 0.85rem;
}
.ui-checkbox-field {
display: flex;
min-height: 92px;
flex-direction: column;
justify-content: space-between;
padding: 1rem 1.1rem;
padding: 0.75rem 0.85rem;
}
.ui-toolbar-checkbox {
@@ -992,49 +991,66 @@ html.theme-dark .brand-button {
border-color: color-mix(in srgb, var(--color-indigo-400) 34%, var(--color-zinc-800));
}
.ui-checkbox {
appearance: none;
-webkit-appearance: none;
width: 0.95rem;
height: 0.95rem;
flex-shrink: 0;
border: 1.5px solid rgb(148 163 184 / 0.9);
border-radius: 0.3rem;
background: rgb(255 255 255 / 0.94);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.95);
background-repeat: no-repeat;
background-position: center;
background-size: 0.62rem 0.62rem;
transition:
border-color 160ms ease,
background-color 160ms ease,
box-shadow 160ms ease,
transform 160ms ease;
.ui-switch {
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
flex-shrink: 0;
}
.ui-checkbox:hover {
border-color: rgb(249 115 22 / 0.72);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.95),
0 0 0 3px rgb(249 115 22 / 0.08);
.ui-switch-input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.ui-checkbox:checked {
border-color: var(--color-indigo-500);
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3 7.2L5.7 10L11 4.6' stroke='white' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
linear-gradient(135deg, var(--button-start) 0%, var(--button-end) 100%);
box-shadow:
0 0 0 2px rgb(249 115 22 / 0.12),
0 4px 12px rgb(249 115 22 / 0.18);
.ui-switch-track {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
border-radius: 9999px;
background: rgb(200 210 220 / 0.6);
border: 1.5px solid rgb(120 130 145 / 0.7);
transition: background-color 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
}
.ui-checkbox:focus-visible {
.ui-switch-knob {
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
border-radius: 9999px;
background: white;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.15);
transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.ui-switch:hover .ui-switch-track {
border-color: rgb(249 115 22 / 0.5);
box-shadow: 0 0 0 3px rgb(249 115 22 / 0.08);
}
.ui-switch-input:checked + .ui-switch-track {
background: linear-gradient(135deg, rgb(249 115 22 / 0.92) 0%, rgb(245 158 11 / 0.92) 100%);
border-color: rgb(249 115 22 / 0.7);
}
.ui-switch-input:checked + .ui-switch-track .ui-switch-knob {
transform: translateX(16px);
}
.ui-switch-input:focus-visible + .ui-switch-track {
outline: none;
box-shadow:
0 0 0 3px rgb(249 115 22 / 0.14),
inset 0 0 0 2px rgb(255 255 255 / 0.96);
box-shadow: 0 0 0 3px rgb(249 115 22 / 0.18);
}
.ui-input,
@@ -1143,37 +1159,29 @@ html.theme-dark .ui-textarea::placeholder {
color: rgb(111 131 155 / 0.9);
}
html.theme-dark .ui-checkbox {
border-color: rgb(151 170 194 / 0.86);
background: rgb(14 24 40 / 0.92);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.06),
0 0 0 1px rgb(8 12 20 / 0.32);
html.theme-dark .ui-switch-track {
background: rgb(50 60 75 / 0.7);
border-color: rgb(100 116 139 / 0.5);
}
html.theme-dark .ui-checkbox:hover {
border-color: rgb(241 165 97 / 0.78);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.06),
0 0 0 1px rgb(8 12 20 / 0.32),
0 0 0 3px rgb(232 132 58 / 0.12);
html.theme-dark .ui-switch:hover .ui-switch-track {
border-color: rgb(251 146 60 / 0.6);
box-shadow: 0 0 0 3px rgb(251 146 60 / 0.12);
}
html.theme-dark .ui-checkbox:checked {
border-color: rgb(241 165 97 / 0.78);
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3 7.2L5.7 10L11 4.6' stroke='white' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
linear-gradient(135deg, rgb(249 115 22 / 0.96) 0%, rgb(245 158 11 / 0.96) 100%);
box-shadow:
0 0 0 1px rgb(255 196 128 / 0.24),
0 0 0 3px rgb(232 132 58 / 0.16),
0 6px 16px rgb(232 132 58 / 0.2);
html.theme-dark .ui-switch-input:checked + .ui-switch-track {
background: linear-gradient(135deg, rgb(249 115 22 / 0.96) 0%, rgb(245 158 11 / 0.96) 100%);
border-color: rgb(251 146 60 / 0.7);
box-shadow: 0 0 0 1px rgb(255 196 128 / 0.18), 0 4px 12px rgb(232 132 58 / 0.18);
}
html.theme-dark .ui-checkbox:focus-visible {
box-shadow:
0 0 0 3px rgb(232 132 58 / 0.18),
inset 0 0 0 2px rgb(9 16 28 / 0.95);
html.theme-dark .ui-switch-knob {
background: rgb(226 232 240 / 0.96);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.3);
}
html.theme-dark .ui-switch-input:focus-visible + .ui-switch-track {
box-shadow: 0 0 0 3px rgb(251 146 60 / 0.2);
}
.ui-select option {

View File

@@ -15,7 +15,7 @@ import {
} from '../components/channel/channelSchema';
import WhatsAppQRCodePanel from '../components/channel/WhatsAppQRCodePanel';
import WhatsAppStatusPanel from '../components/channel/WhatsAppStatusPanel';
import { CheckboxField } from '../components/ui/FormControls';
import { SwitchField } from '../components/ui/FormControls';
import PageHeader from '../components/layout/PageHeader';
import type { ChannelKey } from '../components/channel/channelSchema';
import { cloneJSON } from '../utils/object';
@@ -204,7 +204,7 @@ const ChannelSettings: React.FC = () => {
const stateLabel = wa?.connected ? t('online') : wa?.logged_in ? t('whatsappStateDisconnected') : wa?.qr_available ? t('whatsappStateAwaitingScan') : t('offline');
return (
<div className="space-y-6 px-5 py-5 md:px-7 md:py-6 xl:px-8">
<div className="space-y-4 px-5 py-5 md:px-7 md:py-6 xl:px-8">
<PageHeader
title={t(definition.titleKey)}
titleClassName="ui-text-primary text-3xl font-bold"
@@ -224,18 +224,23 @@ const ChannelSettings: React.FC = () => {
}
/>
<div className={`grid gap-6 ${key === 'whatsapp' ? 'xl:grid-cols-[1fr_0.92fr]' : ''}`}>
<div className="space-y-4">
{definition.sections.map((section) => {
<div className={`grid gap-4 ${key === 'whatsapp' ? 'xl:grid-cols-[1fr_0.92fr]' : ''}`}>
<div className="brand-card ui-panel rounded-2xl p-5">
{definition.sections.map((section, idx) => {
const Icon = getChannelSectionIcon(section.id);
return (
<ChannelSectionCard
key={section.id}
icon={<Icon className="ui-icon-muted h-[18px] w-[18px]" />}
title={t(section.titleKey)}
hint={t(section.hintKey)}
>
<div className={`grid gap-4 ${section.columns === 1 ? 'grid-cols-1' : 'lg:grid-cols-2'}`}>
<div key={section.id}>
{idx > 0 && <hr className="ui-border-subtle my-4 border-t" />}
<div className="ui-section-header mb-3">
<div className="ui-subpanel flex h-11 w-11 shrink-0 items-center justify-center">
<Icon className="ui-icon-muted h-[18px] w-[18px]" />
</div>
<div className="min-w-0">
<div className="ui-text-primary text-lg font-semibold">{t(section.titleKey)}</div>
<p className="ui-text-muted mt-0.5 text-sm">{t(section.hintKey)}</p>
</div>
</div>
<div className={`grid gap-3 ${section.columns === 1 ? 'grid-cols-1' : 'lg:grid-cols-2'}`}>
{section.fields.map((field) => (
<ChannelFieldRenderer
key={field.key}
@@ -249,7 +254,7 @@ const ChannelSettings: React.FC = () => {
/>
))}
</div>
</ChannelSectionCard>
</div>
);
})}
</div>

View File

@@ -350,7 +350,7 @@ const Chat: React.FC = () => {
return (
<div className="flex h-full min-w-0 p-4 md:p-6 xl:p-8">
<div className="flex-1 flex flex-col brand-card ui-panel rounded-[30px] overflow-hidden">
<div className="flex-1 flex flex-col brand-card ui-panel rounded-2xl overflow-hidden">
<div className="ui-surface-muted ui-border-subtle px-4 py-3 border-b flex items-center gap-2 min-w-0 overflow-x-auto">
<div className="flex items-center gap-2 min-w-0 shrink-0">
<Button onClick={() => setChatTab('main')} variant={chatTab === 'main' ? 'primary' : 'neutral'} size="xs">{t('mainChat')}</Button>

View File

@@ -100,7 +100,7 @@ const Config: React.FC = () => {
t={t}
/>
<div className="flex-1 brand-card ui-border-subtle border rounded-[30px] overflow-hidden flex flex-col shadow-sm min-h-[420px]">
<div className="flex-1 brand-card ui-border-subtle border rounded-2xl overflow-hidden flex flex-col shadow-sm min-h-[420px]">
{!showRaw ? (
<div className="flex-1 flex min-h-0">
<ConfigSidebar

View File

@@ -6,7 +6,7 @@ import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/ui/Button';
import EmptyState from '../components/data-display/EmptyState';
import { CheckboxCardField, FieldBlock, SelectField, TextField, TextareaField } from '../components/ui/FormControls';
import { SwitchCardField, FieldBlock, SelectField, TextField, TextareaField } from '../components/ui/FormControls';
import { ModalBackdrop, ModalBody, ModalCard, ModalFooter, ModalHeader, ModalShell } from '../components/ui/ModalFrame';
import PageHeader from '../components/layout/PageHeader';
import { CronJob } from '../types';
@@ -192,7 +192,7 @@ const Cron: React.FC = () => {
{cron.map((j) => {
const schedule = formatSchedule(j, t);
return (
<div key={j.id} className="brand-card rounded-[30px] border border-zinc-800/80 p-6 flex flex-col group hover:border-zinc-700/50 transition-colors">
<div key={j.id} className="brand-card rounded-2xl border border-zinc-800/80 p-6 flex flex-col group hover:border-zinc-700/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="font-semibold text-zinc-100 mb-1">{j.name || j.id}</h3>
@@ -344,15 +344,15 @@ const Cron: React.FC = () => {
</div>
<div className="grid grid-cols-1 gap-3 pt-2 md:grid-cols-2">
<CheckboxCardField
<SwitchCardField
checked={cronForm.deliver}
help={t('cronDeliverHint', { defaultValue: 'Send the message through the selected channel.' })}
help={t('cronDeliverHint')}
label={t('deliver')}
onChange={(checked) => setCronForm({ ...cronForm, deliver: checked })}
/>
<CheckboxCardField
<SwitchCardField
checked={cronForm.enabled}
help={t('cronEnabledHint', { defaultValue: 'Enable this cron job immediately after saving.' })}
help={t('cronEnabledHint')}
label={t('active')}
onChange={(checked) => setCronForm({ ...cronForm, enabled: checked })}
/>

View File

@@ -62,7 +62,7 @@ const Dashboard: React.FC = () => {
}, [nodeAlerts]);
return (
<div className="p-4 md:p-6 xl:p-8 w-full space-y-6 xl:space-y-8">
<div className="p-4 md:p-5 w-full space-y-4">
<PageHeader
title={t('dashboard')}
subtitle={
@@ -138,7 +138,7 @@ const Dashboard: React.FC = () => {
)}
</SectionPanel>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6 items-stretch">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 items-stretch">
<SectionPanel title={t('taskAudit')} icon={<Activity className="w-5 h-5 text-zinc-400" />} className="min-h-[340px] h-full">
<div className="space-y-3">
{recentTasks.length === 0 ? (

View File

@@ -56,7 +56,7 @@ const LogCodes: React.FC = () => {
}
/>
<div className="brand-card ui-border-subtle border rounded-[30px] overflow-hidden">
<div className="brand-card ui-border-subtle border rounded-2xl overflow-hidden">
<table className="w-full text-sm">
<thead className="ui-soft-panel ui-border-subtle border-b">
<tr className="ui-text-secondary">

View File

@@ -95,18 +95,20 @@ const Logs: React.FC = () => {
};
return (
<div className="p-4 md:p-6 xl:p-8 w-full space-y-4 h-full flex flex-col">
<div className="p-3 md:p-4 w-full space-y-3 h-full flex flex-col min-h-0">
<PageHeader
title={t('logs')}
titleClassName="ui-text-primary"
subtitle={
<div className={`ui-pill flex items-center gap-1.5 px-2.5 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider border ${
isStreaming ? 'ui-pill-success' : 'ui-pill-neutral'
}`}>
<div className={`w-1.5 h-1.5 rounded-full ${isStreaming ? 'ui-dot-live animate-pulse' : 'ui-dot-neutral'}`} />
{isStreaming ? t('live') : t('paused')}
title={
<div className="flex items-center gap-3">
<span>{t('logs')}</span>
<div className={`ui-pill flex items-center gap-1.5 px-2.5 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wider border ${
isStreaming ? 'ui-pill-success' : 'ui-pill-neutral'
}`}>
<div className={`w-1.5 h-1.5 rounded-full ${isStreaming ? 'ui-dot-live animate-pulse' : 'ui-dot-neutral'}`} />
{isStreaming ? t('live') : t('paused')}
</div>
</div>
}
titleClassName="ui-text-primary flex items-center gap-3"
actions={
<ToolbarRow>
<Button onClick={() => setShowRaw(!showRaw)} gap="2">
@@ -122,7 +124,7 @@ const Logs: React.FC = () => {
}
/>
<div className="flex-1 brand-card ui-border-subtle border rounded-[30px] overflow-hidden flex flex-col shadow-2xl">
<div className="flex-1 brand-card ui-border-subtle border rounded-2xl overflow-hidden flex flex-col shadow-2xl">
<div className="ui-soft-panel ui-border-subtle px-4 py-2 border-b flex items-center justify-between">
<div className="flex items-center gap-2">
<Terminal className="ui-icon-muted w-4 h-4" />

View File

@@ -116,7 +116,7 @@ const Memory: React.FC = () => {
return (
<div className="h-full p-4 md:p-5 xl:p-6">
<div className="flex h-full flex-col overflow-hidden rounded-[30px] border brand-card ui-border-subtle lg:flex-row">
<div className="flex h-full flex-col overflow-hidden rounded-2xl border brand-card ui-border-subtle lg:flex-row">
<aside className="ui-border-subtle w-full overflow-y-auto border-b p-2 md:p-3 lg:w-72 lg:border-r lg:border-b-0">
<div className="sidebar-section rounded-[24px] p-2 md:p-2.5 space-y-1">
<div className="flex items-center justify-between">

View File

@@ -206,7 +206,7 @@ const Providers: React.FC = () => {
}
/>
<div className="brand-card ui-border-subtle border rounded-[30px] p-4 md:p-6 space-y-4">
<div className="brand-card ui-border-subtle border rounded-2xl p-4 md:p-6 space-y-4">
<ProviderRuntimeToolbar
newProxyName={newProxyName}
onAddProxy={() => addProxy(newProxyName)}

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/ui/Button';
import { TextField, TextareaField, ToolbarCheckboxField } from '../components/ui/FormControls';
import { TextField, TextareaField, ToolbarSwitchField } from '../components/ui/FormControls';
import { ModalBackdrop, ModalBody, ModalCard, ModalHeader, ModalShell } from '../components/ui/ModalFrame';
import PageHeader from '../components/layout/PageHeader';
import ToolbarRow from '../components/layout/ToolbarRow';
@@ -231,7 +231,7 @@ const Skills: React.FC = () => {
>
{installingSkill ? t('loading') : t('install')}
</Button>
<ToolbarCheckboxField
<ToolbarSwitchField
checked={ignoreSuspicious}
className={installingSkill ? 'pointer-events-none opacity-60' : 'shrink-0'}
help={t('skillsIgnoreSuspiciousHint', { defaultValue: 'Use --force to ignore suspicious package warnings.' })}
@@ -256,7 +256,7 @@ const Skills: React.FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-6">
{skills.map(s => (
<div key={s.id} className="brand-card rounded-[28px] border border-zinc-800/80 p-6 flex flex-col shadow-sm group hover:border-zinc-700/50 transition-colors">
<div key={s.id} className="brand-card rounded-2xl border border-zinc-800/80 p-6 flex flex-col shadow-sm group hover:border-zinc-700/50 transition-colors">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-zinc-800/50 flex items-center justify-center border border-zinc-700/50">

View File

@@ -238,7 +238,7 @@ const SubagentProfiles: React.FC = () => {
<div className="flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-[360px_1fr] gap-4">
<ProfileListPanel
emptyLabel="No subagent profiles."
emptyLabel={t('profileEmptyLabel')}
items={items}
onSelect={onSelect}
selectedId={selectedId}
@@ -263,15 +263,15 @@ const SubagentProfiles: React.FC = () => {
promptPlaceholder={t('agentPromptContentPlaceholder')}
promptPathHint={promptPathHint}
promptPathInvalid={!!promptPathErrorKey}
roleLabel="Role"
roleLabel={t('profileRoleLabel')}
saving={saving}
statusLabel={t('status')}
toolAllowlistHint={<><span className="ui-text-subtle font-mono">skill_exec</span> is inherited automatically and does not need to be listed here.</>}
toolAllowlistHint={<><span className="ui-text-subtle font-mono">skill_exec</span> {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')}
/>
</div>
</div>

View File

@@ -111,15 +111,15 @@ const TaskAudit: React.FC = () => {
title={t('taskAudit')}
titleClassName="text-xl md:text-2xl font-semibold"
actions={(
<ToolbarRow className="flex-nowrap">
<SelectField dense className="w-[152px] shrink-0" value={sourceFilter} onChange={(e) => setSourceFilter(e.target.value)}>
<ToolbarRow className="flex flex-wrap sm:flex-nowrap justify-end items-center gap-2">
<SelectField dense className="w-full sm:w-[152px] shrink-0" value={sourceFilter} onChange={(e) => setSourceFilter(e.target.value)}>
<option value="all">{t('allSources')}</option>
<option value="direct">{t('sourceDirect')}</option>
<option value="memory_todo">{t('sourceMemoryTodo')}</option>
<option value="task_watchdog">task_watchdog</option>
<option value="-">-</option>
</SelectField>
<SelectField dense className="w-[152px] shrink-0" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<SelectField dense className="w-full sm:w-[152px] shrink-0" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="all">{t('allStatus')}</option>
<option value="running">{t('statusRunning')}</option>
<option value="waiting">{t('statusWaiting')}</option>