feat: v0.1.1 迭代——人设外键重构、Chat UI优化、意识节点防幻觉、日志双视图
1. 人设外键重构:persona_template 成为 system_prompt 唯一权威来源, agent/系统节点通过 persona_id FK 引用,含数据迁移脚本 2. Chat UI:去掉底部AI提示、加号改为弹出菜单、新建对话乐观跳转 3. 意识节点:无可用worker时禁止编造agent_id,只能自行完成或拒绝 4. 日志页面:双tab布局(系统日志 + 工作流日志列表选择) 5. 其他:SSE流式聊天、对话删除/重命名、standalone模式修复 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
import { ProvidersSettings } from './ProvidersSettings';
|
||||
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||
import { WorkflowConfigSettings } from './WorkflowConfigSettings';
|
||||
import { SystemLogsView } from './SystemLogsView';
|
||||
import { PersonaTemplateSettings } from './PersonaTemplateSettings';
|
||||
|
||||
export function AgentLayout() {
|
||||
@@ -14,8 +12,6 @@ export function AgentLayout() {
|
||||
{ key: 'worker', label: t('agent.individual') },
|
||||
{ key: 'persona', label: t('agent.personaManagement') },
|
||||
{ key: 'providers', label: t('agent.providerManagement') },
|
||||
{ key: 'config', label: t('agent.config') },
|
||||
{ key: 'logs', label: t('agent.systemLogs') },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -39,8 +35,6 @@ export function AgentLayout() {
|
||||
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{innerAgentTab === 'persona' && <PersonaTemplateSettings />}
|
||||
{innerAgentTab === 'providers' && <ProvidersSettings />}
|
||||
{innerAgentTab === 'config' && <WorkflowConfigSettings />}
|
||||
{innerAgentTab === 'logs' && <SystemLogsView />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import apiClient from '../../api/client';
|
||||
import { Plus, Edit2, Trash2, X, Save, Loader2, FileText, Copy } from 'lucide-react';
|
||||
import type { Provider } from '../../types';
|
||||
import { Plus, Edit2, Trash2, X, Save, Loader2, FileText } from 'lucide-react';
|
||||
|
||||
interface PersonaTemplate {
|
||||
template_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
system_prompt: string;
|
||||
agent_type: string;
|
||||
provider_title: string | null;
|
||||
model_id: string | null;
|
||||
tools: string[];
|
||||
tags: string[];
|
||||
is_builtin: boolean;
|
||||
owner_id: string | null;
|
||||
}
|
||||
|
||||
export function PersonaTemplateSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [templates, setTemplates] = useState<PersonaTemplate[]>([]);
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [availableTools, setAvailableTools] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -34,14 +24,8 @@ export function PersonaTemplateSettings() {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [tplRes, provRes, toolsRes] = await Promise.all([
|
||||
apiClient.get('/api/v1/agent/template'),
|
||||
apiClient.get('/api/v1/provider/list'),
|
||||
apiClient.get('/api/v1/resource/tool')
|
||||
]);
|
||||
setTemplates(tplRes.data.templates || []);
|
||||
setProviders(Object.values(provRes.data.provider_list || {}));
|
||||
setAvailableTools(toolsRes.data.tools || []);
|
||||
const res = await apiClient.get('/api/v1/agent/template');
|
||||
setTemplates(res.data.templates || []);
|
||||
} catch { setError(t('agent.loadFailed')); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
@@ -49,9 +33,7 @@ export function PersonaTemplateSettings() {
|
||||
useEffect(() => { fetchData(); }, []);
|
||||
|
||||
const handleAddNew = () => {
|
||||
setEditData({ name: '', description: '', system_prompt: '', agent_type: 'ordinary_individual',
|
||||
provider_title: providers.length > 0 ? providers[0].provider_title : '',
|
||||
model_id: '', tools: [], tags: [] });
|
||||
setEditData({ name: '', system_prompt: '' });
|
||||
setIsNew(true);
|
||||
setIsEditing(true);
|
||||
setModalMessage('');
|
||||
@@ -70,25 +52,12 @@ export function PersonaTemplateSettings() {
|
||||
catch { alert(t('common.deleteFailed')); }
|
||||
};
|
||||
|
||||
const handleCreateWorker = async (id: string) => {
|
||||
try {
|
||||
await apiClient.post(`/api/v1/agent/worker/from-template/${id}`);
|
||||
alert(t('agent.workerCreatedFromTemplate'));
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.detail || t('common.saveFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setModalMessage('');
|
||||
setSubmitLoading(true);
|
||||
try {
|
||||
const payload = { name: editData.name, description: editData.description,
|
||||
system_prompt: editData.system_prompt, agent_type: editData.agent_type,
|
||||
provider_title: editData.provider_title || null,
|
||||
model_id: editData.model_id || null,
|
||||
tools: editData.tools || [], tags: editData.tags || [] };
|
||||
const payload = { name: editData.name, system_prompt: editData.system_prompt };
|
||||
if (isNew) await apiClient.post('/api/v1/agent/template', payload);
|
||||
else await apiClient.put(`/api/v1/agent/template/${editData.template_id}`, payload);
|
||||
setIsEditing(false);
|
||||
@@ -98,15 +67,6 @@ export function PersonaTemplateSettings() {
|
||||
} finally { setSubmitLoading(false); }
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
ordinary_individual: 'bg-bg-secondary text-text-muted',
|
||||
skill_individual: 'bg-success-bg text-success',
|
||||
special_individual: 'bg-warning-bg text-warning',
|
||||
};
|
||||
return <span className={`px-2 py-0.5 rounded-md text-[10px] font-medium ${colors[type] || colors.ordinary_individual}`}>{t(`agent.type.${type}`, type)}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl space-y-6">
|
||||
<div className="flex justify-between items-end">
|
||||
@@ -133,133 +93,46 @@ export function PersonaTemplateSettings() {
|
||||
<span className="text-sm">{t('agent.noTemplates')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="bg-bg-secondary border-b border-border-primary text-text-muted text-xs uppercase tracking-wider">
|
||||
<th className="px-5 py-3 font-semibold">{t('agent.templateName')}</th>
|
||||
<th className="px-5 py-3 font-semibold">{t('agent.type')}</th>
|
||||
<th className="px-5 py-3 font-semibold">{t('agent.tags')}</th>
|
||||
<th className="px-5 py-3 font-semibold text-right">{t('common.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-secondary">
|
||||
{templates.map((tpl) => (
|
||||
<tr key={tpl.template_id} className="hover:bg-bg-hover transition-colors">
|
||||
<td className="px-5 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-bg-secondary border border-border-primary flex items-center justify-center">
|
||||
<FileText size={14} className="text-text-muted" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-text-primary text-xs">{tpl.name}</span>
|
||||
{tpl.is_builtin && <span className="text-[10px] text-accent">{t('agent.builtin')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3">{getTypeBadge(tpl.agent_type)}</td>
|
||||
<td className="px-5 py-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(tpl.tags || []).map(tag => (
|
||||
<span key={tag} className="px-1.5 py-0.5 rounded text-[10px] bg-bg-secondary text-text-muted">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<button onClick={() => handleCreateWorker(tpl.template_id)} title={t('agent.createFromTemplate')} className="p-1.5 text-text-muted hover:text-success hover:bg-success-bg rounded-lg transition-all mr-0.5"><Copy size={14} /></button>
|
||||
{!tpl.is_builtin && <button onClick={() => handleEdit(tpl)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all mr-0.5"><Edit2 size={14} /></button>}
|
||||
{!tpl.is_builtin && <button onClick={() => handleDelete(tpl.template_id)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all"><Trash2 size={14} /></button>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="divide-y divide-border-secondary">
|
||||
{templates.map((tpl) => (
|
||||
<div key={tpl.template_id} className="flex items-center justify-between px-5 py-3.5 hover:bg-bg-hover transition-colors">
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="w-8 h-8 rounded-lg bg-bg-secondary border border-border-primary flex items-center justify-center shrink-0">
|
||||
<FileText size={14} className="text-text-muted" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium text-text-primary text-sm">{tpl.name}</div>
|
||||
<div className="text-xs text-text-muted truncate mt-0.5">{tpl.system_prompt || t('agent.noPrompt')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 shrink-0 ml-3">
|
||||
<button onClick={() => handleEdit(tpl)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all"><Edit2 size={14} /></button>
|
||||
<button onClick={() => handleDelete(tpl.template_id)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all"><Trash2 size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-border-primary animate-fade-in-scale">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary sticky top-0 bg-bg-card z-10">
|
||||
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto border border-border-primary">
|
||||
<div className="flex justify-between items-center p-5 border-b border-border-primary">
|
||||
<h2 className="text-base font-bold text-text-primary">{isNew ? t('agent.addTemplate') : t('agent.editTemplate')}</h2>
|
||||
<button onClick={() => setIsEditing(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={20} /></button>
|
||||
</div>
|
||||
<form onSubmit={handleModalSave} className="p-5 space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.templateName')}</label>
|
||||
<input type="text" required value={editData.name || ''} onChange={(e) => setEditData({...editData, name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.type')}</label>
|
||||
<select value={editData.agent_type || 'ordinary_individual'} onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="ordinary_individual">{t('agent.type.ordinary_individual')}</option>
|
||||
<option value="skill_individual">{t('agent.type.skill_individual')}</option>
|
||||
<option value="special_individual">{t('agent.type.special_individual')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.provider')}</label>
|
||||
<select value={editData.provider_title || ''} onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="">{t('common.none')}</option>
|
||||
{providers.map((p) => (<option key={p.provider_title} value={p.provider_title}>{p.provider_title}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.model')}</label>
|
||||
{(() => {
|
||||
const sp = providers.find(p => p.provider_title === editData.provider_title);
|
||||
const models = sp?.provider_models || [];
|
||||
return (
|
||||
<select value={editData.model_id || ''} onChange={(e) => setEditData({...editData, model_id: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="">{t('common.none')}</option>
|
||||
{models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.description')}</label>
|
||||
<textarea value={editData.description || ''} onChange={(e) => setEditData({...editData, description: e.target.value})} rows={2}
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.templateName')}</label>
|
||||
<input type="text" required value={editData.name || ''} onChange={(e) => setEditData({...editData, name: e.target.value})}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.systemPrompt')}</label>
|
||||
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={4}
|
||||
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={6}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.tags')}</label>
|
||||
<input type="text" value={(editData.tags || []).join(', ')}
|
||||
onChange={(e) => setEditData({...editData, tags: e.target.value.split(',').map(s => s.trim()).filter(Boolean)})}
|
||||
placeholder={t('agent.tagsPlaceholder')}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.tools')}</label>
|
||||
<div className="flex flex-wrap gap-1.5 p-3 bg-bg-input border border-border-primary rounded-xl max-h-40 overflow-y-auto">
|
||||
{availableTools.map(tool => {
|
||||
const currentTools: string[] = editData.tools || [];
|
||||
const isSelected = currentTools.includes(tool);
|
||||
return (
|
||||
<button key={tool} type="button" onClick={() => {
|
||||
const updated = isSelected ? currentTools.filter(x => x !== tool) : [...currentTools, tool];
|
||||
setEditData({...editData, tools: updated});
|
||||
}}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${isSelected ? 'bg-accent-light text-accent border border-accent/20' : 'bg-bg-secondary text-text-muted border border-border-primary hover:border-text-muted'}`}>
|
||||
{tool}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{availableTools.length === 0 && <span className="text-xs text-text-muted">{t('agent.noTools')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{modalMessage && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{modalMessage}</div>}
|
||||
<div className="pt-3 flex justify-end gap-2 border-t border-border-primary">
|
||||
<button type="button" onClick={() => setIsEditing(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshCw, Search } from 'lucide-react';
|
||||
import { RefreshCw, Search, Server, GitBranch } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
interface EventLog {
|
||||
id: number;
|
||||
@@ -13,20 +14,52 @@ interface EventLog {
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
interface WorkflowSummary {
|
||||
trace_id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface WorkflowStep {
|
||||
name: string;
|
||||
step: number;
|
||||
node: string;
|
||||
action: string;
|
||||
status: string;
|
||||
agent_id: string | null;
|
||||
}
|
||||
|
||||
const LEVEL_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
error: { bg: 'bg-[rgba(196,145,122,0.12)]', text: 'text-[#a0705a]', label: 'ERROR' },
|
||||
warn: { bg: 'bg-[rgba(196,168,130,0.15)]', text: 'text-[#9a7d5e]', label: 'WARN' },
|
||||
info: { bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]', label: 'INFO' },
|
||||
};
|
||||
|
||||
const STATUS_STYLES: Record<string, { bg: string; text: string }> = {
|
||||
completed: { bg: 'bg-success-bg', text: 'text-success' },
|
||||
failed: { bg: 'bg-[rgba(196,145,122,0.12)]', text: 'text-[#a0705a]' },
|
||||
working: { bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]' },
|
||||
pending: { bg: 'bg-bg-secondary', text: 'text-text-muted' },
|
||||
};
|
||||
|
||||
export function SystemLogsView() {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTab] = useState<'system' | 'workflow'>('system');
|
||||
|
||||
// System logs state
|
||||
const [logs, setLogs] = useState<EventLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [traceFilter, setTraceFilter] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState('');
|
||||
const [levelFilter, setLevelFilter] = useState('');
|
||||
|
||||
// Workflow logs state
|
||||
const [workflows, setWorkflows] = useState<WorkflowSummary[]>([]);
|
||||
const [selectedTrace, setSelectedTrace] = useState<string | null>(null);
|
||||
const [workflowSteps, setWorkflowSteps] = useState<WorkflowStep[]>([]);
|
||||
const [wfLoading, setWfLoading] = useState(false);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -35,13 +68,8 @@ export function SystemLogsView() {
|
||||
if (typeFilter) params.set('event_type', typeFilter);
|
||||
if (levelFilter) params.set('level', levelFilter);
|
||||
params.set('limit', '200');
|
||||
|
||||
const resp = await fetch(`/api/v1/system/logs?${params.toString()}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||||
});
|
||||
if (!resp.ok) throw new Error('Failed to fetch logs');
|
||||
const data = await resp.json();
|
||||
setLogs(data.logs || []);
|
||||
const resp = await apiClient.get(`/api/v1/system/logs?${params.toString()}`);
|
||||
setLogs(resp.data.logs || []);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
@@ -49,7 +77,36 @@ export function SystemLogsView() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchLogs(); }, []);
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
const resp = await apiClient.get('/api/v1/workflow/list');
|
||||
setWorkflows(resp.data.workflows || []);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchWorkflowDetail = async (traceId: string) => {
|
||||
setWfLoading(true);
|
||||
try {
|
||||
const resp = await apiClient.get(`/api/v1/workflow/${traceId}`);
|
||||
setWorkflowSteps(resp.data.steps || []);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setWorkflowSteps([]);
|
||||
} finally {
|
||||
setWfLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === 'system') fetchLogs();
|
||||
else fetchWorkflows();
|
||||
}, [tab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTrace) fetchWorkflowDetail(selectedTrace);
|
||||
}, [selectedTrace]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -58,87 +115,187 @@ export function SystemLogsView() {
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-text-primary">{t('agent.systemLogs')}</h2>
|
||||
<button onClick={fetchLogs} disabled={loading} className="p-2 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all">
|
||||
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
|
||||
{/* Tab Switcher */}
|
||||
<div className="flex items-center gap-6 mb-6">
|
||||
<button
|
||||
onClick={() => setTab('system')}
|
||||
className={`flex items-center gap-2 pb-2 text-sm font-medium border-b-2 transition-colors ${tab === 'system' ? 'border-accent text-accent' : 'border-transparent text-text-muted hover:text-text-primary'}`}
|
||||
>
|
||||
<Server size={14} />
|
||||
{t('agent.systemLogs')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('workflow')}
|
||||
className={`flex items-center gap-2 pb-2 text-sm font-medium border-b-2 transition-colors ${tab === 'workflow' ? 'border-accent text-accent' : 'border-transparent text-text-muted hover:text-text-primary'}`}
|
||||
>
|
||||
<GitBranch size={14} />
|
||||
{t('agent.workflowLogs')}
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={() => tab === 'system' ? fetchLogs() : fetchWorkflows()}
|
||||
disabled={loading || wfLoading}
|
||||
className="p-2 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all"
|
||||
>
|
||||
<RefreshCw size={16} className={(loading || wfLoading) ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="grid grid-cols-4 gap-3 mb-5">
|
||||
<input
|
||||
type="text"
|
||||
value={traceFilter}
|
||||
onChange={(e) => setTraceFilter(e.target.value)}
|
||||
placeholder={t('agent.logFilterTraceId')}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
/>
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
>
|
||||
<option value="">{t('agent.logFilterAllTypes')}</option>
|
||||
<option value="workflow_start">workflow_start</option>
|
||||
<option value="step_enter">step_enter</option>
|
||||
<option value="step_complete">step_complete</option>
|
||||
<option value="step_error">step_error</option>
|
||||
<option value="workflow_complete">workflow_complete</option>
|
||||
<option value="workflow_fail">workflow_fail</option>
|
||||
<option value="system">system</option>
|
||||
</select>
|
||||
<select
|
||||
value={levelFilter}
|
||||
onChange={(e) => setLevelFilter(e.target.value)}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
>
|
||||
<option value="">{t('agent.logFilterAllLevels')}</option>
|
||||
<option value="info">INFO</option>
|
||||
<option value="warn">WARN</option>
|
||||
<option value="error">ERROR</option>
|
||||
</select>
|
||||
<button type="submit" className="flex items-center justify-center gap-2 px-4 py-2 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors">
|
||||
<Search size={14} /> {t('agent.logSearch')}
|
||||
</button>
|
||||
</form>
|
||||
{tab === 'system' ? (
|
||||
<>
|
||||
<form onSubmit={handleSearch} className="grid grid-cols-4 gap-3 mb-5">
|
||||
<input
|
||||
type="text"
|
||||
value={traceFilter}
|
||||
onChange={(e) => setTraceFilter(e.target.value)}
|
||||
placeholder={t('agent.logFilterTraceId')}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
/>
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
>
|
||||
<option value="">{t('agent.logFilterAllTypes')}</option>
|
||||
<option value="workflow_start">workflow_start</option>
|
||||
<option value="step_enter">step_enter</option>
|
||||
<option value="step_complete">step_complete</option>
|
||||
<option value="step_error">step_error</option>
|
||||
<option value="workflow_complete">workflow_complete</option>
|
||||
<option value="workflow_fail">workflow_fail</option>
|
||||
<option value="system">system</option>
|
||||
</select>
|
||||
<select
|
||||
value={levelFilter}
|
||||
onChange={(e) => setLevelFilter(e.target.value)}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
>
|
||||
<option value="">{t('agent.logFilterAllLevels')}</option>
|
||||
<option value="info">INFO</option>
|
||||
<option value="warn">WARN</option>
|
||||
<option value="error">ERROR</option>
|
||||
</select>
|
||||
<button type="submit" className="flex items-center justify-center gap-2 px-4 py-2 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors">
|
||||
<Search size={14} /> {t('agent.logSearch')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="bg-bg-card border border-border-primary rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto max-h-[60vh] overflow-y-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-bg-secondary sticky top-0">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logLevel')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logType')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">Trace ID</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logNode')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logMessage')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logTime')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-primary">
|
||||
{logs.length === 0 ? (
|
||||
<tr><td colSpan={6} className="px-4 py-12 text-center text-text-muted">{t('agent.noLogs')}</td></tr>
|
||||
<div className="bg-bg-card border border-border-primary rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto max-h-[60vh] overflow-y-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-bg-secondary sticky top-0">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logLevel')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logType')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">Trace ID</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logNode')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logMessage')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logTime')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-primary">
|
||||
{logs.length === 0 ? (
|
||||
<tr><td colSpan={6} className="px-4 py-12 text-center text-text-muted">{t('agent.noLogs')}</td></tr>
|
||||
) : (
|
||||
logs.map((log) => {
|
||||
const style = LEVEL_STYLES[log.level] || LEVEL_STYLES.info;
|
||||
return (
|
||||
<tr key={log.id} className="hover:bg-bg-secondary/50 transition-colors">
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${style.bg} ${style.text}`}>{style.label}</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary font-mono">{log.event_type}</td>
|
||||
<td className="px-4 py-2.5 text-text-muted font-mono">{log.trace_id.slice(-8)}</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary">{log.node_name || '-'}</td>
|
||||
<td className="px-4 py-2.5 text-text-primary max-w-xs truncate" title={log.message}>{log.message}</td>
|
||||
<td className="px-4 py-2.5 text-text-muted whitespace-nowrap">{log.created_at ? new Date(log.created_at).toLocaleString() : '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-4 h-[calc(100vh-220px)]">
|
||||
{/* Workflow List */}
|
||||
<div className="w-72 shrink-0 bg-bg-card border border-border-primary rounded-xl overflow-hidden flex flex-col">
|
||||
<div className="px-4 py-3 border-b border-border-primary bg-bg-secondary">
|
||||
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">{t('agent.workflowLogList')}</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto divide-y divide-border-secondary">
|
||||
{workflows.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center text-text-muted text-xs">{t('workflow.noWorkflows')}</div>
|
||||
) : (
|
||||
logs.map((log) => {
|
||||
const style = LEVEL_STYLES[log.level] || LEVEL_STYLES.info;
|
||||
return (
|
||||
<tr key={log.id} className="hover:bg-bg-secondary/50 transition-colors">
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${style.bg} ${style.text}`}>{style.label}</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary font-mono">{log.event_type}</td>
|
||||
<td className="px-4 py-2.5 text-text-muted font-mono">{log.trace_id.slice(-8)}</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary">{log.node_name || '-'}</td>
|
||||
<td className="px-4 py-2.5 text-text-primary max-w-xs truncate" title={log.message}>{log.message}</td>
|
||||
<td className="px-4 py-2.5 text-text-muted whitespace-nowrap">{log.created_at ? new Date(log.created_at).toLocaleString() : '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
workflows.map((wf) => (
|
||||
<button
|
||||
key={wf.trace_id}
|
||||
onClick={() => setSelectedTrace(wf.trace_id)}
|
||||
className={`w-full text-left px-4 py-3 hover:bg-bg-hover transition-colors ${selectedTrace === wf.trace_id ? 'bg-accent-light border-l-2 border-accent' : ''}`}
|
||||
>
|
||||
<div className="text-xs font-medium text-text-primary truncate">{wf.title}</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${(STATUS_STYLES[wf.status] || STATUS_STYLES.pending).bg} ${(STATUS_STYLES[wf.status] || STATUS_STYLES.pending).text}`}>
|
||||
{wf.status}
|
||||
</span>
|
||||
<span className="text-[10px] text-text-muted">{new Date(wf.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Steps Detail */}
|
||||
<div className="flex-1 bg-bg-card border border-border-primary rounded-xl overflow-hidden flex flex-col">
|
||||
{!selectedTrace ? (
|
||||
<div className="flex-1 flex items-center justify-center text-text-muted text-sm">
|
||||
{t('agent.selectWorkflowToView')}
|
||||
</div>
|
||||
) : wfLoading ? (
|
||||
<div className="flex-1 flex items-center justify-center text-text-muted text-sm">
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-bg-secondary sticky top-0">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">#</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.name')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logNode')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('common.status')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.workflowAction')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-primary">
|
||||
{workflowSteps.length === 0 ? (
|
||||
<tr><td colSpan={5} className="px-4 py-12 text-center text-text-muted">{t('workflow.noStepsYet')}</td></tr>
|
||||
) : (
|
||||
workflowSteps.map((step, idx) => {
|
||||
const ss = STATUS_STYLES[step.status] || STATUS_STYLES.pending;
|
||||
return (
|
||||
<tr key={idx} className="hover:bg-bg-secondary/50 transition-colors">
|
||||
<td className="px-4 py-2.5 text-text-muted">{step.step || idx + 1}</td>
|
||||
<td className="px-4 py-2.5 text-text-primary font-medium">{step.name}</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary">{step.node}</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${ss.bg} ${ss.text}`}>{step.status}</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary max-w-sm truncate" title={step.action}>{step.action}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,25 @@ interface WorkerIndividual {
|
||||
description?: string;
|
||||
provider_title: string;
|
||||
model_id: string;
|
||||
system_prompt?: string;
|
||||
persona_id?: string;
|
||||
output_template?: string;
|
||||
bound_skill?: string;
|
||||
workspace?: string;
|
||||
tools?: string;
|
||||
}
|
||||
|
||||
interface PersonaTemplate {
|
||||
template_id: string;
|
||||
name: string;
|
||||
system_prompt: string;
|
||||
}
|
||||
|
||||
export function WorkerIndividualSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [workers, setWorkers] = useState<WorkerIndividual[]>([]);
|
||||
const [systemNodes, setSystemNodes] = useState<any[]>([]);
|
||||
const [personaTemplates, setPersonaTemplates] = useState<PersonaTemplate[]>([]);
|
||||
const [availableSkills, setAvailableSkills] = useState<string[]>([]);
|
||||
const [availableTools, setAvailableTools] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -36,17 +43,19 @@ export function WorkerIndividualSettings() {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [provRes, workRes, sysRes, toolsRes, skillsRes] = await Promise.all([
|
||||
const [provRes, workRes, sysRes, toolsRes, skillsRes, tplRes] = await Promise.all([
|
||||
apiClient.get('/api/v1/provider/list'),
|
||||
apiClient.get('/api/v1/agent/worker'),
|
||||
apiClient.get('/api/v1/agent'),
|
||||
apiClient.get('/api/v1/resource/tool'),
|
||||
apiClient.get('/api/v1/resource/skill')
|
||||
apiClient.get('/api/v1/resource/skill'),
|
||||
apiClient.get('/api/v1/agent/template')
|
||||
]);
|
||||
setProviders(Object.values(provRes.data.provider_list || {}));
|
||||
setWorkers(workRes.data.workers || []);
|
||||
setAvailableTools(toolsRes.data.tools || []);
|
||||
setAvailableSkills(Object.keys(skillsRes.data.skills || {}));
|
||||
setPersonaTemplates(tplRes.data.templates || []);
|
||||
|
||||
const providersList = Object.values(provRes.data.provider_list || {}) as Provider[];
|
||||
const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
|
||||
@@ -61,7 +70,7 @@ export function WorkerIndividualSettings() {
|
||||
provider_title: found?.provider_title || defaultProvider,
|
||||
model_id: found?.model_id || '',
|
||||
tools: found?.tools ? JSON.stringify(found.tools) : '[]',
|
||||
custom_system_prompt: found?.custom_system_prompt || '',
|
||||
persona_id: found?.persona_id || '',
|
||||
is_system: true
|
||||
};
|
||||
}));
|
||||
@@ -90,7 +99,7 @@ export function WorkerIndividualSettings() {
|
||||
const handleAddNew = () => {
|
||||
setEditData({ agent_name: '', agent_type: 'ordinary_individual', description: '',
|
||||
provider_title: providers.length > 0 ? providers[0].provider_title : '', model_id: '',
|
||||
system_prompt: '', output_template: '{}', bound_skill: '{}', workspace: '[]', tools: '[]' });
|
||||
persona_id: '', output_template: '{}', bound_skill: '{}', workspace: '[]', tools: '[]' });
|
||||
setIsNew(true);
|
||||
setIsEditing(true);
|
||||
setModalMessage('');
|
||||
@@ -112,7 +121,7 @@ export function WorkerIndividualSettings() {
|
||||
provider_title: editData.provider_title,
|
||||
model_id: editData.model_id,
|
||||
tools: JSON.parse(editData.tools || '[]'),
|
||||
custom_system_prompt: (editData as any).custom_system_prompt || null,
|
||||
persona_id: (editData as any).persona_id || null,
|
||||
display_name: (editData as any).display_name || null
|
||||
});
|
||||
} else {
|
||||
@@ -280,10 +289,12 @@ export function WorkerIndividualSettings() {
|
||||
</div>
|
||||
{(editData as any).is_system && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.customPrompt')}</label>
|
||||
<textarea value={(editData as any).custom_system_prompt || ''} onChange={(e) => setEditData({...editData, custom_system_prompt: e.target.value} as any)} rows={3}
|
||||
placeholder={t('agent.customPromptPlaceholder')}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.persona')}</label>
|
||||
<select value={(editData as any).persona_id || ''} onChange={(e) => setEditData({...editData, persona_id: e.target.value || null} as any)}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="">{t('common.none')}</option>
|
||||
{personaTemplates.map(p => <option key={p.template_id} value={p.template_id}>{p.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{!(editData as any).is_system && (
|
||||
@@ -294,9 +305,12 @@ export function WorkerIndividualSettings() {
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.systemPrompt')}</label>
|
||||
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={3}
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
|
||||
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.persona')}</label>
|
||||
<select value={editData.persona_id || ''} onChange={(e) => setEditData({...editData, persona_id: e.target.value})} required
|
||||
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
|
||||
<option value="" disabled>{t('agent.selectPersona')}</option>
|
||||
{personaTemplates.map(p => <option key={p.template_id} value={p.template_id}>{p.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user