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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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