mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-12 23:27:30 +08:00
refresh webui forms and fix whatsapp bridge login state
This commit is contained in:
@@ -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
32
cmd/cmd_gateway_test.go
Normal 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
79
scripts/build-slim.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
24
webui/src/components/Checkbox.tsx
Normal file
24
webui/src/components/Checkbox.tsx
Normal 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 };
|
||||
38
webui/src/components/FieldCard.tsx
Normal file
38
webui/src/components/FieldCard.tsx
Normal 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 };
|
||||
38
webui/src/components/FormField.tsx
Normal file
38
webui/src/components/FormField.tsx
Normal 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 };
|
||||
@@ -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)}
|
||||
|
||||
23
webui/src/components/Input.tsx
Normal file
23
webui/src/components/Input.tsx
Normal 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 };
|
||||
@@ -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>
|
||||
|
||||
25
webui/src/components/Select.tsx
Normal file
25
webui/src/components/Select.tsx
Normal 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 };
|
||||
@@ -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)}
|
||||
|
||||
23
webui/src/components/Textarea.tsx
Normal file
23
webui/src/components/Textarea.tsx
Normal 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 };
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user