mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-12 14:07:30 +08:00
Update UI components from checkboxes to switches and introduce a recursive configuration component.
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -21,4 +21,9 @@ build
|
||||
/clawgo_test
|
||||
|
||||
.gocache
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
cmd/workspace/*
|
||||
channels
|
||||
|
||||
*.pid
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')}
|
||||
|
||||
35
webui/src/components/ui/.!18152!RecursiveConfig.tsx
Normal file
35
webui/src/components/ui/.!18152!RecursiveConfig.tsx
Normal 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(() => {
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })}
|
||||
/>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user