refresh webui forms and fix whatsapp bridge login state

This commit is contained in:
LPF
2026-03-11 00:26:33 +08:00
parent cfab4cd1cc
commit d9872c3da7
26 changed files with 632 additions and 308 deletions

View File

@@ -1156,7 +1156,7 @@ func buildHeartbeatService(cfg *config.Config, msgBus *bus.MessageBus) *heartbea
}
func setupEmbeddedWhatsAppBridge(ctx context.Context, cfg *config.Config) (*channels.WhatsAppBridgeService, bool) {
if cfg == nil || !cfg.Channels.WhatsApp.Enabled || !shouldEmbedWhatsAppBridge(cfg) {
if !shouldStartEmbeddedWhatsAppBridge(cfg) {
return nil, false
}
cfg.Channels.WhatsApp.BridgeURL = embeddedWhatsAppBridgeURL(cfg)
@@ -1169,6 +1169,10 @@ func setupEmbeddedWhatsAppBridge(ctx context.Context, cfg *config.Config) (*chan
return svc, true
}
func shouldStartEmbeddedWhatsAppBridge(cfg *config.Config) bool {
return cfg != nil && shouldEmbedWhatsAppBridge(cfg)
}
func shouldEmbedWhatsAppBridge(cfg *config.Config) bool {
raw := strings.TrimSpace(cfg.Channels.WhatsApp.BridgeURL)
if raw == "" {

32
cmd/cmd_gateway_test.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"testing"
"github.com/YspCoder/clawgo/pkg/config"
)
func TestShouldStartEmbeddedWhatsAppBridge(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Channels.WhatsApp.Enabled = false
cfg.Channels.WhatsApp.BridgeURL = ""
if !shouldStartEmbeddedWhatsAppBridge(cfg) {
t.Fatalf("expected embedded bridge to start when using default embedded url")
}
cfg.Channels.WhatsApp.BridgeURL = "ws://127.0.0.1:3001"
if !shouldStartEmbeddedWhatsAppBridge(cfg) {
t.Fatalf("expected embedded bridge to start for legacy local bridge url")
}
cfg.Channels.WhatsApp.BridgeURL = "ws://example.com:3001/ws"
if shouldStartEmbeddedWhatsAppBridge(cfg) {
t.Fatalf("expected external bridge url to disable embedded bridge")
}
if shouldStartEmbeddedWhatsAppBridge(nil) {
t.Fatalf("expected nil config to disable embedded bridge")
}
}

79
scripts/build-slim.ps1 Normal file
View File

@@ -0,0 +1,79 @@
[CmdletBinding()]
param(
[string]$Output = "build/clawgo-windows-amd64-slim.exe",
[switch]$EmbedWebUI,
[switch]$Compress
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$embedDir = Join-Path $repoRoot "cmd/workspace"
$workspaceDir = Join-Path $repoRoot "workspace"
$webuiDistDir = Join-Path $repoRoot "webui/dist"
$outputPath = Join-Path $repoRoot $Output
function Copy-DirectoryContents {
param(
[Parameter(Mandatory = $true)][string]$Source,
[Parameter(Mandatory = $true)][string]$Destination
)
if (Test-Path $Destination) {
Remove-Item -Recurse -Force $Destination
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
Copy-Item -Path (Join-Path $Source "*") -Destination $Destination -Recurse -Force
}
try {
if (-not (Test-Path $workspaceDir)) {
throw "Missing workspace source directory: $workspaceDir"
}
Copy-DirectoryContents -Source $workspaceDir -Destination $embedDir
if ($EmbedWebUI) {
if (-not (Test-Path $webuiDistDir)) {
throw "EmbedWebUI was requested, but WebUI dist is missing: $webuiDistDir"
}
$embedWebuiDir = Join-Path $embedDir "webui"
Copy-DirectoryContents -Source $webuiDistDir -Destination $embedWebuiDir
}
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $outputPath) | Out-Null
$env:CGO_ENABLED = "0"
$env:GOOS = "windows"
$env:GOARCH = "amd64"
$version = "dev"
$buildTime = [DateTimeOffset]::Now.ToString("yyyy-MM-ddTHH:mm:sszzz")
$ldflags = "-X main.version=$version -X main.buildTime=$buildTime -s -w -buildid="
$tags = "purego,netgo,osusergo"
& go build -trimpath -buildvcs=false -tags $tags -ldflags $ldflags -o $outputPath ./cmd
if ($LASTEXITCODE -ne 0) {
throw "go build failed with exit code $LASTEXITCODE"
}
if ($Compress) {
$upx = Get-Command upx -ErrorAction SilentlyContinue
if ($null -ne $upx) {
& $upx.Source --best --lzma $outputPath | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "upx failed with exit code $LASTEXITCODE"
}
} else {
Write-Warning "Compress was requested, but upx was not found in PATH"
}
}
$sizeMB = [Math]::Round((Get-Item $outputPath).Length / 1MB, 2)
Write-Host "Build complete: $outputPath ($sizeMB MB)"
}
finally {
if (Test-Path $embedDir) {
Remove-Item -Recurse -Force $embedDir
}
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
type CheckboxProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'>;
function joinClasses(...values: Array<string | undefined | false>) {
return values.filter(Boolean).join(' ');
}
const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(
{ className, ...props },
ref,
) {
return (
<input
{...props}
ref={ref}
type="checkbox"
className={joinClasses('ui-checkbox', className)}
/>
);
});
export default Checkbox;
export type { CheckboxProps };

View File

@@ -0,0 +1,38 @@
import React from 'react';
type FieldCardProps = {
title?: React.ReactNode;
hint?: React.ReactNode;
className?: string;
titleClassName?: string;
hintClassName?: string;
children: React.ReactNode;
};
function joinClasses(...values: Array<string | undefined | false>) {
return values.filter(Boolean).join(' ');
}
const FieldCard: React.FC<FieldCardProps> = ({
title,
hint,
className,
titleClassName,
hintClassName,
children,
}) => {
return (
<div className={joinClasses('rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2', className)}>
{title !== undefined && title !== null && (
<div className={joinClasses('text-zinc-300', titleClassName)}>{title}</div>
)}
{hint !== undefined && hint !== null && (
<div className={joinClasses('text-zinc-500', hintClassName)}>{hint}</div>
)}
{children}
</div>
);
};
export default FieldCard;
export type { FieldCardProps };

View File

@@ -0,0 +1,38 @@
import React from 'react';
type FormFieldProps = {
label?: React.ReactNode;
help?: React.ReactNode;
className?: string;
labelClassName?: string;
helpClassName?: string;
children: React.ReactNode;
};
function joinClasses(...values: Array<string | undefined | false>) {
return values.filter(Boolean).join(' ');
}
const FormField: React.FC<FormFieldProps> = ({
label,
help,
className,
labelClassName,
helpClassName,
children,
}) => {
return (
<label className={joinClasses('block space-y-1.5', className)}>
{label !== undefined && label !== null && (
<div className={joinClasses('ui-form-label', labelClassName)}>{label}</div>
)}
{help !== undefined && help !== null && (
<div className={joinClasses('ui-form-help', helpClassName)}>{help}</div>
)}
{children}
</label>
);
};
export default FormField;
export type { FormFieldProps };

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { useTranslation } from 'react-i18next';
import { Button } from './Button';
import Input from './Input';
type DialogOptions = {
title?: string;
@@ -45,7 +46,7 @@ export const GlobalDialog: React.FC<{
{kind === 'prompt' && (
<div className="space-y-2">
{options.inputLabel && <label className="text-xs text-zinc-400">{options.inputLabel}</label>}
<input
<Input
autoFocus
value={value}
onChange={(e) => setValue(e.target.value)}

View File

@@ -0,0 +1,23 @@
import React from 'react';
type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
function joinClasses(...values: Array<string | undefined | false>) {
return values.filter(Boolean).join(' ');
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(
{ className, ...props },
ref,
) {
return (
<input
{...props}
ref={ref}
className={joinClasses('ui-input', className)}
/>
);
});
export default Input;
export type { InputProps };

View File

@@ -2,6 +2,10 @@ import React, { useMemo, useState } from 'react';
import { Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { FixedButton } from './Button';
import Checkbox from './Checkbox';
import Input from './Input';
import Select from './Select';
import Textarea from './Textarea';
interface RecursiveConfigProps {
data: any;
@@ -67,12 +71,12 @@ const PrimitiveArrayEditor: React.FC<{
</div>
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto_auto] gap-2">
<input
<Input
list={`${path}-suggestions`}
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder={t('recursiveAddValuePlaceholder')}
className="ui-input rounded-xl px-3 py-2 text-sm"
className="rounded-xl px-3 py-2 text-sm"
/>
<datalist id={`${path}-suggestions`}>
{suggestions.map((s) => (
@@ -91,7 +95,7 @@ const PrimitiveArrayEditor: React.FC<{
<Plus className="w-4 h-4" />
</FixedButton>
<select
<Select
value={selected}
onChange={(e) => {
const v = e.target.value;
@@ -104,7 +108,7 @@ const PrimitiveArrayEditor: React.FC<{
{suggestions.filter((s) => !value.includes(s)).map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</Select>
</div>
</div>
);
@@ -140,7 +144,7 @@ const RecursiveConfig: React.FC<RecursiveConfigProps> = ({ data, labels, path =
onChange={(next) => onChange(currentPath, next)}
/>
) : (
<textarea
<Textarea
value={JSON.stringify(value, null, 2)}
onChange={(e) => {
try {
@@ -150,7 +154,7 @@ const RecursiveConfig: React.FC<RecursiveConfigProps> = ({ data, labels, path =
// ignore invalid json during typing
}
}}
className="ui-textarea w-full min-h-28 rounded-xl px-3 py-2 text-sm font-mono"
className="w-full min-h-28 rounded-xl px-3 py-2 text-sm font-mono"
/>
)}
</div>
@@ -180,22 +184,21 @@ const RecursiveConfig: React.FC<RecursiveConfigProps> = ({ data, labels, path =
</div>
{typeof value === 'boolean' ? (
<label className="ui-toggle-card flex items-center gap-3 p-3 cursor-pointer transition-colors group">
<input
type="checkbox"
<Checkbox
checked={value}
onChange={(e) => onChange(currentPath, e.target.checked)}
className="w-4 h-4 rounded border-zinc-700 text-indigo-500 focus:ring-indigo-500"
className="w-4 h-4"
/>
<span className="ui-text-subtle group-hover:ui-text-secondary text-sm transition-colors">
{value ? (labels['enabled_true'] || t('enabled_true')) : (labels['enabled_false'] || t('enabled_false'))}
</span>
</label>
) : (
<input
<Input
type={typeof value === 'number' ? 'number' : 'text'}
value={value === null || value === undefined ? '' : String(value)}
onChange={(e) => onChange(currentPath, typeof value === 'number' ? Number(e.target.value) : e.target.value)}
className="ui-input w-full rounded-xl px-3 py-2.5 text-sm transition-colors font-mono"
className="w-full rounded-xl px-3 py-2.5 text-sm transition-colors font-mono"
/>
)}
</div>

View File

@@ -0,0 +1,25 @@
import React from 'react';
type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
function joinClasses(...values: Array<string | undefined | false>) {
return values.filter(Boolean).join(' ');
}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(function Select(
{ className, children, ...props },
ref,
) {
return (
<select
{...props}
ref={ref}
className={joinClasses('ui-select', className)}
>
{children}
</select>
);
});
export default Select;
export type { SelectProps };

View File

@@ -3,6 +3,7 @@ import { NavLink, useLocation } from 'react-router-dom';
import { LayoutDashboard, MessageSquare, Settings, Clock, Terminal, Zap, FolderOpen, ClipboardList, BrainCircuit, Hash, Bot, Boxes, PanelLeftClose, PanelLeftOpen, Plug, Smartphone, ChevronDown, Radio, MonitorSmartphone } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import Input from './Input';
import NavItem from './NavItem';
const Sidebar: React.FC = () => {
@@ -154,7 +155,7 @@ const Sidebar: React.FC = () => {
{!sidebarCollapsed ? (
<div className="p-3 border-t border-zinc-800 bg-zinc-900/20">
<div className="text-[11px] font-medium text-zinc-500 mb-1 uppercase tracking-wider px-1">{t('gatewayToken')}</div>
<input
<Input
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}

View File

@@ -0,0 +1,23 @@
import React from 'react';
type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
function joinClasses(...values: Array<string | undefined | false>) {
return values.filter(Boolean).join(' ');
}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(
{ className, ...props },
ref,
) {
return (
<textarea
{...props}
ref={ref}
className={joinClasses('ui-textarea', className)}
/>
);
});
export default Textarea;
export type { TextareaProps };

View File

@@ -975,21 +975,33 @@ html.theme-dark .brand-button {
border-radius: 0.4rem;
background: rgb(255 255 255 / 0.94);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.95);
transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
background-repeat: no-repeat;
background-position: center;
background-size: 0.72rem 0.72rem;
transition:
border-color 160ms ease,
background-color 160ms ease,
box-shadow 160ms ease,
transform 160ms ease;
cursor: pointer;
}
.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-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:
inset 0 0 0 2px rgb(255 255 255 / 0.96),
0 0 0 3px rgb(249 115 22 / 0.12);
0 0 0 3px rgb(249 115 22 / 0.14),
0 8px 18px rgb(249 115 22 / 0.2);
transform: translateY(-0.5px);
}
.ui-checkbox:focus-visible {
@@ -1022,12 +1034,10 @@ html.theme-dark .brand-button {
-moz-appearance: none;
cursor: pointer;
padding-right: 2.75rem;
background-image:
linear-gradient(180deg, rgb(255 255 255 / 0.08), rgb(255 255 255 / 0)),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6.5L8 10L12 6.5' stroke='%2364758b' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat, no-repeat;
background-position: 0 0, right 0.95rem center;
background-size: auto, 16px 16px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6.5L8 10L12 6.5' stroke='%2364758b' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.95rem center;
background-size: 16px 16px;
}
.ui-select:hover {
@@ -1077,9 +1087,7 @@ html.theme-dark .ui-select {
}
html.theme-dark .ui-select {
background-image:
linear-gradient(180deg, rgb(255 255 255 / 0.03), rgb(255 255 255 / 0)),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6.5L8 10L12 6.5' stroke='%2390a4bc' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6.5L8 10L12 6.5' stroke='%2390a4bc' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
html.theme-dark .ui-select:hover {
@@ -1093,20 +1101,30 @@ html.theme-dark .ui-textarea::placeholder {
}
html.theme-dark .ui-checkbox {
border-color: rgb(111 131 155 / 0.85);
background: rgb(9 16 28 / 0.76);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.04);
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-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-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:
inset 0 0 0 2px rgb(9 16 28 / 0.95),
0 0 0 3px rgb(232 132 58 / 0.16);
0 0 0 1px rgb(255 196 128 / 0.24),
0 0 0 4px rgb(232 132 58 / 0.18),
0 10px 22px rgb(232 132 58 / 0.22);
}
html.theme-dark .ui-checkbox:focus-visible {
@@ -1115,6 +1133,16 @@ html.theme-dark .ui-checkbox:focus-visible {
inset 0 0 0 2px rgb(9 16 28 / 0.95);
}
.ui-select option {
background: rgb(255 255 255 / 0.98);
color: rgb(51 65 85 / 0.98);
}
html.theme-dark .ui-select option {
background: rgb(12 20 34 / 0.98);
color: rgb(226 232 240 / 0.96);
}
.ui-soft-panel {
border: 1px solid var(--color-zinc-800);
border-radius: var(--radius-subtle);

View File

@@ -5,6 +5,10 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import Checkbox from '../components/Checkbox';
import FormField from '../components/FormField';
import Input from '../components/Input';
import Textarea from '../components/Textarea';
type ChannelKey = 'telegram' | 'whatsapp' | 'discord' | 'feishu' | 'qq' | 'dingtalk' | 'maixcam';
@@ -526,11 +530,10 @@ const ChannelSettings: React.FC = () => {
{t(value ? 'enabled_true' : 'enabled_false')}
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={!!value}
onChange={(e) => setDraft((prev) => ({ ...prev, [field.key]: e.target.checked }))}
className="ui-checkbox mt-1"
className="mt-1"
/>
</label>
);
@@ -543,11 +546,9 @@ const ChannelSettings: React.FC = () => {
{t(value ? 'enabled_true' : 'enabled_false')}
</div>
</div>
<input
type="checkbox"
<Checkbox
checked={!!value}
onChange={(e) => setDraft((prev) => ({ ...prev, [field.key]: e.target.checked }))}
className="ui-checkbox"
/>
</label>
);
@@ -564,33 +565,37 @@ const ChannelSettings: React.FC = () => {
)}
</div>
{helper && <div className="ui-form-help">{helper}</div>}
<textarea
<Textarea
value={formatList(value)}
onChange={(e) => setDraft((prev) => ({ ...prev, [field.key]: parseList(e.target.value) }))}
placeholder={field.placeholder || ''}
className={`ui-textarea px-4 py-3 text-sm ${isWhatsApp ? 'min-h-36 font-mono' : 'min-h-32'}`}
className={`px-4 py-3 text-sm ${isWhatsApp ? 'min-h-36 font-mono' : 'min-h-32'}`}
/>
{isWhatsApp && <div className="ui-form-help text-[11px]">{t('whatsappFieldAllowFromFootnote')}</div>}
</div>
);
}
return (
<div key={field.key} className={`ui-form-field ${isWhatsApp && field.key === 'bridge_url' ? 'lg:col-span-2' : ''}`}>
<label className="ui-form-label">{label}</label>
{helper && <div className="ui-form-help">{helper}</div>}
<input
<FormField
key={field.key}
label={label}
help={helper}
className={`ui-form-field ${isWhatsApp && field.key === 'bridge_url' ? 'lg:col-span-2' : ''}`}
>
<Input
type={field.type}
value={value === null || value === undefined ? '' : String(value)}
onChange={(e) => setDraft((prev) => ({ ...prev, [field.key]: field.type === 'number' ? Number(e.target.value || 0) : e.target.value }))}
placeholder={field.placeholder || ''}
className={`ui-input px-4 py-3 text-sm ${isWhatsApp && field.key === 'bridge_url' ? 'font-mono' : ''}`}
className={`px-4 py-3 text-sm ${isWhatsApp && field.key === 'bridge_url' ? 'font-mono' : ''}`}
/>
</div>
</FormField>
);
};
const wa = waStatus?.status;
const stateLabel = wa?.connected ? t('online') : wa?.logged_in ? t('whatsappStateDisconnected') : wa?.qr_available ? t('whatsappStateAwaitingScan') : t('offline');
const canLogout = !!(wa?.logged_in || wa?.connected || wa?.user_jid);
return (
<div className="space-y-6 px-5 py-5 md:px-7 md:py-6 xl:px-8">
@@ -647,10 +652,12 @@ const ChannelSettings: React.FC = () => {
<div className="ui-text-primary mt-1 text-2xl font-semibold">{stateLabel}</div>
</div>
</div>
<Button onClick={handleLogout} variant="danger" gap="2">
<LogOut className="h-4 w-4" />
{t('logout')}
</Button>
{canLogout ? (
<Button onClick={handleLogout} variant="danger" gap="2">
<LogOut className="h-4 w-4" />
{t('logout')}
</Button>
) : null}
</div>
<div className="grid gap-4 sm:grid-cols-2">

View File

@@ -5,6 +5,9 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import Input from '../components/Input';
import Select from '../components/Select';
import Textarea from '../components/Textarea';
import { ChatItem } from '../types';
type StreamItem = {
@@ -574,9 +577,9 @@ const Chat: React.FC = () => {
<Button onClick={() => setChatTab('subagents')} variant={chatTab === 'subagents' ? 'primary' : 'neutral'} size="xs">{t('subagentGroup')}</Button>
</div>
{chatTab === 'main' && (
<select value={sessionKey} onChange={(e) => setSessionKey(e.target.value)} className="ui-select min-w-[220px] flex-1 rounded-xl px-2.5 py-1.5 text-xs">
<Select value={sessionKey} onChange={(e) => setSessionKey(e.target.value)} className="min-w-[220px] flex-1 rounded-xl px-2.5 py-1.5 text-xs">
{userSessions.map((s: any) => <option key={s.key} value={s.key}>{s.title || s.key}</option>)}
</select>
</Select>
)}
<FixedButton
onClick={() => {
@@ -618,7 +621,7 @@ const Chat: React.FC = () => {
<div className="ui-text-secondary text-sm">{t('subagentDispatchHint')}</div>
</div>
<div className="space-y-3">
<select
<Select
value={dispatchAgentID}
onChange={(e) => setDispatchAgentID(e.target.value)}
className="ui-select w-full rounded-2xl px-3 py-2.5 text-sm"
@@ -628,18 +631,18 @@ const Chat: React.FC = () => {
{formatAgentName(agent.display_name || agent.agent_id, t)} · {agent.role || '-'}
</option>
))}
</select>
<textarea
</Select>
<Textarea
value={dispatchTask}
onChange={(e) => setDispatchTask(e.target.value)}
placeholder={t('subagentTaskPlaceholder')}
className="ui-textarea w-full min-h-[180px] resize-none rounded-2xl px-3 py-3 text-sm"
className="w-full min-h-[180px] resize-none rounded-2xl px-3 py-3 text-sm"
/>
<input
<Input
value={dispatchLabel}
onChange={(e) => setDispatchLabel(e.target.value)}
placeholder={t('subagentLabelPlaceholder')}
className="ui-input w-full rounded-2xl px-3 py-2.5 text-sm"
className="w-full rounded-2xl px-3 py-2.5 text-sm"
/>
<Button onClick={dispatchSubagentTask} disabled={!dispatchAgentID.trim() || !dispatchTask.trim()} variant="primary" size="md_tall" fullWidth>
{t('dispatchToSubagent')}
@@ -759,7 +762,7 @@ const Chat: React.FC = () => {
>
<Paperclip className="w-5 h-5" />
</label>
<input
<Input
value={msg}
onChange={(e) => setMsg(e.target.value)}
onKeyDown={(e) => chatTab === 'main' && e.key === 'Enter' && send()}

View File

@@ -4,7 +4,13 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import Checkbox from '../components/Checkbox';
import FieldCard from '../components/FieldCard';
import FormField from '../components/FormField';
import Input from '../components/Input';
import RecursiveConfig from '../components/RecursiveConfig';
import Select from '../components/Select';
import Textarea from '../components/Textarea';
function setPath(obj: any, path: string, value: any) {
const keys = path.split('.');
@@ -305,11 +311,11 @@ const Config: React.FC = () => {
<Button onClick={() => setBasicMode(v => !v)} size="sm">
{basicMode ? t('configBasicMode') : t('configAdvancedMode')}
</Button>
<label className="ui-text-primary flex items-center gap-2 text-sm">
<input type="checkbox" checked={hotOnly} onChange={(e) => setHotOnly(e.target.checked)} />
{t('configHotOnly')}
</label>
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder={t('configSearchPlaceholder')} className="ui-input min-w-[240px] flex-1 px-3 py-2 rounded-xl text-sm" />
<label className="ui-text-primary flex items-center gap-2 text-sm">
<Checkbox checked={hotOnly} onChange={(e) => setHotOnly(e.target.checked)} />
{t('configHotOnly')}
</label>
<Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder={t('configSearchPlaceholder')} className="min-w-[240px] flex-1 px-3 py-2 rounded-xl text-sm" />
</div>
</div>
@@ -349,8 +355,14 @@ const Config: React.FC = () => {
<div className="brand-card-subtle rounded-2xl border border-zinc-800 p-3 space-y-3">
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="text-sm font-semibold text-zinc-200">{t('configProxies')}</div>
<div className="flex items-center gap-2">
<input value={newProxyName} onChange={(e)=>setNewProxyName(e.target.value)} placeholder={t('configNewProviderName')} className="px-2 py-1 rounded-lg bg-zinc-900/70 border border-zinc-700 text-xs" />
<FormField
label={t('configNewProviderName')}
labelClassName="text-xs text-zinc-400"
className="flex-1 min-w-[180px] space-y-1"
>
<Input value={newProxyName} onChange={(e)=>setNewProxyName(e.target.value)} placeholder={t('configNewProviderName')} className="px-2 py-1 rounded-lg bg-zinc-900/70 border border-zinc-700 text-xs" />
</FormField>
<div className="flex items-end gap-2">
<FixedButton onClick={addProxy} variant="primary" label={t('add')}>
<Plus className="w-4 h-4" />
</FixedButton>
@@ -360,9 +372,15 @@ const Config: React.FC = () => {
{Object.entries(((cfg as any)?.providers?.proxies || {}) as Record<string, any>).map(([name, p]) => (
<div key={name} className="grid grid-cols-1 md:grid-cols-7 gap-2 rounded-xl border border-zinc-800 bg-zinc-900/30 p-2 text-xs">
<div className="md:col-span-1 font-mono text-zinc-300 flex items-center">{name}</div>
<input value={String(p?.api_base || '')} onChange={(e)=>updateProxyField(name, 'api_base', e.target.value)} placeholder={t('configLabels.api_base')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
<input value={String(p?.api_key || '')} onChange={(e)=>updateProxyField(name, 'api_key', e.target.value)} placeholder={t('configLabels.api_key')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
<input value={Array.isArray(p?.models) ? p.models.join(',') : ''} onChange={(e)=>updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.models')}${t('configCommaSeparatedHint')}`} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
<FormField label={t('configLabels.api_base')} labelClassName="text-[11px] text-zinc-500" className="md:col-span-2 space-y-1">
<Input value={String(p?.api_base || '')} onChange={(e)=>updateProxyField(name, 'api_base', e.target.value)} placeholder={t('configLabels.api_base')} className="px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
</FormField>
<FormField label={t('configLabels.api_key')} labelClassName="text-[11px] text-zinc-500" className="md:col-span-2 space-y-1">
<Input value={String(p?.api_key || '')} onChange={(e)=>updateProxyField(name, 'api_key', e.target.value)} placeholder={t('configLabels.api_key')} className="px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
</FormField>
<FormField label={t('configLabels.models')} labelClassName="text-[11px] text-zinc-500" className="md:col-span-1 space-y-1">
<Input value={Array.isArray(p?.models) ? p.models.join(',') : ''} onChange={(e)=>updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.models')}${t('configCommaSeparatedHint')}`} className="px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
</FormField>
<Button onClick={()=>removeProxy(name)} variant="danger" size="xs" radius="lg">{t('delete')}</Button>
</div>
))}
@@ -379,34 +397,30 @@ const Config: React.FC = () => {
<div className="text-xs text-zinc-500">{t('configNodeP2PHint')}</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('enable')}</div>
<input
type="checkbox"
<FieldCard title={t('enable')}>
<Checkbox
checked={Boolean((cfg as any)?.gateway?.nodes?.p2p?.enabled)}
onChange={(e) => updateGatewayP2PField('enabled', e.target.checked)}
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('dashboardNodeP2PTransport')}</div>
<select
</FieldCard>
<FieldCard title={t('dashboardNodeP2PTransport')}>
<Select
value={String((cfg as any)?.gateway?.nodes?.p2p?.transport || 'websocket_tunnel')}
onChange={(e) => updateGatewayP2PField('transport', e.target.value)}
className="ui-select w-full min-h-0 rounded-lg px-2 py-1 text-xs"
className="w-full min-h-0 rounded-lg px-2 py-1 text-xs"
>
<option value="websocket_tunnel">websocket_tunnel</option>
<option value="webrtc">webrtc</option>
</select>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('dashboardNodeP2PIce')}</div>
<input
</Select>
</FieldCard>
<FieldCard title={t('dashboardNodeP2PIce')}>
<Input
value={Array.isArray((cfg as any)?.gateway?.nodes?.p2p?.stun_servers) ? (cfg as any).gateway.nodes.p2p.stun_servers.join(', ') : ''}
onChange={(e) => updateGatewayP2PField('stun_servers', e.target.value.split(',').map((s) => s.trim()).filter(Boolean))}
placeholder={t('configNodeP2PStunPlaceholder')}
className="w-full px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800"
/>
</label>
</FieldCard>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-2 flex-wrap">
@@ -418,19 +432,19 @@ const Config: React.FC = () => {
{Array.isArray((cfg as any)?.gateway?.nodes?.p2p?.ice_servers) && (cfg as any).gateway.nodes.p2p.ice_servers.length > 0 ? (
((cfg as any).gateway.nodes.p2p.ice_servers as Array<any>).map((server, index) => (
<div key={`ice-${index}`} className="grid grid-cols-1 md:grid-cols-7 gap-2 rounded-xl border border-zinc-800 bg-zinc-900/30 p-2 text-xs">
<input
<Input
value={Array.isArray(server?.urls) ? server.urls.join(', ') : ''}
onChange={(e) => updateGatewayIceServer(index, 'urls', e.target.value.split(',').map((s) => s.trim()).filter(Boolean))}
placeholder={t('configNodeP2PIceUrlsPlaceholder')}
className="md:col-span-3 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800"
/>
<input
<Input
value={String(server?.username || '')}
onChange={(e) => updateGatewayIceServer(index, 'username', e.target.value)}
placeholder={t('configNodeP2PIceUsername')}
className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800"
/>
<input
<Input
value={String(server?.credential || '')}
onChange={(e) => updateGatewayIceServer(index, 'credential', e.target.value)}
placeholder={t('configNodeP2PIceCredential')}
@@ -449,90 +463,78 @@ const Config: React.FC = () => {
<div className="text-xs text-zinc-500">{t('configNodeDispatchHint')}</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchPreferLocal')}</div>
<input
type="checkbox"
<FieldCard title={t('configNodeDispatchPreferLocal')}>
<Checkbox
checked={Boolean((cfg as any)?.gateway?.nodes?.dispatch?.prefer_local)}
onChange={(e) => updateGatewayDispatchField('prefer_local', e.target.checked)}
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchPreferP2P')}</div>
<input
type="checkbox"
</FieldCard>
<FieldCard title={t('configNodeDispatchPreferP2P')}>
<Checkbox
checked={Boolean((cfg as any)?.gateway?.nodes?.dispatch?.prefer_p2p ?? true)}
onChange={(e) => updateGatewayDispatchField('prefer_p2p', e.target.checked)}
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchAllowRelay')}</div>
<input
type="checkbox"
</FieldCard>
<FieldCard title={t('configNodeDispatchAllowRelay')}>
<Checkbox
checked={Boolean((cfg as any)?.gateway?.nodes?.dispatch?.allow_relay_fallback ?? true)}
onChange={(e) => updateGatewayDispatchField('allow_relay_fallback', e.target.checked)}
/>
</label>
</FieldCard>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchActionTags')}</div>
<textarea
<FieldCard title={t('configNodeDispatchActionTags')}>
<Textarea
value={formatTagRuleText((cfg as any)?.gateway?.nodes?.dispatch?.action_tags)}
onChange={(e) => updateGatewayDispatchField('action_tags', parseTagRuleText(e.target.value))}
placeholder={t('configNodeDispatchActionTagsPlaceholder')}
className="min-h-28 w-full rounded-xl bg-zinc-950/70 border border-zinc-800 px-3 py-2"
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchAgentTags')}</div>
<textarea
</FieldCard>
<FieldCard title={t('configNodeDispatchAgentTags')}>
<Textarea
value={formatTagRuleText((cfg as any)?.gateway?.nodes?.dispatch?.agent_tags)}
onChange={(e) => updateGatewayDispatchField('agent_tags', parseTagRuleText(e.target.value))}
placeholder={t('configNodeDispatchAgentTagsPlaceholder')}
className="min-h-28 w-full rounded-xl bg-zinc-950/70 border border-zinc-800 px-3 py-2"
/>
</label>
</FieldCard>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchAllowActions')}</div>
<textarea
<FieldCard title={t('configNodeDispatchAllowActions')}>
<Textarea
value={formatTagRuleText((cfg as any)?.gateway?.nodes?.dispatch?.allow_actions)}
onChange={(e) => updateGatewayDispatchField('allow_actions', parseTagRuleText(e.target.value))}
placeholder={t('configNodeDispatchAllowActionsPlaceholder')}
className="min-h-28 w-full rounded-xl bg-zinc-950/70 border border-zinc-800 px-3 py-2"
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchDenyActions')}</div>
<textarea
</FieldCard>
<FieldCard title={t('configNodeDispatchDenyActions')}>
<Textarea
value={formatTagRuleText((cfg as any)?.gateway?.nodes?.dispatch?.deny_actions)}
onChange={(e) => updateGatewayDispatchField('deny_actions', parseTagRuleText(e.target.value))}
placeholder={t('configNodeDispatchDenyActionsPlaceholder')}
className="min-h-28 w-full rounded-xl bg-zinc-950/70 border border-zinc-800 px-3 py-2"
/>
</label>
</FieldCard>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchAllowAgents')}</div>
<textarea
<FieldCard title={t('configNodeDispatchAllowAgents')}>
<Textarea
value={formatTagRuleText((cfg as any)?.gateway?.nodes?.dispatch?.allow_agents)}
onChange={(e) => updateGatewayDispatchField('allow_agents', parseTagRuleText(e.target.value))}
placeholder={t('configNodeDispatchAllowAgentsPlaceholder')}
className="min-h-28 w-full rounded-xl bg-zinc-950/70 border border-zinc-800 px-3 py-2"
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeDispatchDenyAgents')}</div>
<textarea
</FieldCard>
<FieldCard title={t('configNodeDispatchDenyAgents')}>
<Textarea
value={formatTagRuleText((cfg as any)?.gateway?.nodes?.dispatch?.deny_agents)}
onChange={(e) => updateGatewayDispatchField('deny_agents', parseTagRuleText(e.target.value))}
placeholder={t('configNodeDispatchDenyAgentsPlaceholder')}
className="min-h-28 w-full rounded-xl bg-zinc-950/70 border border-zinc-800 px-3 py-2"
/>
</label>
</FieldCard>
</div>
</div>
<div className="border-t border-zinc-800/70 pt-3 space-y-3">
@@ -541,44 +543,38 @@ const Config: React.FC = () => {
<div className="text-xs text-zinc-500">{t('configNodeArtifactsHint')}</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('enable')}</div>
<input
type="checkbox"
<FieldCard title={t('enable')}>
<Checkbox
checked={Boolean((cfg as any)?.gateway?.nodes?.artifacts?.enabled)}
onChange={(e) => updateGatewayArtifactsField('enabled', e.target.checked)}
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeArtifactsKeepLatest')}</div>
<input
</FieldCard>
<FieldCard title={t('configNodeArtifactsKeepLatest')}>
<Input
type="number"
min={1}
value={Number((cfg as any)?.gateway?.nodes?.artifacts?.keep_latest || 500)}
onChange={(e) => updateGatewayArtifactsField('keep_latest', Math.max(1, Number.parseInt(e.target.value || '0', 10) || 1))}
className="w-full px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800"
/>
</label>
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeArtifactsPruneOnRead')}</div>
<input
type="checkbox"
</FieldCard>
<FieldCard title={t('configNodeArtifactsPruneOnRead')}>
<Checkbox
checked={Boolean((cfg as any)?.gateway?.nodes?.artifacts?.prune_on_read ?? true)}
onChange={(e) => updateGatewayArtifactsField('prune_on_read', e.target.checked)}
/>
</label>
</FieldCard>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
<label className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3 space-y-2">
<div className="text-zinc-300">{t('configNodeArtifactsRetainDays')}</div>
<input
<FieldCard title={t('configNodeArtifactsRetainDays')}>
<Input
type="number"
min={0}
value={Number((cfg as any)?.gateway?.nodes?.artifacts?.retain_days ?? 7)}
onChange={(e) => updateGatewayArtifactsField('retain_days', Math.max(0, Number.parseInt(e.target.value || '0', 10) || 0))}
className="w-full px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800"
/>
</label>
</FieldCard>
</div>
</div>
</div>
@@ -598,7 +594,7 @@ const Config: React.FC = () => {
</div>
</div>
) : (
<textarea
<Textarea
value={cfgRaw}
onChange={(e) => setCfgRaw(e.target.value)}
className="flex-1 w-full bg-zinc-950/35 p-6 font-mono text-sm text-zinc-300 focus:outline-none resize-none"

View File

@@ -5,6 +5,11 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import Checkbox from '../components/Checkbox';
import FormField from '../components/FormField';
import Input from '../components/Input';
import Select from '../components/Select';
import Textarea from '../components/Textarea';
import { CronJob } from '../types';
import { formatLocalDateTime } from '../utils/time';
@@ -265,41 +270,37 @@ const Cron: React.FC = () => {
<div className="p-6 space-y-4 max-h-[70vh] overflow-y-auto relative z-[1]">
<div className="grid grid-cols-2 gap-4">
<label className="block">
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">{t('jobName')}</span>
<input
<FormField label={t('jobName')} labelClassName="text-sm font-medium text-zinc-400">
<Input
type="text"
value={cronForm.name}
onChange={(e) => setCronForm({ ...cronForm, name: e.target.value })}
className="ui-input rounded-xl px-3 py-2 text-sm"
className="rounded-xl px-3 py-2 text-sm"
/>
</label>
<label className="block">
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">{t('cronExpression')}</span>
<input
</FormField>
<FormField label={t('cronExpression')} labelClassName="text-sm font-medium text-zinc-400">
<Input
type="text"
value={cronForm.expr}
onChange={(e) => setCronForm({ ...cronForm, expr: e.target.value })}
placeholder={t('cronExpressionPlaceholder')}
className="ui-input rounded-xl px-3 py-2 text-sm"
className="rounded-xl px-3 py-2 text-sm"
/>
</label>
</FormField>
</div>
<label className="block">
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">{t('message')}</span>
<textarea
<FormField label={t('message')} labelClassName="text-sm font-medium text-zinc-400">
<Textarea
value={cronForm.message}
onChange={(e) => setCronForm({ ...cronForm, message: e.target.value })}
rows={3}
className="ui-textarea rounded-xl px-3 py-2 text-sm resize-none"
className="rounded-xl px-3 py-2 text-sm resize-none"
/>
</label>
</FormField>
<div className="grid grid-cols-2 gap-4">
<label className="block">
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">{t('channel')}</span>
<select
<FormField label={t('channel')} labelClassName="text-sm font-medium text-zinc-400">
<Select
value={cronForm.channel}
onChange={(e) => {
const nextChannel = e.target.value;
@@ -307,53 +308,50 @@ const Cron: React.FC = () => {
const nextTo = candidates.includes(cronForm.to) ? cronForm.to : (candidates[0] || '');
setCronForm({ ...cronForm, channel: nextChannel, to: nextTo });
}}
className="ui-select rounded-xl px-3 py-2 text-sm"
className="rounded-xl px-3 py-2 text-sm"
>
{(enabledChannels.length > 0 ? enabledChannels : [cronForm.channel]).map((ch) => (
<option key={ch} value={ch}>{ch}</option>
))}
</select>
</label>
<label className="block">
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">{t('to')}</span>
</Select>
</FormField>
<FormField label={t('to')} labelClassName="text-sm font-medium text-zinc-400">
{((channelRecipients[cronForm.channel] || []).length > 0) ? (
<select
<Select
value={cronForm.to}
onChange={(e) => setCronForm({ ...cronForm, to: e.target.value })}
className="ui-select rounded-xl px-3 py-2 text-sm"
className="rounded-xl px-3 py-2 text-sm"
>
{(channelRecipients[cronForm.channel] || []).map((id) => (
<option key={id} value={id}>{id}</option>
))}
</select>
</Select>
) : (
<input
<Input
type="text"
value={cronForm.to}
onChange={(e) => setCronForm({ ...cronForm, to: e.target.value })}
placeholder={t('recipientId')}
className="ui-input rounded-xl px-3 py-2 text-sm"
className="rounded-xl px-3 py-2 text-sm"
/>
)}
</label>
</FormField>
</div>
<div className="flex items-center gap-6 pt-2">
<label className="flex items-center gap-3 cursor-pointer group">
<input
type="checkbox"
<Checkbox
checked={cronForm.deliver}
onChange={(e) => setCronForm({ ...cronForm, deliver: e.target.checked })}
className="w-4 h-4 rounded border-zinc-700 text-indigo-500 focus:ring-indigo-500 focus:ring-offset-zinc-900 bg-zinc-950"
className="w-4 h-4"
/>
<span className="text-sm font-medium text-zinc-400 group-hover:text-zinc-200 transition-colors">{t('deliver')}</span>
</label>
<label className="flex items-center gap-3 cursor-pointer group">
<input
type="checkbox"
<Checkbox
checked={cronForm.enabled}
onChange={(e) => setCronForm({ ...cronForm, enabled: e.target.checked })}
className="w-4 h-4 rounded border-zinc-700 text-indigo-500 focus:ring-indigo-500 focus:ring-offset-zinc-900 bg-zinc-950"
className="w-4 h-4"
/>
<span className="text-sm font-medium text-zinc-400 group-hover:text-zinc-200 transition-colors">{t('active')}</span>
</label>

View File

@@ -3,6 +3,7 @@ import { AlertTriangle, RefreshCw, Route, ServerCrash, Workflow } from 'lucide-r
import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { FixedButton } from '../components/Button';
import Select from '../components/Select';
type EKGKV = { key?: string; score?: number; count?: number };
@@ -160,11 +161,11 @@ const EKG: React.FC = () => {
<div className="ui-text-muted mt-1 text-sm">{t('ekgOverviewHint')}</div>
</div>
<div className="flex items-center gap-2">
<select value={ekgWindow} onChange={(e) => setEkgWindow(e.target.value as '6h' | '24h' | '7d')} className="ui-select h-12 min-w-[96px] rounded-xl px-3 text-sm">
<Select value={ekgWindow} onChange={(e) => setEkgWindow(e.target.value as '6h' | '24h' | '7d')} className="h-12 min-w-[96px] rounded-xl px-3 text-sm">
<option value="6h">6h</option>
<option value="24h">24h</option>
<option value="7d">7d</option>
</select>
</Select>
<FixedButton
onClick={fetchData}
variant="primary"

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import Input from '../components/Input';
type CodeItem = {
code: number;
@@ -43,11 +44,11 @@ const LogCodes: React.FC = () => {
<div className="p-4 md:p-6 xl:p-8 w-full space-y-6">
<div className="flex items-center justify-between gap-3 flex-wrap">
<h1 className="ui-text-secondary text-2xl font-semibold tracking-tight">{t('logCodes')}</h1>
<input
<Input
value={kw}
onChange={(e) => setKw(e.target.value)}
placeholder={t('logCodesSearchPlaceholder')}
className="ui-input w-full sm:w-80 rounded-xl px-3 py-2 text-sm"
className="w-full sm:w-80 rounded-xl px-3 py-2 text-sm"
/>
</div>

View File

@@ -5,6 +5,10 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import Checkbox from '../components/Checkbox';
import FormField from '../components/FormField';
import Input from '../components/Input';
import Select from '../components/Select';
type MCPDraftServer = {
enabled: boolean;
@@ -474,46 +478,40 @@ const MCP: React.FC = () => {
<div className="max-h-[80vh] overflow-y-auto px-6 py-5 space-y-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-2">
<div className="text-xs text-zinc-400">{t('configNewMCPServerName')}</div>
<input value={draftName} onChange={(e) => setDraftName(e.target.value)} className="ui-input h-11 px-3" />
</label>
<label className="space-y-2">
<div className="text-xs text-zinc-400">{t('configLabels.description')}</div>
<input value={draft.description} onChange={(e) => updateDraftField('description', e.target.value)} className="ui-input h-11 px-3" />
</label>
<FormField label={t('configNewMCPServerName')} labelClassName="text-xs text-zinc-400" className="space-y-2">
<Input value={draftName} onChange={(e) => setDraftName(e.target.value)} className="h-11 px-3" />
</FormField>
<FormField label={t('configLabels.description')} labelClassName="text-xs text-zinc-400" className="space-y-2">
<Input value={draft.description} onChange={(e) => updateDraftField('description', e.target.value)} className="h-11 px-3" />
</FormField>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<label className="space-y-2">
<div className="text-xs text-zinc-400">transport</div>
<select value={draft.transport} onChange={(e) => updateDraftField('transport', e.target.value)} className="ui-select h-11 px-3">
<FormField label="transport" labelClassName="text-xs text-zinc-400" className="space-y-2">
<Select value={draft.transport} onChange={(e) => updateDraftField('transport', e.target.value)} className="h-11 px-3">
<option value="stdio">stdio</option>
<option value="http">http</option>
<option value="streamable_http">streamable_http</option>
<option value="sse">sse</option>
</select>
</label>
<label className="space-y-2">
<div className="text-xs text-zinc-400">enabled</div>
</Select>
</FormField>
<FormField label="enabled" labelClassName="text-xs text-zinc-400" className="space-y-2">
<div className="ui-toggle-card flex h-11 items-center rounded-xl px-3">
<input type="checkbox" checked={draft.enabled} onChange={(e) => updateDraftField('enabled', e.target.checked)} />
<Checkbox checked={draft.enabled} onChange={(e) => updateDraftField('enabled', e.target.checked)} />
</div>
</label>
</FormField>
{draft.transport === 'stdio' && (
<label className="space-y-2">
<div className="text-xs text-zinc-400">permission</div>
<select value={draft.permission} onChange={(e) => updateDraftField('permission', e.target.value)} className="ui-select h-11 px-3">
<FormField label="permission" labelClassName="text-xs text-zinc-400" className="space-y-2">
<Select value={draft.permission} onChange={(e) => updateDraftField('permission', e.target.value)} className="h-11 px-3">
<option value="workspace">workspace</option>
<option value="full">full</option>
</select>
</label>
</Select>
</FormField>
)}
{draft.transport === 'stdio' && (
<label className="space-y-2">
<div className="text-xs text-zinc-400">{t('configLabels.package')}</div>
<input value={draft.package} onChange={(e) => updateDraftField('package', e.target.value)} className="ui-input h-11 px-3" />
</label>
<FormField label={t('configLabels.package')} labelClassName="text-xs text-zinc-400" className="space-y-2">
<Input value={draft.package} onChange={(e) => updateDraftField('package', e.target.value)} className="h-11 px-3" />
</FormField>
)}
</div>
@@ -521,17 +519,17 @@ const MCP: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="space-y-2">
<div className="text-xs text-zinc-400">{t('configLabels.command')}</div>
<input value={draft.command} onChange={(e) => updateDraftField('command', e.target.value)} className="ui-input h-11 px-3" />
<Input value={draft.command} onChange={(e) => updateDraftField('command', e.target.value)} className="h-11 px-3" />
</label>
<label className="space-y-2">
<div className="text-xs text-zinc-400">{t('configLabels.working_dir')}</div>
<input value={draft.working_dir} onChange={(e) => updateDraftField('working_dir', e.target.value)} className="ui-input h-11 px-3" />
<Input value={draft.working_dir} onChange={(e) => updateDraftField('working_dir', e.target.value)} className="h-11 px-3" />
</label>
</div>
) : (
<label className="space-y-2">
<div className="text-xs text-zinc-400">{t('configLabels.url')}</div>
<input value={draft.url} onChange={(e) => updateDraftField('url', e.target.value)} className="ui-input h-11 px-3" />
<Input value={draft.url} onChange={(e) => updateDraftField('url', e.target.value)} className="h-11 px-3" />
</label>
)}
@@ -555,7 +553,7 @@ const MCP: React.FC = () => {
</span>
))}
</div>
<input
<Input
value={draftArgInput}
onChange={(e) => setDraftArgInput(e.target.value)}
onKeyDown={(e) => {
@@ -566,7 +564,7 @@ const MCP: React.FC = () => {
}}
onBlur={() => addDraftArg(draftArgInput)}
placeholder={t('configMCPArgsEnterHint')}
className="ui-input h-11 px-3"
className="h-11 px-3"
/>
</div>
)}

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import Textarea from '../components/Textarea';
const Memory: React.FC = () => {
const { t } = useTranslation();
@@ -146,7 +147,7 @@ const Memory: React.FC = () => {
<h2 className="ui-text-primary font-semibold">{active || t('noFileSelected')}</h2>
<Button onClick={saveFile} variant="primary" size="sm" radius="xl">{t('save')}</Button>
</div>
<textarea value={content} onChange={(e) => setContent(e.target.value)} className="ui-textarea w-full h-[50vh] lg:h-[80vh] rounded-[24px] p-4" />
<Textarea value={content} onChange={(e) => setContent(e.target.value)} className="w-full h-[50vh] lg:h-[80vh] rounded-[24px] p-4" />
</div>
</main>
</div>

View File

@@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import { useAppContext } from '../context/AppContext';
import { Button, FixedButton, LinkButton } from '../components/Button';
import Input from '../components/Input';
import Select from '../components/Select';
import { formatLocalDateTime } from '../utils/time';
function dataUrlForArtifact(artifact: any) {
@@ -161,26 +163,26 @@ const NodeArtifacts: React.FC = () => {
<div>{t('nodeArtifactsRetentionRemaining')}: {Number(retentionSummary?.remaining || filteredItems.length || 0)}</div>
</div>
<div className="grid grid-cols-1 gap-2">
<select value={nodeFilter} onChange={(e) => setNodeFilter(e.target.value)} className="ui-select rounded-xl px-2 py-2 text-xs">
<Select value={nodeFilter} onChange={(e) => setNodeFilter(e.target.value)} className="rounded-xl px-2 py-2 text-xs">
<option value="all">{t('allNodes')}</option>
{nodes.map((node) => <option key={node} value={node}>{node}</option>)}
</select>
<select value={actionFilter} onChange={(e) => setActionFilter(e.target.value)} className="ui-select rounded-xl px-2 py-2 text-xs">
</Select>
<Select value={actionFilter} onChange={(e) => setActionFilter(e.target.value)} className="rounded-xl px-2 py-2 text-xs">
<option value="all">{t('allActions')}</option>
{actions.map((action) => <option key={action} value={action}>{action}</option>)}
</select>
<select value={kindFilter} onChange={(e) => setKindFilter(e.target.value)} className="ui-select rounded-xl px-2 py-2 text-xs">
</Select>
<Select value={kindFilter} onChange={(e) => setKindFilter(e.target.value)} className="rounded-xl px-2 py-2 text-xs">
<option value="all">{t('allKinds')}</option>
{kinds.map((kind) => <option key={kind} value={kind}>{kind}</option>)}
</select>
</Select>
</div>
<div className="grid grid-cols-[1fr_auto] gap-2">
<input
<Input
value={keepLatest}
onChange={(e) => setKeepLatest(e.target.value)}
inputMode="numeric"
placeholder={t('nodeArtifactsKeepLatest')}
className="ui-input rounded-xl px-3 py-2 text-xs"
className="rounded-xl px-3 py-2 text-xs"
/>
<Button onClick={pruneArtifacts} disabled={prunePending} variant="warning" size="xs_tall">
{prunePending ? t('loading') : t('nodeArtifactsPrune')}

View File

@@ -5,6 +5,9 @@ import { Check, RefreshCw } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { formatLocalDateTime } from '../utils/time';
import { Button, FixedButton, LinkButton } from '../components/Button';
import Input from '../components/Input';
import Select from '../components/Select';
import Textarea from '../components/Textarea';
function dataUrlForArtifact(artifact: any) {
const mime = String(artifact?.mime_type || '').trim() || 'application/octet-stream';
@@ -234,11 +237,11 @@ const Nodes: React.FC = () => {
<div className="grid grid-cols-1 xl:grid-cols-[300px_1fr_1.1fr] gap-4 flex-1 min-h-0">
<div className="brand-card ui-panel rounded-[28px] overflow-hidden flex flex-col min-h-0">
<div className="px-3 py-2 border-b border-zinc-800 dark:border-zinc-700 space-y-2">
<input
<Input
value={nodeFilter}
onChange={(e) => setNodeFilter(e.target.value)}
placeholder={t('nodesFilterPlaceholder')}
className="ui-input rounded-xl px-3 py-2 text-sm"
className="rounded-xl px-3 py-2 text-sm"
/>
</div>
<div className="overflow-y-auto min-h-0">
@@ -398,19 +401,19 @@ const Nodes: React.FC = () => {
<div className="px-3 py-2 border-b border-zinc-800 dark:border-zinc-700 space-y-2">
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('nodeDispatchDetail')}</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<select value={dispatchActionFilter} onChange={(e) => setDispatchActionFilter(e.target.value)} className="ui-select rounded-xl px-2 py-2 text-xs">
<Select value={dispatchActionFilter} onChange={(e) => setDispatchActionFilter(e.target.value)} className="rounded-xl px-2 py-2 text-xs">
<option value="all">{t('allActions')}</option>
{dispatchActions.map((action) => <option key={action} value={action}>{action}</option>)}
</select>
<select value={dispatchTransportFilter} onChange={(e) => setDispatchTransportFilter(e.target.value)} className="ui-select rounded-xl px-2 py-2 text-xs">
</Select>
<Select value={dispatchTransportFilter} onChange={(e) => setDispatchTransportFilter(e.target.value)} className="rounded-xl px-2 py-2 text-xs">
<option value="all">{t('allTransports')}</option>
{dispatchTransports.map((transport) => <option key={transport} value={transport}>{transport}</option>)}
</select>
<select value={dispatchStatusFilter} onChange={(e) => setDispatchStatusFilter(e.target.value)} className="ui-select rounded-xl px-2 py-2 text-xs">
</Select>
<Select value={dispatchStatusFilter} onChange={(e) => setDispatchStatusFilter(e.target.value)} className="rounded-xl px-2 py-2 text-xs">
<option value="all">{t('allStatus')}</option>
<option value="ok">ok</option>
<option value="error">error</option>
</select>
</Select>
</div>
</div>
<div className="grid grid-rows-[220px_1fr] min-h-0 flex-1">
@@ -476,24 +479,24 @@ const Nodes: React.FC = () => {
<div className="grid grid-cols-3 gap-2">
<label className="space-y-1">
<div className="text-zinc-500 text-[11px]">{t('mode')}</div>
<select value={replayModeDraft} onChange={(e) => setReplayModeDraft(e.target.value)} className="ui-select w-full rounded-xl px-2 py-2 text-xs">
<Select value={replayModeDraft} onChange={(e) => setReplayModeDraft(e.target.value)} className="w-full rounded-xl px-2 py-2 text-xs">
<option value="auto">auto</option>
<option value="p2p">p2p</option>
<option value="relay">relay</option>
</select>
</Select>
</label>
<label className="space-y-1 col-span-2">
<div className="text-zinc-500 text-[11px]">{t('model')}</div>
<input value={replayModelDraft} onChange={(e) => setReplayModelDraft(e.target.value)} className="ui-input w-full rounded-xl px-3 py-2 text-xs" />
<Input value={replayModelDraft} onChange={(e) => setReplayModelDraft(e.target.value)} className="w-full rounded-xl px-3 py-2 text-xs" />
</label>
</div>
<label className="space-y-1 block">
<div className="text-zinc-500 text-[11px]">{t('task')}</div>
<textarea value={replayTaskDraft} onChange={(e) => setReplayTaskDraft(e.target.value)} className="ui-textarea min-h-24 w-full p-3 text-xs" />
<Textarea value={replayTaskDraft} onChange={(e) => setReplayTaskDraft(e.target.value)} className="min-h-24 w-full p-3 text-xs" />
</label>
<label className="space-y-1 block">
<div className="text-zinc-500 text-[11px]">{t('args')}</div>
<textarea value={replayArgsDraft} onChange={(e) => setReplayArgsDraft(e.target.value)} className="ui-textarea min-h-40 w-full p-3 text-xs font-mono" />
<Textarea value={replayArgsDraft} onChange={(e) => setReplayArgsDraft(e.target.value)} className="min-h-40 w-full p-3 text-xs font-mono" />
</label>
</div>
</div>

View File

@@ -5,6 +5,9 @@ import { useTranslation } from 'react-i18next';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import Checkbox from '../components/Checkbox';
import Input from '../components/Input';
import Textarea from '../components/Textarea';
const Skills: React.FC = () => {
const { t } = useTranslation();
@@ -190,11 +193,10 @@ const Skills: React.FC = () => {
<div className="flex items-start justify-between gap-3 flex-wrap">
<h1 className="text-2xl font-semibold tracking-tight">{t('skills')}</h1>
<div className="flex items-center gap-2 flex-wrap w-full xl:w-auto">
<input disabled={installingSkill} value={installName} onChange={(e) => setInstallName(e.target.value)} placeholder={t('skillsNamePlaceholder')} className="w-full sm:w-72 px-3 py-2 bg-zinc-950/70 border border-zinc-800 rounded-xl text-sm disabled:opacity-60" />
<Input disabled={installingSkill} value={installName} onChange={(e) => setInstallName(e.target.value)} placeholder={t('skillsNamePlaceholder')} className="w-full sm:w-72 px-3 py-2 bg-zinc-950/70 border border-zinc-800 rounded-xl text-sm disabled:opacity-60" />
<Button disabled={installingSkill} onClick={installSkill} variant="success">{installingSkill ? t('loading') : t('install')}</Button>
<label className="flex items-center gap-2 text-xs text-zinc-400">
<input
type="checkbox"
<Checkbox
checked={ignoreSuspicious}
disabled={installingSkill}
onChange={(e) => setIgnoreSuspicious(e.target.checked)}
@@ -298,7 +300,7 @@ const Skills: React.FC = () => {
</FixedButton>
</div>
</div>
<textarea value={fileContent} onChange={(e)=>setFileContent(e.target.value)} className="flex-1 bg-zinc-950 text-zinc-200 font-mono text-sm p-4 resize-none outline-none" />
<Textarea value={fileContent} onChange={(e)=>setFileContent(e.target.value)} className="flex-1 bg-zinc-950 text-zinc-200 font-mono text-sm p-4 resize-none outline-none" />
</main>
</motion.div>
</div>

View File

@@ -4,6 +4,10 @@ import { Check, Plus, RefreshCw } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
import { Button, FixedButton } from '../components/Button';
import FormField from '../components/FormField';
import Input from '../components/Input';
import Select from '../components/Select';
import Textarea from '../components/Textarea';
type SubagentProfile = {
agent_id: string;
@@ -317,83 +321,75 @@ const SubagentProfiles: React.FC = () => {
<div className="brand-card ui-border-subtle rounded-[28px] border p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<div className="ui-text-subtle text-xs mb-1">{t('id')}</div>
<input
<FormField label={t('id')} labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Input
value={draft.agent_id || ''}
disabled={!!selected}
onChange={(e) => setDraft({ ...draft, agent_id: e.target.value })}
className="ui-input w-full px-2 py-1.5 text-xs rounded-xl disabled:opacity-60"
className="w-full px-2 py-1.5 text-xs rounded-xl disabled:opacity-60"
placeholder="coder"
/>
</div>
<div>
<div className="ui-text-subtle text-xs mb-1">{t('name')}</div>
<input
</FormField>
<FormField label={t('name')} labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Input
value={draft.name || ''}
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
placeholder="Code Agent"
/>
</div>
<div>
<div className="ui-text-subtle text-xs mb-1">Role</div>
<input
</FormField>
<FormField label="Role" labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Input
value={draft.role || ''}
onChange={(e) => setDraft({ ...draft, role: e.target.value })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
placeholder="coding"
/>
</div>
<div>
<div className="ui-text-subtle text-xs mb-1">{t('status')}</div>
<select
</FormField>
<FormField label={t('status')} labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Select
value={draft.status || 'active'}
onChange={(e) => setDraft({ ...draft, status: e.target.value })}
className="ui-select w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
>
<option value="active">active</option>
<option value="disabled">disabled</option>
</select>
</div>
<div>
<div className="ui-text-subtle text-xs mb-1">notify_main_policy</div>
<select
</Select>
</FormField>
<FormField label="notify_main_policy" labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Select
value={draft.notify_main_policy || 'final_only'}
onChange={(e) => setDraft({ ...draft, notify_main_policy: e.target.value })}
className="ui-select w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
>
<option value="final_only">final_only</option>
<option value="internal_only">internal_only</option>
<option value="milestone">milestone</option>
<option value="on_blocked">on_blocked</option>
<option value="always">always</option>
</select>
</div>
<div className="md:col-span-2">
<div className="ui-text-subtle text-xs mb-1">system_prompt_file</div>
<input
</Select>
</FormField>
<FormField label="system_prompt_file" labelClassName="ui-text-subtle text-xs" className="space-y-1 md:col-span-2">
<Input
value={draft.system_prompt_file || ''}
onChange={(e) => setDraft({ ...draft, system_prompt_file: e.target.value })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
placeholder="agents/coder/AGENT.md"
/>
</div>
<div className="md:col-span-2">
<div className="ui-text-subtle text-xs mb-1">{t('memoryNamespace')}</div>
<input
</FormField>
<FormField label={t('memoryNamespace')} labelClassName="ui-text-subtle text-xs" className="space-y-1 md:col-span-2">
<Input
value={draft.memory_namespace || ''}
onChange={(e) => setDraft({ ...draft, memory_namespace: e.target.value })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
placeholder="coder"
/>
</div>
<div className="md:col-span-2">
<div className="ui-text-subtle text-xs mb-1">{t('toolAllowlist')}</div>
<input
</FormField>
<FormField label={t('toolAllowlist')} labelClassName="ui-text-subtle text-xs" className="space-y-1 md:col-span-2">
<Input
value={allowlistText}
onChange={(e) => setDraft({ ...draft, tool_allowlist: parseAllowlist(e.target.value) })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
placeholder="read_file, list_files, memory_search"
/>
<div className="ui-text-muted mt-1 text-[11px]">
@@ -408,16 +404,16 @@ const SubagentProfiles: React.FC = () => {
))}
</div>
)}
</div>
</FormField>
<div className="md:col-span-2">
<div className="flex items-center justify-between mb-1 gap-3">
<div className="ui-text-subtle text-xs">system_prompt_file content</div>
<div className="ui-text-muted text-[11px]">{promptFileFound ? t('promptFileReady') : t('promptFileMissing')}</div>
</div>
<textarea
<Textarea
value={promptFileContent}
onChange={(e) => setPromptFileContent(e.target.value)}
className="ui-textarea w-full px-2 py-1 text-xs rounded min-h-[220px]"
className="w-full px-2 py-1 text-xs rounded min-h-[220px]"
placeholder={t('agentPromptContentPlaceholder')}
/>
<div className="mt-2 flex items-center gap-2">
@@ -426,46 +422,42 @@ const SubagentProfiles: React.FC = () => {
</Button>
</div>
</div>
<div>
<div className="ui-text-subtle text-xs mb-1">{t('maxRetries')}</div>
<input
<FormField label={t('maxRetries')} labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Input
type="number"
min={0}
value={Number(draft.max_retries || 0)}
onChange={(e) => setDraft({ ...draft, max_retries: Number(e.target.value) || 0 })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
/>
</div>
<div>
<div className="ui-text-subtle text-xs mb-1">{t('retryBackoffMs')}</div>
<input
</FormField>
<FormField label={t('retryBackoffMs')} labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Input
type="number"
min={0}
value={Number(draft.retry_backoff_ms || 0)}
onChange={(e) => setDraft({ ...draft, retry_backoff_ms: Number(e.target.value) || 0 })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
/>
</div>
<div>
<div className="ui-text-subtle text-xs mb-1">Max Task Chars</div>
<input
</FormField>
<FormField label="Max Task Chars" labelClassName="ui-text-subtle text-xs" className="space-y-1">
<Input
type="number"
min={0}
value={Number(draft.max_task_chars || 0)}
onChange={(e) => setDraft({ ...draft, max_task_chars: Number(e.target.value) || 0 })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
/>
</div>
<div className="md:col-span-2">
<div className="ui-text-subtle text-xs mb-1">Max Result Chars</div>
<input
</FormField>
<FormField label="Max Result Chars" labelClassName="ui-text-subtle text-xs" className="space-y-1 md:col-span-2">
<Input
type="number"
min={0}
value={Number(draft.max_result_chars || 0)}
onChange={(e) => setDraft({ ...draft, max_result_chars: Number(e.target.value) || 0 })}
className="ui-input w-full px-2 py-1 text-xs rounded"
className="w-full px-2 py-1 text-xs rounded"
/>
</div>
</FormField>
</div>
<div className="flex items-center gap-2">

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Check, RefreshCw } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { FixedButton } from '../components/Button';
import Select from '../components/Select';
import { formatLocalDateTime } from '../utils/time';
type TaskAuditItem = {
@@ -113,14 +114,14 @@ const TaskAudit: React.FC = () => {
<div className="flex items-center justify-between flex-wrap gap-3">
<h1 className="text-xl md:text-2xl font-semibold">{t('taskAudit')}</h1>
<div className="flex items-center gap-2">
<select value={sourceFilter} onChange={(e)=>setSourceFilter(e.target.value)} className="ui-select rounded-xl px-2 py-1.5 text-xs">
<Select value={sourceFilter} onChange={(e)=>setSourceFilter(e.target.value)} className="rounded-xl px-2 py-1.5 text-xs">
<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>
</select>
<select value={statusFilter} onChange={(e)=>setStatusFilter(e.target.value)} className="ui-select rounded-xl px-2 py-1.5 text-xs">
</Select>
<Select value={statusFilter} onChange={(e)=>setStatusFilter(e.target.value)} className="rounded-xl px-2 py-1.5 text-xs">
<option value="all">{t('allStatus')}</option>
<option value="running">{t('statusRunning')}</option>
<option value="waiting">{t('statusWaiting')}</option>
@@ -128,7 +129,7 @@ const TaskAudit: React.FC = () => {
<option value="success">{t('statusSuccess')}</option>
<option value="error">{t('statusError')}</option>
<option value="suppressed">{t('statusSuppressed')}</option>
</select>
</Select>
<FixedButton onClick={fetchData} variant="primary" label={loading ? t('loading') : t('refresh')}>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</FixedButton>