mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 17:07:29 +08:00
fix webui i18n
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type DialogOptions = {
|
||||
title?: string;
|
||||
@@ -16,6 +17,7 @@ export const GlobalDialog: React.FC<{
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ open, kind, options, onConfirm, onCancel }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
@@ -24,15 +26,15 @@ export const GlobalDialog: React.FC<{
|
||||
<motion.div className="w-full max-w-md rounded-2xl border border-zinc-700 bg-zinc-900 shadow-2xl"
|
||||
initial={{ scale: 0.95, y: 8 }} animate={{ scale: 1, y: 0 }} exit={{ scale: 0.95, y: 8 }}>
|
||||
<div className="px-5 py-4 border-b border-zinc-800">
|
||||
<h3 className="text-sm font-semibold text-zinc-100">{options.title || (kind === 'confirm' ? 'Please confirm' : 'Notice')}</h3>
|
||||
<h3 className="text-sm font-semibold text-zinc-100">{options.title || (kind === 'confirm' ? t('dialogPleaseConfirm') : t('dialogNotice'))}</h3>
|
||||
</div>
|
||||
<div className="px-5 py-4 text-sm text-zinc-300 whitespace-pre-wrap">{options.message}</div>
|
||||
<div className="px-5 pb-5 flex items-center justify-end gap-2">
|
||||
{kind === 'confirm' && (
|
||||
<button onClick={onCancel} className="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-sm">{options.cancelText || 'Cancel'}</button>
|
||||
<button onClick={onCancel} className="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-200 text-sm">{options.cancelText || t('cancel')}</button>
|
||||
)}
|
||||
<button onClick={onConfirm} className={`px-3 py-1.5 rounded-lg text-sm ${options.danger ? 'bg-red-600 hover:bg-red-500 text-white' : 'bg-indigo-600 hover:bg-indigo-500 text-white'}`}>
|
||||
{options.confirmText || 'OK'}
|
||||
{options.confirmText || t('dialogOk')}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -21,7 +21,7 @@ const Header: React.FC = () => {
|
||||
<div className="w-8 h-8 md:w-9 md:h-9 rounded-xl bg-indigo-500 flex items-center justify-center shadow-lg shadow-indigo-500/20 shrink-0">
|
||||
<Terminal className="w-4 h-4 md:w-5 md:h-5 text-white" />
|
||||
</div>
|
||||
<span className="hidden md:inline font-semibold text-lg md:text-xl tracking-tight truncate">ClawGo</span>
|
||||
<span className="hidden md:inline font-semibold text-lg md:text-xl tracking-tight truncate">{t('appName')}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 md:gap-6">
|
||||
|
||||
@@ -25,6 +25,7 @@ const PrimitiveArrayEditor: React.FC<{
|
||||
path: string;
|
||||
onChange: (next: any[]) => void;
|
||||
}> = ({ value, path, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [draft, setDraft] = useState('');
|
||||
const [selected, setSelected] = useState('');
|
||||
|
||||
@@ -54,7 +55,7 @@ const PrimitiveArrayEditor: React.FC<{
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{value.length === 0 && <span className="text-xs text-zinc-500 italic">(empty)</span>}
|
||||
{value.length === 0 && <span className="text-xs text-zinc-500 italic">{t('empty')}</span>}
|
||||
{value.map((item, idx) => (
|
||||
<span key={`${item}-${idx}`} className="inline-flex items-center gap-1 px-2 py-1 rounded bg-zinc-900 border border-zinc-700 text-xs font-mono text-zinc-200">
|
||||
{String(item)}
|
||||
@@ -68,7 +69,7 @@ const PrimitiveArrayEditor: React.FC<{
|
||||
list={`${path}-suggestions`}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
placeholder="输入新值后添加"
|
||||
placeholder={t('recursiveAddValuePlaceholder')}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
<datalist id={`${path}-suggestions`}>
|
||||
@@ -84,7 +85,7 @@ const PrimitiveArrayEditor: React.FC<{
|
||||
}}
|
||||
className="px-3 py-2 text-xs rounded-lg bg-zinc-800 hover:bg-zinc-700"
|
||||
>
|
||||
添加
|
||||
{t('add')}
|
||||
</button>
|
||||
|
||||
<select
|
||||
@@ -96,7 +97,7 @@ const PrimitiveArrayEditor: React.FC<{
|
||||
}}
|
||||
className="px-3 py-2 text-xs rounded-lg bg-zinc-950 border border-zinc-800"
|
||||
>
|
||||
<option value="">下拉选择</option>
|
||||
<option value="">{t('recursiveSelectOption')}</option>
|
||||
{suggestions.filter((s) => !value.includes(s)).map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
|
||||
@@ -18,6 +18,8 @@ interface AppContextType {
|
||||
setCron: (cron: CronJob[]) => void;
|
||||
skills: Skill[];
|
||||
setSkills: (skills: Skill[]) => void;
|
||||
clawhubInstalled: boolean;
|
||||
clawhubPath: string;
|
||||
sessions: Session[];
|
||||
setSessions: React.Dispatch<React.SetStateAction<Session[]>>;
|
||||
refreshAll: () => Promise<void>;
|
||||
@@ -53,6 +55,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [nodes, setNodes] = useState('[]');
|
||||
const [cron, setCron] = useState<CronJob[]>([]);
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [clawhubInstalled, setClawhubInstalled] = useState(false);
|
||||
const [clawhubPath, setClawhubPath] = useState('');
|
||||
const [sessions, setSessions] = useState<Session[]>([{ key: 'main', title: 'main' }]);
|
||||
const [gatewayVersion, setGatewayVersion] = useState('unknown');
|
||||
const [webuiVersion, setWebuiVersion] = useState('unknown');
|
||||
@@ -121,6 +125,8 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (!r.ok) throw new Error('Failed to load skills');
|
||||
const j = await r.json();
|
||||
setSkills(Array.isArray(j.skills) ? j.skills : []);
|
||||
setClawhubInstalled(!!j.clawhub_installed);
|
||||
setClawhubPath(typeof j.clawhub_path === 'string' ? j.clawhub_path : '');
|
||||
setIsGatewayOnline(true);
|
||||
} catch (e) {
|
||||
setIsGatewayOnline(false);
|
||||
@@ -174,7 +180,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
<AppContext.Provider value={{
|
||||
token, setToken, sidebarOpen, setSidebarOpen, isGatewayOnline, setIsGatewayOnline,
|
||||
cfg, setCfg, cfgRaw, setCfgRaw, nodes, setNodes,
|
||||
cron, setCron, skills, setSkills,
|
||||
cron, setCron, skills, setSkills, clawhubInstalled, clawhubPath,
|
||||
sessions, setSessions,
|
||||
refreshAll, refreshCron, refreshNodes, refreshSkills, refreshSessions, refreshVersion, loadConfig,
|
||||
gatewayVersion, webuiVersion, hotReloadFields, hotReloadFieldDetails, q
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GlobalDialog, DialogOptions } from '../components/GlobalDialog';
|
||||
|
||||
type UIContextType = {
|
||||
@@ -15,15 +16,16 @@ type UIContextType = {
|
||||
const UIContext = createContext<UIContextType | undefined>(undefined);
|
||||
|
||||
export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingText, setLoadingText] = useState('Loading...');
|
||||
const [loadingText, setLoadingText] = useState(t('loading'));
|
||||
const [dialog, setDialog] = useState<null | { kind: 'notice' | 'confirm'; options: DialogOptions; resolve: (v: any) => void }>(null);
|
||||
const [customModal, setCustomModal] = useState<null | { title?: string; node: React.ReactNode }>(null);
|
||||
|
||||
const value = useMemo<UIContextType>(() => ({
|
||||
loading,
|
||||
showLoading: (text?: string) => {
|
||||
setLoadingText(text || 'Loading...');
|
||||
setLoadingText(text || t('loading'));
|
||||
setLoading(true);
|
||||
},
|
||||
hideLoading: () => setLoading(false),
|
||||
@@ -37,7 +39,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
}),
|
||||
openModal: (node, title) => setCustomModal({ node, title }),
|
||||
closeModal: () => setCustomModal(null),
|
||||
}), [loading]);
|
||||
}), [loading, t]);
|
||||
|
||||
const closeDialog = (result: boolean) => {
|
||||
if (!dialog) return;
|
||||
@@ -76,7 +78,7 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
<motion.div className="w-full max-w-4xl rounded-2xl border border-zinc-700 bg-zinc-900 shadow-2xl overflow-hidden"
|
||||
initial={{ scale: 0.96 }} animate={{ scale: 1 }} exit={{ scale: 0.96 }}>
|
||||
<div className="px-5 py-3 border-b border-zinc-800 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-zinc-100">{customModal.title || 'Modal'}</h3>
|
||||
<h3 className="text-sm font-semibold text-zinc-100">{customModal.title || t('modal')}</h3>
|
||||
<button onClick={() => setCustomModal(null)} className="text-zinc-400 hover:text-zinc-200">✕</button>
|
||||
</div>
|
||||
<div className="p-4 max-h-[80vh] overflow-auto">{customModal.node}</div>
|
||||
|
||||
@@ -107,6 +107,118 @@ const resources = {
|
||||
save: 'Save',
|
||||
create: 'Create',
|
||||
update: 'Update',
|
||||
delete: 'Delete',
|
||||
add: 'Add',
|
||||
install: 'Install',
|
||||
installed: 'Installed',
|
||||
notInstalled: 'Not Installed',
|
||||
status: 'Status',
|
||||
source: 'Source',
|
||||
duration: 'Duration',
|
||||
provider: 'Provider',
|
||||
model: 'Model',
|
||||
time: 'Time',
|
||||
level: 'Level',
|
||||
code: 'Code',
|
||||
template: 'Template',
|
||||
content: 'Content',
|
||||
id: 'ID',
|
||||
files: 'Files',
|
||||
close: 'Close',
|
||||
path: 'Path',
|
||||
before: 'Before',
|
||||
after: 'After',
|
||||
hide: 'Hide',
|
||||
show: 'Show',
|
||||
clear: 'Clear',
|
||||
pause: 'Pause',
|
||||
resume: 'Resume',
|
||||
live: 'Live',
|
||||
raw: 'Raw',
|
||||
pretty: 'Pretty',
|
||||
entries: 'entries',
|
||||
appName: 'ClawGo',
|
||||
webui: 'WebUI',
|
||||
node: 'Node',
|
||||
unknownIp: 'Unknown IP',
|
||||
memoryFiles: 'Memory Files',
|
||||
memoryFileNamePrompt: 'Memory file name',
|
||||
noFileSelected: 'No file selected',
|
||||
noDescription: 'No description provided.',
|
||||
empty: '(empty)',
|
||||
modal: 'Modal',
|
||||
dialogPleaseConfirm: 'Please confirm',
|
||||
dialogNotice: 'Notice',
|
||||
dialogOk: 'OK',
|
||||
requestFailed: 'Request Failed',
|
||||
saved: 'Saved',
|
||||
reloadHistory: 'Reload History',
|
||||
user: 'User',
|
||||
exec: 'Exec',
|
||||
agent: 'Agent',
|
||||
toolOutput: 'tool output',
|
||||
chatServerError: 'Error: Failed to get response from server.',
|
||||
waitingForLogs: 'Waiting for logs...',
|
||||
systemLog: 'system.log',
|
||||
codeCaller: 'Code/Caller',
|
||||
recursiveAddValuePlaceholder: 'Type new value and add',
|
||||
recursiveSelectOption: 'Select option',
|
||||
logCodesSearchPlaceholder: 'Search code/text',
|
||||
logCodesNoCodes: 'No codes',
|
||||
skillsDeleteTitle: 'Delete Skill',
|
||||
skillsDeleteMessage: 'Are you sure you want to delete this skill?',
|
||||
skillsClawhubMissingTitle: 'clawhub not detected',
|
||||
skillsClawhubMissingMessage: 'clawhub is not installed. Install dependencies and clawhub automatically?',
|
||||
skillsInstallNow: 'Install now',
|
||||
skillsInstallingDeps: 'Installing node/npm and clawhub...',
|
||||
skillsInstallFailedTitle: 'Install failed',
|
||||
skillsInstallFailedMessage: 'Failed to install clawhub',
|
||||
skillsInstallDoneTitle: 'Install complete',
|
||||
skillsInstallDoneMessage: 'clawhub is installed. You can continue installing skills.',
|
||||
skillsAddTitle: 'Add Skill',
|
||||
skillsAddMessage: 'Upload skill archive (.zip / .tar.gz / .tgz / .tar). It will be extracted into skills folder and archive will be removed.',
|
||||
skillsSelectArchive: 'Select archive',
|
||||
skillsImporting: 'Uploading and importing skill...',
|
||||
skillsImportFailedTitle: 'Import failed',
|
||||
skillsImportFailedMessage: 'Failed to import skill archive',
|
||||
skillsImportDoneTitle: 'Import complete',
|
||||
skillsImportedPrefix: 'Imported',
|
||||
skillsImportDoneMessage: 'Skill imported successfully.',
|
||||
skillsFileSaved: 'Skill file saved successfully.',
|
||||
skillsNamePlaceholder: 'skill name',
|
||||
skillsClawhubNotFound: 'clawhub not found',
|
||||
skillsClawhubStatus: 'clawhub',
|
||||
skillsAdd: 'Add Skill',
|
||||
skillsNoTools: 'No tools defined',
|
||||
skillsFileEdit: 'File Edit',
|
||||
configDiffPreview: 'Diff Preview',
|
||||
configBasicMode: 'Basic Mode',
|
||||
configAdvancedMode: 'Advanced Mode',
|
||||
configHotOnly: 'Hot-reload fields only',
|
||||
configSearchPlaceholder: 'Search group...',
|
||||
configHotFieldsFull: 'Hot-reload fields (full)',
|
||||
configTopLevel: 'Top Level',
|
||||
configProxies: 'Proxies',
|
||||
configNewProviderName: 'new provider name',
|
||||
configNoCustomProviders: 'No custom providers yet.',
|
||||
configNoGroups: 'No config groups found.',
|
||||
configDiffPreviewCount: 'Diff Preview ({{count}} items)',
|
||||
saveConfigFailed: 'Failed to save config',
|
||||
sourceAutonomy: 'autonomy',
|
||||
sourceDirect: 'direct',
|
||||
sourceMemoryTodo: 'memory_todo',
|
||||
statusRunning: 'running',
|
||||
statusWaiting: 'waiting',
|
||||
statusBlocked: 'blocked',
|
||||
statusSuccess: 'success',
|
||||
statusError: 'error',
|
||||
statusSuppressed: 'suppressed',
|
||||
taskId: 'Task ID',
|
||||
inputPreview: 'Input Preview',
|
||||
blockReason: 'Block Reason',
|
||||
actionFailed: 'Action failed',
|
||||
cronExpressionPlaceholder: '*/5 * * * *',
|
||||
recipientId: 'recipient id',
|
||||
configLabels: {
|
||||
gateway: 'Gateway',
|
||||
host: 'Host',
|
||||
@@ -157,6 +269,8 @@ const resources = {
|
||||
google_maps: 'Google Maps',
|
||||
url_context: 'URL Context',
|
||||
api_base: 'API Base',
|
||||
protocol: 'Protocol',
|
||||
models: 'Models',
|
||||
organization: 'Organization',
|
||||
project: 'Project',
|
||||
region: 'Region',
|
||||
@@ -292,6 +406,118 @@ const resources = {
|
||||
save: '保存',
|
||||
create: '创建',
|
||||
update: '更新',
|
||||
delete: '删除',
|
||||
add: '添加',
|
||||
install: '安装',
|
||||
installed: '已安装',
|
||||
notInstalled: '未安装',
|
||||
status: '状态',
|
||||
source: '来源',
|
||||
duration: '耗时',
|
||||
provider: '提供商',
|
||||
model: '模型',
|
||||
time: '时间',
|
||||
level: '级别',
|
||||
code: '代码',
|
||||
template: '模板',
|
||||
content: '内容',
|
||||
id: 'ID',
|
||||
files: '文件',
|
||||
close: '关闭',
|
||||
path: '路径',
|
||||
before: '变更前',
|
||||
after: '变更后',
|
||||
hide: '隐藏',
|
||||
show: '显示',
|
||||
clear: '清空',
|
||||
pause: '暂停',
|
||||
resume: '继续',
|
||||
live: '实时',
|
||||
raw: '原始',
|
||||
pretty: '格式化',
|
||||
entries: '条',
|
||||
appName: 'ClawGo',
|
||||
webui: 'WebUI',
|
||||
node: '节点',
|
||||
unknownIp: '未知 IP',
|
||||
memoryFiles: '记忆文件',
|
||||
memoryFileNamePrompt: '记忆文件名',
|
||||
noFileSelected: '未选择文件',
|
||||
noDescription: '暂无描述。',
|
||||
empty: '(空)',
|
||||
modal: '弹窗',
|
||||
dialogPleaseConfirm: '请确认',
|
||||
dialogNotice: '提示',
|
||||
dialogOk: '确定',
|
||||
requestFailed: '请求失败',
|
||||
saved: '已保存',
|
||||
reloadHistory: '重新加载历史',
|
||||
user: '用户',
|
||||
exec: '执行',
|
||||
agent: '助手',
|
||||
toolOutput: '工具输出',
|
||||
chatServerError: '错误:获取服务端响应失败。',
|
||||
waitingForLogs: '等待日志中...',
|
||||
systemLog: '系统日志',
|
||||
codeCaller: '代码/来源',
|
||||
recursiveAddValuePlaceholder: '输入新值后添加',
|
||||
recursiveSelectOption: '下拉选择',
|
||||
logCodesSearchPlaceholder: '搜索代码/文本',
|
||||
logCodesNoCodes: '暂无代码',
|
||||
skillsDeleteTitle: '删除技能',
|
||||
skillsDeleteMessage: '确认删除这个技能吗?',
|
||||
skillsClawhubMissingTitle: '未检测到 clawhub',
|
||||
skillsClawhubMissingMessage: '检测到系统中未安装 clawhub。是否自动安装依赖环境并安装 clawhub?',
|
||||
skillsInstallNow: '立即安装',
|
||||
skillsInstallingDeps: '正在安装 node/npm 与 clawhub...',
|
||||
skillsInstallFailedTitle: '安装失败',
|
||||
skillsInstallFailedMessage: '安装 clawhub 失败',
|
||||
skillsInstallDoneTitle: '安装完成',
|
||||
skillsInstallDoneMessage: 'clawhub 已安装,可继续安装技能。',
|
||||
skillsAddTitle: '添加技能',
|
||||
skillsAddMessage: '请上传技能压缩包(.zip / .tar.gz / .tgz / .tar)。上传后将自动解压到 skills 目录,并删除上传压缩包。',
|
||||
skillsSelectArchive: '选择压缩包',
|
||||
skillsImporting: '正在上传并导入技能...',
|
||||
skillsImportFailedTitle: '导入失败',
|
||||
skillsImportFailedMessage: '技能压缩包导入失败',
|
||||
skillsImportDoneTitle: '导入完成',
|
||||
skillsImportedPrefix: '已导入',
|
||||
skillsImportDoneMessage: '技能导入成功。',
|
||||
skillsFileSaved: '技能文件保存成功。',
|
||||
skillsNamePlaceholder: '技能名',
|
||||
skillsClawhubNotFound: '未找到 clawhub',
|
||||
skillsClawhubStatus: 'clawhub',
|
||||
skillsAdd: '添加技能',
|
||||
skillsNoTools: '未定义工具',
|
||||
skillsFileEdit: '文件编辑',
|
||||
configDiffPreview: '差异预览',
|
||||
configBasicMode: '基础模式',
|
||||
configAdvancedMode: '高级模式',
|
||||
configHotOnly: '仅热更新字段',
|
||||
configSearchPlaceholder: '搜索分类...',
|
||||
configHotFieldsFull: '热更新字段(完整)',
|
||||
configTopLevel: '顶层分类',
|
||||
configProxies: '代理配置',
|
||||
configNewProviderName: '新 provider 名称',
|
||||
configNoCustomProviders: '暂无自定义 provider。',
|
||||
configNoGroups: '未找到配置分组。',
|
||||
configDiffPreviewCount: '配置差异预览({{count}}项)',
|
||||
saveConfigFailed: '保存配置失败',
|
||||
sourceAutonomy: 'autonomy',
|
||||
sourceDirect: 'direct',
|
||||
sourceMemoryTodo: 'memory_todo',
|
||||
statusRunning: 'running',
|
||||
statusWaiting: 'waiting',
|
||||
statusBlocked: 'blocked',
|
||||
statusSuccess: 'success',
|
||||
statusError: 'error',
|
||||
statusSuppressed: 'suppressed',
|
||||
taskId: '任务 ID',
|
||||
inputPreview: '输入预览',
|
||||
blockReason: '阻断原因',
|
||||
actionFailed: '操作失败',
|
||||
cronExpressionPlaceholder: '*/5 * * * *',
|
||||
recipientId: '接收者 ID',
|
||||
configLabels: {
|
||||
gateway: '网关',
|
||||
host: '主机',
|
||||
@@ -342,6 +568,8 @@ const resources = {
|
||||
google_maps: '谷歌地图',
|
||||
url_context: 'URL 上下文',
|
||||
api_base: 'API 基础地址',
|
||||
protocol: '协议',
|
||||
models: '模型列表',
|
||||
organization: '组织',
|
||||
project: '项目',
|
||||
region: '区域',
|
||||
|
||||
@@ -33,15 +33,15 @@ const Chat: React.FC = () => {
|
||||
else if (baseRole === 'system') role = 'system';
|
||||
|
||||
let text = m.content || '';
|
||||
let label = role === 'user' ? 'User' : role === 'tool' ? 'Exec' : role === 'system' ? 'System' : 'Agent';
|
||||
let label = role === 'user' ? t('user') : role === 'tool' ? t('exec') : role === 'system' ? t('system') : t('agent');
|
||||
|
||||
if (Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
||||
role = 'exec';
|
||||
label = 'Exec';
|
||||
label = t('exec');
|
||||
text = `${text}\n[tool calls: ${m.tool_calls.map((x: any) => x?.function?.name || x?.name).filter(Boolean).join(', ')}]`;
|
||||
}
|
||||
if (baseRole === 'tool') {
|
||||
text = `[tool output]\n${text}`;
|
||||
text = `[${t('toolOutput')}]\n${text}`;
|
||||
}
|
||||
return { role, text, label };
|
||||
});
|
||||
@@ -69,7 +69,7 @@ const Chat: React.FC = () => {
|
||||
}
|
||||
|
||||
const userText = msg + (media ? `\n[Attached File: ${f?.name}]` : '');
|
||||
setChat((prev) => [...prev, { role: 'user', text: userText, label: 'User' }]);
|
||||
setChat((prev) => [...prev, { role: 'user', text: userText, label: t('user') }]);
|
||||
|
||||
const currentMsg = msg;
|
||||
setMsg('');
|
||||
@@ -89,7 +89,7 @@ const Chat: React.FC = () => {
|
||||
const decoder = new TextDecoder();
|
||||
let assistantText = '';
|
||||
|
||||
setChat((prev) => [...prev, { role: 'assistant', text: '', label: 'Agent' }]);
|
||||
setChat((prev) => [...prev, { role: 'assistant', text: '', label: t('agent') }]);
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
@@ -98,7 +98,7 @@ const Chat: React.FC = () => {
|
||||
assistantText += chunk;
|
||||
setChat((prev) => {
|
||||
const next = [...prev];
|
||||
next[next.length - 1] = { role: 'assistant', text: assistantText, label: 'Agent' };
|
||||
next[next.length - 1] = { role: 'assistant', text: assistantText, label: t('agent') };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
@@ -106,7 +106,7 @@ const Chat: React.FC = () => {
|
||||
// refresh full persisted history (includes tool/internal traces)
|
||||
loadHistory();
|
||||
} catch (e) {
|
||||
setChat((prev) => [...prev, { role: 'system', text: 'Error: Failed to get response from server.', label: 'System' }]);
|
||||
setChat((prev) => [...prev, { role: 'system', text: t('chatServerError'), label: t('system') }]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +126,12 @@ const Chat: React.FC = () => {
|
||||
<div className="flex-1 flex flex-col bg-zinc-950/50">
|
||||
<div className="px-4 py-3 border-b border-zinc-800 flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm text-zinc-300 font-medium">Session</h2>
|
||||
<h2 className="text-sm text-zinc-300 font-medium">{t('session')}</h2>
|
||||
<select value={sessionKey} onChange={(e)=>setSessionKey(e.target.value)} className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200">
|
||||
{(sessions || []).map((s:any)=> <option key={s.key} value={s.key}>{s.key}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={loadHistory} className="flex items-center gap-1 px-2 py-1 text-xs rounded bg-zinc-800 hover:bg-zinc-700"><RefreshCw className="w-3 h-3"/>Reload History</button>
|
||||
<button onClick={loadHistory} className="flex items-center gap-1 px-2 py-1 text-xs rounded bg-zinc-800 hover:bg-zinc-700"><RefreshCw className="w-3 h-3"/>{t('reloadHistory')}</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
@@ -173,7 +173,7 @@ const Chat: React.FC = () => {
|
||||
<div className={`flex items-start gap-2 max-w-[96%] ${isUser ? 'flex-row-reverse' : 'flex-row'}`}>
|
||||
<div className={`w-7 h-7 mt-1 rounded-full text-xs font-bold flex items-center justify-center shrink-0 ${avatarClass}`}>{avatar}</div>
|
||||
<div className={`max-w-[92%] rounded-2xl px-4 py-3 shadow-sm ${bubbleClass}`}>
|
||||
<div className="text-[11px] opacity-80 mb-1">{m.label || (isUser ? 'User' : isExec ? 'Exec' : isSystem ? 'System' : 'Agent')}</div>
|
||||
<div className="text-[11px] opacity-80 mb-1">{m.label || (isUser ? t('user') : isExec ? t('exec') : isSystem ? t('system') : t('agent'))}</div>
|
||||
<p className="whitespace-pre-wrap text-[14px] leading-relaxed">{m.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ const Config: React.FC = () => {
|
||||
setBaseline(JSON.parse(JSON.stringify(payload)));
|
||||
setShowDiff(false);
|
||||
} catch (e) {
|
||||
alert('Failed to save config: ' + e);
|
||||
alert(`${t('saveConfigFailed')}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,15 +154,15 @@ const Config: React.FC = () => {
|
||||
<button onClick={async () => { await loadConfig(); setTimeout(() => setBaseline(JSON.parse(JSON.stringify(cfg))), 0); }} className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors">
|
||||
<RefreshCw className="w-4 h-4" /> {t('reload')}
|
||||
</button>
|
||||
<button onClick={() => setShowDiff(true)} className="px-3 py-2 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">差异预览</button>
|
||||
<button onClick={() => setShowDiff(true)} className="px-3 py-2 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">{t('configDiffPreview')}</button>
|
||||
<button onClick={() => setBasicMode(v => !v)} className="px-3 py-2 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">
|
||||
{basicMode ? '基础模式' : '高级模式'}
|
||||
{basicMode ? t('configBasicMode') : t('configAdvancedMode')}
|
||||
</button>
|
||||
<label className="flex items-center gap-2 text-sm text-zinc-300">
|
||||
<input type="checkbox" checked={hotOnly} onChange={(e) => setHotOnly(e.target.checked)} />
|
||||
仅热更新字段
|
||||
{t('configHotOnly')}
|
||||
</label>
|
||||
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="搜索分类..." className="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-lg text-sm" />
|
||||
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder={t('configSearchPlaceholder')} className="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-lg text-sm" />
|
||||
</div>
|
||||
<button onClick={saveConfig} className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium transition-colors shadow-sm">
|
||||
<Save className="w-4 h-4" /> {t('saveChanges')}
|
||||
@@ -170,7 +170,7 @@ const Config: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900/40 border border-zinc-800/80 rounded-2xl p-4">
|
||||
<div className="text-sm font-semibold text-zinc-300 mb-2">热更新字段(完整)</div>
|
||||
<div className="text-sm font-semibold text-zinc-300 mb-2">{t('configHotFieldsFull')}</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-xs">
|
||||
{hotReloadFieldDetails.map((it) => (
|
||||
<div key={it.path} className="p-2 rounded bg-zinc-950 border border-zinc-800">
|
||||
@@ -185,7 +185,7 @@ const Config: React.FC = () => {
|
||||
{!showRaw ? (
|
||||
<div className="flex-1 flex min-h-0">
|
||||
<aside className="w-44 md:w-56 border-r border-zinc-800 bg-zinc-950/40 p-2 md:p-3 overflow-y-auto shrink-0">
|
||||
<div className="text-xs text-zinc-500 uppercase tracking-widest mb-2 px-2">Top Level</div>
|
||||
<div className="text-xs text-zinc-500 uppercase tracking-widest mb-2 px-2">{t('configTopLevel')}</div>
|
||||
<div className="space-y-1">
|
||||
{filteredTopKeys.map((k) => (
|
||||
<button
|
||||
@@ -203,25 +203,25 @@ const Config: React.FC = () => {
|
||||
{activeTop === 'providers' && !showRaw && (
|
||||
<div className="rounded-xl border border-zinc-800 bg-zinc-950/40 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">Proxies</div>
|
||||
<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="new provider name" className="px-2 py-1 rounded bg-zinc-900 border border-zinc-700 text-xs" />
|
||||
<button onClick={addProxy} className="px-2 py-1 rounded bg-indigo-600 hover:bg-indigo-500 text-xs">Add</button>
|
||||
<input value={newProxyName} onChange={(e)=>setNewProxyName(e.target.value)} placeholder={t('configNewProviderName')} className="px-2 py-1 rounded bg-zinc-900 border border-zinc-700 text-xs" />
|
||||
<button onClick={addProxy} className="px-2 py-1 rounded bg-indigo-600 hover:bg-indigo-500 text-xs">{t('add')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{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-8 gap-2 rounded-lg border border-zinc-800 bg-zinc-900/40 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="api_base" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||
<input value={String(p?.api_key || '')} onChange={(e)=>updateProxyField(name, 'api_key', e.target.value)} placeholder="api_key" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||
<input value={String(p?.protocol || '')} onChange={(e)=>updateProxyField(name, 'protocol', e.target.value)} placeholder="protocol" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 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="models,a,b" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||
<button onClick={()=>removeProxy(name)} className="md:col-span-1 px-2 py-1 rounded bg-red-900/60 hover:bg-red-800 text-red-100">Delete</button>
|
||||
<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 bg-zinc-950 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 bg-zinc-950 border border-zinc-800" />
|
||||
<input value={String(p?.protocol || '')} onChange={(e)=>updateProxyField(name, 'protocol', e.target.value)} placeholder={t('configLabels.protocol')} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 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')},a,b`} className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||
<button onClick={()=>removeProxy(name)} className="md:col-span-1 px-2 py-1 rounded bg-red-900/60 hover:bg-red-800 text-red-100">{t('delete')}</button>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys(((cfg as any)?.providers?.proxies || {}) as Record<string, any>).length === 0 && (
|
||||
<div className="text-xs text-zinc-500">No custom providers yet.</div>
|
||||
<div className="text-xs text-zinc-500">{t('configNoCustomProviders')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,7 +236,7 @@ const Config: React.FC = () => {
|
||||
onChange={(path, val) => setCfg(v => setPath(v, path, val))}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-zinc-500 text-sm">No config groups found.</div>
|
||||
<div className="text-zinc-500 text-sm">{t('configNoGroups')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,16 +254,16 @@ const Config: React.FC = () => {
|
||||
<div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-4xl max-h-[85vh] bg-zinc-950 border border-zinc-800 rounded-2xl overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-zinc-800 flex items-center justify-between">
|
||||
<div className="font-semibold">配置差异预览({diffRows.length}项)</div>
|
||||
<button className="px-3 py-1 rounded bg-zinc-800" onClick={() => setShowDiff(false)}>关闭</button>
|
||||
<div className="font-semibold">{t('configDiffPreviewCount', { count: diffRows.length })}</div>
|
||||
<button className="px-3 py-1 rounded bg-zinc-800" onClick={() => setShowDiff(false)}>{t('close')}</button>
|
||||
</div>
|
||||
<div className="overflow-auto text-xs">
|
||||
<table className="w-full">
|
||||
<thead className="sticky top-0 bg-zinc-900 text-zinc-300">
|
||||
<tr>
|
||||
<th className="text-left p-2">Path</th>
|
||||
<th className="text-left p-2">Before</th>
|
||||
<th className="text-left p-2">After</th>
|
||||
<th className="text-left p-2">{t('path')}</th>
|
||||
<th className="text-left p-2">{t('before')}</th>
|
||||
<th className="text-left p-2">{t('after')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -134,10 +134,10 @@ const Cron: React.FC = () => {
|
||||
await refreshCron();
|
||||
} else {
|
||||
const err = await r.text();
|
||||
alert('Action failed: ' + err);
|
||||
alert(`${t('actionFailed')}: ${err}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Action failed: ' + e);
|
||||
alert(`${t('actionFailed')}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ const Cron: React.FC = () => {
|
||||
<div>
|
||||
<h3 className="font-semibold text-zinc-100 mb-1">{j.name || j.id}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-mono text-zinc-500 uppercase tracking-wider bg-zinc-800/50 px-2 py-0.5 rounded">ID: {j.id.slice(-6)}</span>
|
||||
<span className="text-[10px] font-mono text-zinc-500 uppercase tracking-wider bg-zinc-800/50 px-2 py-0.5 rounded">{t('id')}: {j.id.slice(-6)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{j.enabled ? (
|
||||
@@ -259,7 +259,7 @@ const Cron: React.FC = () => {
|
||||
type="text"
|
||||
value={cronForm.expr}
|
||||
onChange={(e) => setCronForm({ ...cronForm, expr: e.target.value })}
|
||||
placeholder="*/5 * * * *"
|
||||
placeholder={t('cronExpressionPlaceholder')}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
|
||||
/>
|
||||
</label>
|
||||
@@ -310,7 +310,7 @@ const Cron: React.FC = () => {
|
||||
type="text"
|
||||
value={cronForm.to}
|
||||
onChange={(e) => setCronForm({ ...cronForm, to: e.target.value })}
|
||||
placeholder="recipient id"
|
||||
placeholder={t('recipientId')}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ const Dashboard: React.FC = () => {
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-semibold tracking-tight">{t('dashboard')}</h1>
|
||||
<div className="mt-1 text-xs text-zinc-500">Gateway: {gatewayVersion} · WebUI: {webuiVersion}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{t('gateway')}: {gatewayVersion} · {t('webui')}: {webuiVersion}</div>
|
||||
</div>
|
||||
<button onClick={refreshAll} className="flex items-center gap-2 px-3 md:px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors shrink-0">
|
||||
<RefreshCw className="w-4 h-4" /> {t('refreshAll')}
|
||||
|
||||
@@ -51,7 +51,7 @@ const EKG: React.FC = () => {
|
||||
return (
|
||||
<div className="h-full p-4 md:p-6 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<h1 className="text-xl md:text-2xl font-semibold">EKG</h1>
|
||||
<h1 className="text-xl md:text-2xl font-semibold">{t('ekg')}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<select value={ekgWindow} onChange={(e)=>setEkgWindow(e.target.value as '6h' | '24h' | '7d')} className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs">
|
||||
<option value="6h">6h</option>
|
||||
|
||||
@@ -46,7 +46,7 @@ const LogCodes: React.FC = () => {
|
||||
<input
|
||||
value={kw}
|
||||
onChange={(e) => setKw(e.target.value)}
|
||||
placeholder="Search code/text"
|
||||
placeholder={t('logCodesSearchPlaceholder')}
|
||||
className="w-72 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
@@ -55,8 +55,8 @@ const LogCodes: React.FC = () => {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-zinc-900/90 border-b border-zinc-800">
|
||||
<tr className="text-zinc-400">
|
||||
<th className="text-left p-3 font-medium w-40">Code</th>
|
||||
<th className="text-left p-3 font-medium">Template</th>
|
||||
<th className="text-left p-3 font-medium w-40">{t('code')}</th>
|
||||
<th className="text-left p-3 font-medium">{t('template')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -68,7 +68,7 @@ const LogCodes: React.FC = () => {
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<tr>
|
||||
<td className="p-6 text-zinc-500" colSpan={2}>No codes</td>
|
||||
<td className="p-6 text-zinc-500" colSpan={2}>{t('logCodesNoCodes')}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
|
||||
@@ -174,7 +174,7 @@ const Logs: React.FC = () => {
|
||||
isStreaming ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' : 'bg-zinc-800 text-zinc-500 border-zinc-700'
|
||||
}`}>
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${isStreaming ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-600'}`} />
|
||||
{isStreaming ? 'Live' : 'Paused'}
|
||||
{isStreaming ? t('live') : t('paused')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -182,7 +182,7 @@ const Logs: React.FC = () => {
|
||||
onClick={() => setShowRaw(!showRaw)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors text-zinc-300"
|
||||
>
|
||||
{showRaw ? 'Pretty' : 'Raw'}
|
||||
{showRaw ? t('pretty') : t('raw')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsStreaming(!isStreaming)}
|
||||
@@ -190,10 +190,10 @@ const Logs: React.FC = () => {
|
||||
isStreaming ? 'bg-zinc-800 hover:bg-zinc-700 text-zinc-300' : 'bg-indigo-600 hover:bg-indigo-500 text-white'
|
||||
}`}
|
||||
>
|
||||
{isStreaming ? <><Square className="w-4 h-4" /> Pause</> : <><Play className="w-4 h-4" /> Resume</>}
|
||||
{isStreaming ? <><Square className="w-4 h-4" /> {t('pause')}</> : <><Play className="w-4 h-4" /> {t('resume')}</>}
|
||||
</button>
|
||||
<button onClick={clearLogs} className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors text-zinc-300">
|
||||
<Trash2 className="w-4 h-4" /> Clear
|
||||
<Trash2 className="w-4 h-4" /> {t('clear')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,15 +202,15 @@ const Logs: React.FC = () => {
|
||||
<div className="bg-zinc-900/50 px-4 py-2 border-b border-zinc-800 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-zinc-500" />
|
||||
<span className="text-xs font-mono text-zinc-500">system.log</span>
|
||||
<span className="text-xs font-mono text-zinc-500">{t('systemLog')}</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-widest">{logs.length} entries</span>
|
||||
<span className="text-[10px] font-mono text-zinc-600 uppercase tracking-widest">{logs.length} {t('entries')}</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto selection:bg-indigo-500/30">
|
||||
{logs.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-zinc-700 space-y-2 p-4">
|
||||
<Terminal className="w-8 h-8 opacity-10" />
|
||||
<p>Waiting for logs...</p>
|
||||
<p>{t('waitingForLogs')}</p>
|
||||
</div>
|
||||
) : showRaw ? (
|
||||
<div className="p-3 font-mono text-xs space-y-1">
|
||||
@@ -223,11 +223,11 @@ const Logs: React.FC = () => {
|
||||
<table className="w-full text-xs">
|
||||
<thead className="sticky top-0 bg-zinc-900/95 border-b border-zinc-800">
|
||||
<tr className="text-zinc-400">
|
||||
<th className="text-left p-2 font-medium">Time</th>
|
||||
<th className="text-left p-2 font-medium">Level</th>
|
||||
<th className="text-left p-2 font-medium">Message</th>
|
||||
<th className="text-left p-2 font-medium">Error</th>
|
||||
<th className="text-left p-2 font-medium">Code/Caller</th>
|
||||
<th className="text-left p-2 font-medium">{t('time')}</th>
|
||||
<th className="text-left p-2 font-medium">{t('level')}</th>
|
||||
<th className="text-left p-2 font-medium">{t('message')}</th>
|
||||
<th className="text-left p-2 font-medium">{t('error')}</th>
|
||||
<th className="text-left p-2 font-medium">{t('codeCaller')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
|
||||
const Memory: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { q } = useAppContext();
|
||||
const [files, setFiles] = useState<string[]>([]);
|
||||
const [active, setActive] = useState('');
|
||||
@@ -42,7 +44,7 @@ const Memory: React.FC = () => {
|
||||
}
|
||||
|
||||
async function createFile() {
|
||||
const name = prompt('memory file name', `note-${Date.now()}.md`);
|
||||
const name = prompt(t('memoryFileNamePrompt'), `note-${Date.now()}.md`);
|
||||
if (!name) return;
|
||||
await fetch(`/webui/api/memory${q}`, {
|
||||
method: 'POST',
|
||||
@@ -61,7 +63,7 @@ const Memory: React.FC = () => {
|
||||
<div className="flex h-full">
|
||||
<aside className="w-72 border-r border-zinc-800 p-4 space-y-2 overflow-y-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-semibold">Memory Files</h2>
|
||||
<h2 className="font-semibold">{t('memoryFiles')}</h2>
|
||||
<button onClick={createFile} className="px-2 py-1 rounded bg-zinc-800">+</button>
|
||||
</div>
|
||||
{files.map((f) => (
|
||||
@@ -73,8 +75,8 @@ const Memory: React.FC = () => {
|
||||
</aside>
|
||||
<main className="flex-1 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-semibold">{active || 'No file selected'}</h2>
|
||||
<button onClick={saveFile} className="px-3 py-1 rounded bg-indigo-600">Save</button>
|
||||
<h2 className="font-semibold">{active || t('noFileSelected')}</h2>
|
||||
<button onClick={saveFile} className="px-3 py-1 rounded bg-indigo-600">{t('save')}</button>
|
||||
</div>
|
||||
<textarea value={content} onChange={(e) => setContent(e.target.value)} className="w-full h-[80vh] bg-zinc-900 border border-zinc-800 rounded p-3" />
|
||||
</main>
|
||||
|
||||
@@ -36,12 +36,12 @@ const Nodes: React.FC = () => {
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-3 h-3 rounded-full ${node.online ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]'}`} />
|
||||
<div>
|
||||
<div className="font-semibold text-zinc-200">{node.name || node.id || `Node ${i + 1}`}</div>
|
||||
<div className="font-semibold text-zinc-200">{node.name || node.id || `${t('node')} ${i + 1}`}</div>
|
||||
<div className="text-xs text-zinc-500 font-mono mt-0.5">{node.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs font-mono text-zinc-400 bg-zinc-800/50 px-2 py-1 rounded">{node.ip || 'Unknown IP'}</div>
|
||||
<div className="text-xs font-mono text-zinc-400 bg-zinc-800/50 px-2 py-1 rounded">{node.ip || t('unknownIp')}</div>
|
||||
<div className="text-[10px] text-zinc-600 mt-1 font-mono uppercase tracking-widest">v{node.version || '0.0.0'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, RefreshCw, Trash2, Edit2, Zap, X, FileText, Save } from 'lucide-react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Plus, RefreshCw, Trash2, Zap, X, FileText, Save } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
import { useUI } from '../context/UIContext';
|
||||
import { Skill } from '../types';
|
||||
|
||||
const initialSkillForm: Omit<Skill, 'id'> = {
|
||||
name: '',
|
||||
description: '',
|
||||
tools: [],
|
||||
system_prompt: ''
|
||||
};
|
||||
|
||||
const Skills: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { skills, refreshSkills, q } = useAppContext();
|
||||
const { skills, refreshSkills, q, clawhubInstalled, clawhubPath } = useAppContext();
|
||||
const ui = useUI();
|
||||
const [installName, setInstallName] = useState('');
|
||||
const qp = (k: string, v: string) => `${q}${q ? '&' : '?'}${k}=${encodeURIComponent(v)}`;
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingSkill, setEditingSkill] = useState<Skill | null>(null);
|
||||
const [form, setForm] = useState<Omit<Skill, 'id'>>(initialSkillForm);
|
||||
|
||||
const [isFileModalOpen, setIsFileModalOpen] = useState(false);
|
||||
const [activeSkill, setActiveSkill] = useState<string>('');
|
||||
const [skillFiles, setSkillFiles] = useState<string[]>([]);
|
||||
const [activeFile, setActiveFile] = useState<string>('');
|
||||
const [fileContent, setFileContent] = useState('');
|
||||
const uploadRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
async function deleteSkill(id: string) {
|
||||
if (!await ui.confirmDialog({ title: 'Delete Skill', message: 'Are you sure you want to delete this skill?', danger: true, confirmText: 'Delete' })) return;
|
||||
if (!await ui.confirmDialog({ title: t('skillsDeleteTitle'), message: t('skillsDeleteMessage'), danger: true, confirmText: t('delete') })) return;
|
||||
try {
|
||||
await fetch(`/webui/api/skills${qp('id', id)}`, { method: 'DELETE' });
|
||||
await refreshSkills();
|
||||
@@ -39,39 +29,95 @@ const Skills: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function installClawHubIfNeeded() {
|
||||
if (clawhubInstalled) return true;
|
||||
const confirm = await ui.confirmDialog({
|
||||
title: t('skillsClawhubMissingTitle'),
|
||||
message: t('skillsClawhubMissingMessage'),
|
||||
confirmText: t('skillsInstallNow')
|
||||
});
|
||||
if (!confirm) return false;
|
||||
|
||||
ui.showLoading(t('skillsInstallingDeps'));
|
||||
try {
|
||||
const r = await fetch(`/webui/api/skills${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'install_clawhub' }),
|
||||
});
|
||||
const text = await r.text();
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: t('skillsInstallFailedTitle'), message: text || t('skillsInstallFailedMessage') });
|
||||
return false;
|
||||
}
|
||||
await ui.notify({ title: t('skillsInstallDoneTitle'), message: t('skillsInstallDoneMessage') });
|
||||
await refreshSkills();
|
||||
return true;
|
||||
} finally {
|
||||
ui.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function installSkill() {
|
||||
const name = installName.trim();
|
||||
if (!name) return;
|
||||
|
||||
const ready = await installClawHubIfNeeded();
|
||||
if (!ready) return;
|
||||
|
||||
const r = await fetch(`/webui/api/skills${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'install', name }),
|
||||
});
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: 'Request Failed', message: await r.text() });
|
||||
await ui.notify({ title: t('requestFailed'), message: await r.text() });
|
||||
return;
|
||||
}
|
||||
setInstallName('');
|
||||
await refreshSkills();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
async function onAddSkillClick() {
|
||||
const yes = await ui.confirmDialog({
|
||||
title: t('skillsAddTitle'),
|
||||
message: t('skillsAddMessage'),
|
||||
confirmText: t('skillsSelectArchive')
|
||||
});
|
||||
if (!yes) return;
|
||||
uploadRef.current?.click();
|
||||
}
|
||||
|
||||
async function onArchiveSelected(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const f = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (!f) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
|
||||
ui.showLoading(t('skillsImporting'));
|
||||
try {
|
||||
const action = editingSkill ? 'update' : 'create';
|
||||
const r = await fetch(`/webui/api/skills${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action, ...(editingSkill && { id: editingSkill.id }), ...form })
|
||||
body: fd,
|
||||
});
|
||||
|
||||
if (r.ok) {
|
||||
setIsModalOpen(false);
|
||||
await refreshSkills();
|
||||
} else {
|
||||
await ui.notify({ title: 'Request Failed', message: await r.text() });
|
||||
const text = await r.text();
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: t('skillsImportFailedTitle'), message: text || t('skillsImportFailedMessage') });
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
await ui.notify({ title: 'Error', message: String(e) });
|
||||
let imported: string[] = [];
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
imported = Array.isArray(j.imported) ? j.imported : [];
|
||||
} catch {
|
||||
imported = [];
|
||||
}
|
||||
await ui.notify({ title: t('skillsImportDoneTitle'), message: imported.length > 0 ? `${t('skillsImportedPrefix')}: ${imported.join(', ')}` : t('skillsImportDoneMessage') });
|
||||
await refreshSkills();
|
||||
} finally {
|
||||
ui.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +126,7 @@ const Skills: React.FC = () => {
|
||||
setIsFileModalOpen(true);
|
||||
const r = await fetch(`/webui/api/skills${q ? `${q}&id=${encodeURIComponent(skillId)}&files=1` : `?id=${encodeURIComponent(skillId)}&files=1`}`);
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: 'Request Failed', message: await r.text() });
|
||||
await ui.notify({ title: t('requestFailed'), message: await r.text() });
|
||||
return;
|
||||
}
|
||||
const j = await r.json();
|
||||
@@ -98,7 +144,7 @@ const Skills: React.FC = () => {
|
||||
const url = `/webui/api/skills${q ? `${q}&id=${encodeURIComponent(skillId)}&file=${encodeURIComponent(file)}` : `?id=${encodeURIComponent(skillId)}&file=${encodeURIComponent(file)}`}`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: 'Request Failed', message: await r.text() });
|
||||
await ui.notify({ title: t('requestFailed'), message: await r.text() });
|
||||
return;
|
||||
}
|
||||
const j = await r.json();
|
||||
@@ -114,26 +160,31 @@ const Skills: React.FC = () => {
|
||||
body: JSON.stringify({ action: 'write_file', id: activeSkill, file: activeFile, content: fileContent }),
|
||||
});
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: 'Request Failed', message: await r.text() });
|
||||
await ui.notify({ title: t('requestFailed'), message: await r.text() });
|
||||
return;
|
||||
}
|
||||
await ui.notify({ title: 'Saved', message: 'Skill file saved successfully.' });
|
||||
await ui.notify({ title: t('saved'), message: t('skillsFileSaved') });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto space-y-8">
|
||||
<input ref={uploadRef} type="file" accept=".zip,.tar,.tar.gz,.tgz" className="hidden" onChange={onArchiveSelected} />
|
||||
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t('skills')}</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<input value={installName} onChange={(e) => setInstallName(e.target.value)} placeholder="skill name" className="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-lg text-sm" />
|
||||
<button onClick={installSkill} className="px-3 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-sm font-medium">Install</button>
|
||||
<input value={installName} onChange={(e) => setInstallName(e.target.value)} placeholder={t('skillsNamePlaceholder')} className="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-lg text-sm" />
|
||||
<button onClick={installSkill} className="px-3 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-sm font-medium">{t('install')}</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`text-xs px-2 py-1 rounded-md border ${clawhubInstalled ? 'text-emerald-300 border-emerald-700/50 bg-emerald-900/20' : 'text-amber-300 border-amber-700/50 bg-amber-900/20'}`} title={clawhubPath || t('skillsClawhubNotFound')}>
|
||||
{t('skillsClawhubStatus')}: {clawhubInstalled ? t('installed') : t('notInstalled')}
|
||||
</div>
|
||||
<button onClick={() => refreshSkills()} className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-sm font-medium transition-colors">
|
||||
<RefreshCw className="w-4 h-4" /> {t('refresh')}
|
||||
</button>
|
||||
<button onClick={() => { setEditingSkill(null); setForm(initialSkillForm); setIsModalOpen(true); }} className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium transition-colors shadow-sm">
|
||||
<Plus className="w-4 h-4" /> Add Skill
|
||||
<button onClick={onAddSkillClick} className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium transition-colors shadow-sm">
|
||||
<Plus className="w-4 h-4" /> {t('skillsAdd')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,47 +199,32 @@ const Skills: React.FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-zinc-100">{s.name}</h3>
|
||||
<span className="text-[10px] font-mono text-zinc-500 uppercase tracking-widest">ID: {s.id.slice(-6)}</span>
|
||||
<span className="text-[10px] font-mono text-zinc-500 uppercase tracking-widest">{t('id')}: {s.id.slice(-6)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-zinc-400 mb-6 line-clamp-2">{s.description || 'No description provided.'}</p>
|
||||
<p className="text-sm text-zinc-400 mb-6 line-clamp-2">{s.description || t('noDescription')}</p>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div>
|
||||
<div className="text-[10px] text-zinc-500 uppercase tracking-widest mb-2">Tools</div>
|
||||
<div className="text-[10px] text-zinc-500 uppercase tracking-widest mb-2">{t('tools')}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(Array.isArray(s.tools) ? s.tools : []).map(tool => (
|
||||
<span key={tool} className="px-2 py-1 bg-zinc-800/50 text-zinc-300 text-[10px] font-mono rounded border border-zinc-700/50">{tool}</span>
|
||||
))}
|
||||
{(!Array.isArray(s.tools) || s.tools.length === 0) && <span className="text-xs text-zinc-600 italic">No tools defined</span>}
|
||||
{(!Array.isArray(s.tools) || s.tools.length === 0) && <span className="text-xs text-zinc-600 italic">{t('skillsNoTools')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-4 border-t border-zinc-800/50 mt-auto">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingSkill(s);
|
||||
setForm({
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
tools: Array.isArray(s.tools) ? s.tools : [],
|
||||
system_prompt: s.system_prompt || ''
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="flex-1 flex items-center justify-center gap-2 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-lg text-xs font-medium transition-colors text-zinc-300"
|
||||
>
|
||||
<Edit2 className="w-3.5 h-3.5" /> Edit Skill
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openFileManager(s.id)}
|
||||
className="p-2 bg-indigo-500/10 text-indigo-400 hover:bg-indigo-500/20 rounded-lg transition-colors"
|
||||
title="Files"
|
||||
className="flex-1 flex items-center justify-center gap-2 py-2 bg-indigo-500/10 text-indigo-300 hover:bg-indigo-500/20 rounded-lg text-xs font-medium transition-colors"
|
||||
title={t('files')}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<FileText className="w-4 h-4" /> {t('skillsFileEdit')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteSkill(s.id)}
|
||||
@@ -201,56 +237,13 @@ const Skills: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setIsModalOpen(false)} className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} className="relative w-full max-w-2xl bg-zinc-900 border border-zinc-800 rounded-3xl shadow-2xl overflow-hidden">
|
||||
<div className="p-6 border-b border-zinc-800 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">{editingSkill ? 'Edit Skill' : 'Add Skill'}</h2>
|
||||
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-zinc-800 rounded-full transition-colors text-zinc-400"><X className="w-5 h-5" /></button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4 max-h-[70vh] overflow-y-auto">
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">Name</span>
|
||||
<input type="text" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:border-indigo-500 outline-none" />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">Description</span>
|
||||
<input type="text" value={form.description} onChange={e => setForm({ ...form, description: e.target.value })} className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:border-indigo-500 outline-none" />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">Tools (Comma separated)</span>
|
||||
<input
|
||||
type="text"
|
||||
value={form.tools.join(', ')}
|
||||
onChange={e => setForm({ ...form, tools: e.target.value.split(',').map(t => t.trim()).filter(t => t) })}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm font-mono focus:border-indigo-500 outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-zinc-400 mb-1.5 block">System Prompt</span>
|
||||
<textarea value={form.system_prompt} onChange={e => setForm({ ...form, system_prompt: e.target.value })} rows={6} className="w-full bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:border-indigo-500 outline-none resize-none" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="p-6 border-t border-zinc-800 bg-zinc-900/50 flex justify-end gap-3">
|
||||
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm text-zinc-400">Cancel</button>
|
||||
<button onClick={handleSubmit} className="px-6 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-sm font-medium shadow-lg shadow-indigo-600/20">
|
||||
{editingSkill ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
{isFileModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setIsFileModalOpen(false)} className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<motion.div initial={{ opacity: 0, scale: 0.96 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.96 }} className="relative w-full max-w-6xl h-[80vh] bg-zinc-900 border border-zinc-800 rounded-3xl shadow-2xl overflow-hidden flex">
|
||||
<aside className="w-72 border-r border-zinc-800 bg-zinc-950/60 p-3 overflow-y-auto">
|
||||
<div className="text-sm font-semibold mb-3">{activeSkill} files</div>
|
||||
<div className="text-sm font-semibold mb-3">{activeSkill} {t('files')}</div>
|
||||
<div className="space-y-1">
|
||||
{skillFiles.map(f => (
|
||||
<button key={f} onClick={() => openFile(activeSkill, f)} className={`w-full text-left px-2 py-1.5 rounded text-xs font-mono ${activeFile===f ? 'bg-indigo-500/20 text-indigo-200' : 'text-zinc-300 hover:bg-zinc-800'}`}>{f}</button>
|
||||
@@ -259,9 +252,9 @@ const Skills: React.FC = () => {
|
||||
</aside>
|
||||
<main className="flex-1 flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-zinc-800 flex items-center justify-between">
|
||||
<div className="text-sm text-zinc-300 font-mono truncate">{activeFile || '(no file selected)'}</div>
|
||||
<div className="text-sm text-zinc-300 font-mono truncate">{activeFile || t('noFileSelected')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={saveFile} className="px-3 py-1.5 rounded bg-emerald-600 hover:bg-emerald-500 text-white text-xs flex items-center gap-1"><Save className="w-3 h-3"/>Save</button>
|
||||
<button onClick={saveFile} className="px-3 py-1.5 rounded bg-emerald-600 hover:bg-emerald-500 text-white text-xs flex items-center gap-1"><Save className="w-3 h-3"/>{t('save')}</button>
|
||||
<button onClick={() => setIsFileModalOpen(false)} className="p-2 hover:bg-zinc-800 rounded-full transition-colors text-zinc-400"><X className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,19 +109,19 @@ const TaskAudit: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<select value={sourceFilter} onChange={(e)=>setSourceFilter(e.target.value)} className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs">
|
||||
<option value="all">{t('allSources')}</option>
|
||||
<option value="autonomy">autonomy</option>
|
||||
<option value="direct">direct</option>
|
||||
<option value="memory_todo">memory_todo</option>
|
||||
<option value="autonomy">{t('sourceAutonomy')}</option>
|
||||
<option value="direct">{t('sourceDirect')}</option>
|
||||
<option value="memory_todo">{t('sourceMemoryTodo')}</option>
|
||||
<option value="-">-</option>
|
||||
</select>
|
||||
<select value={statusFilter} onChange={(e)=>setStatusFilter(e.target.value)} className="bg-zinc-900 border border-zinc-700 rounded px-2 py-1 text-xs">
|
||||
<option value="all">{t('allStatus')}</option>
|
||||
<option value="running">running</option>
|
||||
<option value="waiting">waiting</option>
|
||||
<option value="blocked">blocked</option>
|
||||
<option value="success">success</option>
|
||||
<option value="error">error</option>
|
||||
<option value="suppressed">suppressed</option>
|
||||
<option value="running">{t('statusRunning')}</option>
|
||||
<option value="waiting">{t('statusWaiting')}</option>
|
||||
<option value="blocked">{t('statusBlocked')}</option>
|
||||
<option value="success">{t('statusSuccess')}</option>
|
||||
<option value="error">{t('statusError')}</option>
|
||||
<option value="suppressed">{t('statusSuppressed')}</option>
|
||||
</select>
|
||||
<button onClick={fetchData} className="px-3 py-1.5 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-sm">{loading ? t('loading') : t('refresh')}</button>
|
||||
</div>
|
||||
@@ -132,7 +132,7 @@ const TaskAudit: React.FC = () => {
|
||||
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('dailySummary')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="date" value={reportDate} onChange={(e)=>setReportDate(e.target.value)} className="px-2 py-1 rounded bg-zinc-900 border border-zinc-700 text-xs" />
|
||||
<button onClick={() => setShowDailyReport(v => !v)} className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-xs">{showDailyReport ? 'Hide' : 'Show'}</button>
|
||||
<button onClick={() => setShowDailyReport(v => !v)} className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-xs">{showDailyReport ? t('hide') : t('show')}</button>
|
||||
<button onClick={exportDailyReport} className="px-2 py-1 rounded bg-zinc-800 hover:bg-zinc-700 text-xs">{t('export')}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,19 +180,19 @@ const TaskAudit: React.FC = () => {
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div><div className="text-zinc-500 text-xs">Task ID</div><div className="font-mono break-all">{selected.task_id}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Status</div><div>{selected.status}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Source</div><div>{selected.source || '-'}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Duration</div><div>{selected.duration_ms || 0}ms</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Channel</div><div>{selected.channel}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Session</div><div className="font-mono break-all">{selected.session}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Provider</div><div>{selected.provider || '-'}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Model</div><div>{selected.model || '-'}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">Time</div><div>{selected.time}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('taskId')}</div><div className="font-mono break-all">{selected.task_id}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('status')}</div><div>{selected.status}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('source')}</div><div>{selected.source || '-'}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('duration')}</div><div>{selected.duration_ms || 0}ms</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('channel')}</div><div>{selected.channel}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('session')}</div><div className="font-mono break-all">{selected.session}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('provider')}</div><div>{selected.provider || '-'}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('model')}</div><div>{selected.model || '-'}</div></div>
|
||||
<div><div className="text-zinc-500 text-xs">{t('time')}</div><div>{selected.time}</div></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-zinc-500 text-xs mb-1">Input Preview</div>
|
||||
<div className="text-zinc-500 text-xs mb-1">{t('inputPreview')}</div>
|
||||
<div className="p-2 rounded bg-zinc-950/60 border border-zinc-800 whitespace-pre-wrap">{selected.input_preview || '-'}</div>
|
||||
</div>
|
||||
|
||||
@@ -202,7 +202,7 @@ const TaskAudit: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-zinc-500 text-xs mb-1">Block Reason</div>
|
||||
<div className="text-zinc-500 text-xs mb-1">{t('blockReason')}</div>
|
||||
<div className="p-2 rounded bg-zinc-950/60 border border-zinc-800 whitespace-pre-wrap text-amber-200">{selected.block_reason || '-'}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ const Tasks: React.FC = () => {
|
||||
|
||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 p-4 space-y-3">
|
||||
<div className="text-xs text-zinc-400 uppercase tracking-wider">{t('taskCrud')}</div>
|
||||
<input value={draft.id || ''} onChange={(e)=>setDraft({ ...draft, id: e.target.value })} placeholder="id" className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
|
||||
<textarea value={draft.content || ''} onChange={(e)=>setDraft({ ...draft, content: e.target.value })} placeholder="content" className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[120px]" />
|
||||
<input value={draft.id || ''} onChange={(e)=>setDraft({ ...draft, id: e.target.value })} placeholder={t('id')} className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded" />
|
||||
<textarea value={draft.content || ''} onChange={(e)=>setDraft({ ...draft, content: e.target.value })} placeholder={t('content')} className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[120px]" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={()=>save('create')} className="px-2 py-1 text-xs rounded bg-emerald-700/70 hover:bg-emerald-600">{t('createTask')}</button>
|
||||
|
||||
Reference in New Issue
Block a user