ideaco 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- package/tailwind.config.mjs +11 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import { useI18n } from '@/lib/i18n';
|
|
6
|
+
import ProviderGrid from './ProviderGrid';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SystemMonitor - Operational dashboard for enterprise subsystems
|
|
10
|
+
* User-facing: manage cron jobs, toggle plugins, view system health
|
|
11
|
+
*/
|
|
12
|
+
export default function SystemMonitor({ embedded = false }) {
|
|
13
|
+
const { t } = useI18n();
|
|
14
|
+
const { company, fetchCronJobs, createCronJob, manageCronJob, fetchPlugins, managePlugin, fetchSkills, manageSkill, fetchKnowledge, searchKnowledge, manageKnowledge, fetchSystemStatus, factoryReset } = useStore();
|
|
15
|
+
const [activeSection, setActiveSection] = useState('providers');
|
|
16
|
+
const [cronData, setCronData] = useState({ summary: {}, jobs: [] });
|
|
17
|
+
const [plugins, setPlugins] = useState([]);
|
|
18
|
+
const [skills, setSkills] = useState([]);
|
|
19
|
+
const [knowledge, setKnowledge] = useState({ bases: [], stats: {} });
|
|
20
|
+
const [systemStatus, setSystemStatus] = useState(null);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
|
|
23
|
+
// Knowledge base state
|
|
24
|
+
const [showCreateKB, setShowCreateKB] = useState(false);
|
|
25
|
+
const [newKB, setNewKB] = useState({ name: '', description: '', type: 'global' });
|
|
26
|
+
const [showAddEntry, setShowAddEntry] = useState(null); // kbId or null
|
|
27
|
+
const [newEntry, setNewEntry] = useState({ title: '', content: '', entryType: 'note', tags: '' });
|
|
28
|
+
const [kbSearchQuery, setKbSearchQuery] = useState('');
|
|
29
|
+
const [kbSearchResults, setKbSearchResults] = useState(null);
|
|
30
|
+
|
|
31
|
+
// Danger zone state
|
|
32
|
+
const [showFactoryReset, setShowFactoryReset] = useState(false);
|
|
33
|
+
const [factoryResetInput, setFactoryResetInput] = useState('');
|
|
34
|
+
const [factoryResetting, setFactoryResetting] = useState(false);
|
|
35
|
+
|
|
36
|
+
// Cron job creation form
|
|
37
|
+
const [showCreateJob, setShowCreateJob] = useState(false);
|
|
38
|
+
const [newJob, setNewJob] = useState({ name: '', cronExpression: '', agentId: '', taskPrompt: '', description: '' });
|
|
39
|
+
|
|
40
|
+
const refresh = useCallback(async () => {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
const [cron, plug, sk, kb, status] = await Promise.all([
|
|
43
|
+
fetchCronJobs(), fetchPlugins(), fetchSkills(), fetchKnowledge(), fetchSystemStatus(),
|
|
44
|
+
]);
|
|
45
|
+
setCronData(cron || { summary: {}, jobs: [] });
|
|
46
|
+
setPlugins(plug || []);
|
|
47
|
+
setSkills(sk || []);
|
|
48
|
+
setKnowledge(kb || { bases: [], stats: {} });
|
|
49
|
+
setSystemStatus(status);
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}, [fetchCronJobs, fetchPlugins, fetchSkills, fetchKnowledge, fetchSystemStatus]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => { refresh(); }, [refresh]);
|
|
54
|
+
|
|
55
|
+
// Get all agents for cron job assignment
|
|
56
|
+
const allAgents = [];
|
|
57
|
+
if (company?.departments) {
|
|
58
|
+
for (const dept of company.departments) {
|
|
59
|
+
for (const m of (dept.members || [])) {
|
|
60
|
+
allAgents.push({ id: m.id, name: m.name, role: m.role, department: dept.name });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleCreateJob = async () => {
|
|
66
|
+
if (!newJob.name || !newJob.cronExpression || !newJob.agentId || !newJob.taskPrompt) return;
|
|
67
|
+
try {
|
|
68
|
+
await createCronJob(newJob);
|
|
69
|
+
setShowCreateJob(false);
|
|
70
|
+
setNewJob({ name: '', cronExpression: '', agentId: '', taskPrompt: '', description: '' });
|
|
71
|
+
await refresh();
|
|
72
|
+
} catch {}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleJobAction = async (action, jobId) => {
|
|
76
|
+
try {
|
|
77
|
+
await manageCronJob(action, jobId);
|
|
78
|
+
await refresh();
|
|
79
|
+
} catch {}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handlePluginToggle = async (pluginId, currentState) => {
|
|
83
|
+
const action = currentState === 'enabled' ? 'disable' : 'enable';
|
|
84
|
+
try {
|
|
85
|
+
await managePlugin(action, pluginId);
|
|
86
|
+
await refresh();
|
|
87
|
+
} catch {}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
const handleSkillToggle = async (skillId, currentState) => {
|
|
93
|
+
const action = currentState === 'enabled' ? 'disable' : 'enable';
|
|
94
|
+
try {
|
|
95
|
+
await manageSkill(action, skillId);
|
|
96
|
+
await refresh();
|
|
97
|
+
} catch {}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleCreateKB = async () => {
|
|
101
|
+
if (!newKB.name) return;
|
|
102
|
+
try {
|
|
103
|
+
await manageKnowledge('create', newKB);
|
|
104
|
+
setShowCreateKB(false);
|
|
105
|
+
setNewKB({ name: '', description: '', type: 'global' });
|
|
106
|
+
await refresh();
|
|
107
|
+
} catch {}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleAddEntry = async () => {
|
|
111
|
+
if (!newEntry.title || !newEntry.content) return;
|
|
112
|
+
try {
|
|
113
|
+
await manageKnowledge('addEntry', {
|
|
114
|
+
kbId: showAddEntry,
|
|
115
|
+
...newEntry,
|
|
116
|
+
tags: newEntry.tags.split(',').map(t => t.trim()).filter(Boolean),
|
|
117
|
+
});
|
|
118
|
+
setShowAddEntry(null);
|
|
119
|
+
setNewEntry({ title: '', content: '', entryType: 'note', tags: '' });
|
|
120
|
+
await refresh();
|
|
121
|
+
} catch {}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleKBSearch = async () => {
|
|
125
|
+
if (!kbSearchQuery.trim()) return;
|
|
126
|
+
const results = await searchKnowledge(kbSearchQuery);
|
|
127
|
+
setKbSearchResults(results);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const SKILL_CATEGORY_ICONS = {
|
|
131
|
+
coding: '💻', analysis: '📊', creative: '✨', communication: '💬',
|
|
132
|
+
automation: '🤖', research: '🔍', design: '🎨', devops: '🚀',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const PLUGIN_CATEGORY_MAP = {
|
|
136
|
+
'builtin-web-search': 'Web & Search',
|
|
137
|
+
'builtin-web-fetch': 'Web & Search',
|
|
138
|
+
'builtin-firecrawl': 'Web & Search',
|
|
139
|
+
'builtin-browser': 'Browser & UI',
|
|
140
|
+
'builtin-canvas': 'Browser & UI',
|
|
141
|
+
'builtin-diffs': 'Browser & UI',
|
|
142
|
+
'builtin-exec': 'Runtime & Execution',
|
|
143
|
+
'builtin-apply-patch': 'Runtime & Execution',
|
|
144
|
+
'builtin-memory': 'Memory & Knowledge',
|
|
145
|
+
'builtin-image': 'Media & Content',
|
|
146
|
+
'builtin-pdf': 'Media & Content',
|
|
147
|
+
'builtin-tts': 'Media & Content',
|
|
148
|
+
'builtin-data-processing': 'Media & Content',
|
|
149
|
+
'builtin-message': 'Communication',
|
|
150
|
+
'builtin-reactions': 'Communication',
|
|
151
|
+
'builtin-bird': 'Communication',
|
|
152
|
+
'builtin-sessions': 'Sessions & Multi-Agent',
|
|
153
|
+
'builtin-subagents': 'Sessions & Multi-Agent',
|
|
154
|
+
'builtin-cron': 'Automation & Infra',
|
|
155
|
+
'builtin-gateway': 'Automation & Infra',
|
|
156
|
+
'builtin-nodes': 'Automation & Infra',
|
|
157
|
+
'builtin-lobster': 'Workflow & AI',
|
|
158
|
+
'builtin-llm-task': 'Workflow & AI',
|
|
159
|
+
'builtin-thinking': 'Workflow & AI',
|
|
160
|
+
'builtin-code-review': 'Code Quality',
|
|
161
|
+
'builtin-notifications': 'Code Quality',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const PLUGIN_CATEGORY_ICONS = {
|
|
165
|
+
'Web & Search': '🌐',
|
|
166
|
+
'Browser & UI': '🖥️',
|
|
167
|
+
'Runtime & Execution': '⚡',
|
|
168
|
+
'Memory & Knowledge': '🧠',
|
|
169
|
+
'Media & Content': '🎨',
|
|
170
|
+
'Communication': '💬',
|
|
171
|
+
'Sessions & Multi-Agent': '🤖',
|
|
172
|
+
'Automation & Infra': '⚙️',
|
|
173
|
+
'Workflow & AI': '🔗',
|
|
174
|
+
'Code Quality': '✅',
|
|
175
|
+
'Other': '🧩',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const sections = [
|
|
179
|
+
{ id: 'providers', icon: '⚡', label: t('sidebar.nav.providers') },
|
|
180
|
+
{ id: 'cron', icon: '⏰', label: t('systemSettings.cards.cron') },
|
|
181
|
+
{ id: 'plugins', icon: '🧩', label: t('systemSettings.cards.plugins') },
|
|
182
|
+
{ id: 'skills', icon: '📚', label: t('systemSettings.cards.skills') },
|
|
183
|
+
{ id: 'knowledge', icon: '🧠', label: t('systemSettings.cards.knowledge') },
|
|
184
|
+
{ id: 'health', icon: '💓', label: t('systemSettings.health.title') },
|
|
185
|
+
{ id: 'danger', icon: '⚠️', label: t('systemSettings.dangerZone.title') },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className={embedded ? 'space-y-6' : 'p-6 space-y-6 animate-fade-in'}>
|
|
190
|
+
{/* Header - 仅独立模式下显示 */}
|
|
191
|
+
{!embedded && (
|
|
192
|
+
<div className="flex items-center justify-between">
|
|
193
|
+
<div>
|
|
194
|
+
<h1 className="text-2xl font-bold">{t('systemSettings.title')}</h1>
|
|
195
|
+
<p className="text-sm text-[var(--muted)] mt-1">{t('systemSettings.subtitle')}</p>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{/* Section Tabs */}
|
|
201
|
+
<div className="flex gap-2">
|
|
202
|
+
{sections.map(s => (
|
|
203
|
+
<button
|
|
204
|
+
key={s.id}
|
|
205
|
+
onClick={() => setActiveSection(s.id)}
|
|
206
|
+
className={`px-4 py-2 rounded-lg text-sm flex items-center gap-2 transition-all ${
|
|
207
|
+
activeSection === s.id
|
|
208
|
+
? 'bg-[var(--accent)] text-white'
|
|
209
|
+
: 'bg-white/5 text-[var(--muted)] hover:bg-white/10'
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
<span>{s.icon}</span>
|
|
213
|
+
<span>{s.label}</span>
|
|
214
|
+
</button>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* === PROVIDERS SECTION === */}
|
|
219
|
+
{activeSection === 'providers' && (
|
|
220
|
+
<div className="space-y-4">
|
|
221
|
+
<ProviderGrid
|
|
222
|
+
showDescription
|
|
223
|
+
showSecretary
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
{/* === CRON SECTION === */}
|
|
229
|
+
{activeSection === 'cron' && (
|
|
230
|
+
<div className="space-y-4">
|
|
231
|
+
{/* Quick Stats */}
|
|
232
|
+
<div className="grid grid-cols-4 gap-4">
|
|
233
|
+
<StatCard label={t('systemSettings.cronStats.running')} value={cronData.summary?.running ? '✅' : '❌'} />
|
|
234
|
+
<StatCard label={t('systemSettings.cronStats.jobs')} value={cronData.jobs?.length || 0} />
|
|
235
|
+
<StatCard label={t('systemSettings.cronDetail.activeJobs')} value={cronData.summary?.activeJobs || 0} color="text-green-400" />
|
|
236
|
+
<StatCard label={t('systemSettings.cronDetail.totalRuns')} value={cronData.summary?.totalRuns || 0} />
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Create Job Button */}
|
|
240
|
+
<div className="flex justify-between items-center">
|
|
241
|
+
<h3 className="text-sm font-semibold">{t('systemSettings.cronDetail.jobList')}</h3>
|
|
242
|
+
<button onClick={() => setShowCreateJob(!showCreateJob)} className="btn-primary text-xs">
|
|
243
|
+
{showCreateJob ? '✕ ' + t('common.cancel') : '+ ' + t('systemSettings.cronDetail.createJob')}
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{/* Create Job Form */}
|
|
248
|
+
{showCreateJob && (
|
|
249
|
+
<div className="card space-y-3 animate-fade-in">
|
|
250
|
+
<h4 className="text-sm font-medium">{t('systemSettings.cronDetail.createJob')}</h4>
|
|
251
|
+
<div className="grid grid-cols-2 gap-3">
|
|
252
|
+
<input
|
|
253
|
+
className="input text-sm"
|
|
254
|
+
placeholder={t('systemSettings.cronForm.name')}
|
|
255
|
+
value={newJob.name}
|
|
256
|
+
onChange={e => setNewJob({ ...newJob, name: e.target.value })}
|
|
257
|
+
/>
|
|
258
|
+
<input
|
|
259
|
+
className="input text-sm"
|
|
260
|
+
placeholder={t('systemSettings.cronForm.schedule')}
|
|
261
|
+
value={newJob.cronExpression}
|
|
262
|
+
onChange={e => setNewJob({ ...newJob, cronExpression: e.target.value })}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
<select
|
|
266
|
+
className="input text-sm w-full"
|
|
267
|
+
value={newJob.agentId}
|
|
268
|
+
onChange={e => setNewJob({ ...newJob, agentId: e.target.value })}
|
|
269
|
+
>
|
|
270
|
+
<option value="">{t('systemSettings.cronForm.selectAgent')}</option>
|
|
271
|
+
{allAgents.map(a => (
|
|
272
|
+
<option key={a.id} value={a.id}>{a.name} ({a.role}) — {a.department}</option>
|
|
273
|
+
))}
|
|
274
|
+
</select>
|
|
275
|
+
<textarea
|
|
276
|
+
className="input text-sm w-full"
|
|
277
|
+
rows={3}
|
|
278
|
+
placeholder={t('systemSettings.cronForm.taskPrompt')}
|
|
279
|
+
value={newJob.taskPrompt}
|
|
280
|
+
onChange={e => setNewJob({ ...newJob, taskPrompt: e.target.value })}
|
|
281
|
+
/>
|
|
282
|
+
<div className="text-xs text-[var(--muted)] space-y-1">
|
|
283
|
+
<div>{t('systemSettings.cronForm.scheduleHint')}</div>
|
|
284
|
+
</div>
|
|
285
|
+
<button onClick={handleCreateJob} className="btn-primary text-sm max-w-xs">
|
|
286
|
+
{t('systemSettings.cronDetail.createJob')}
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
290
|
+
|
|
291
|
+
{/* Job List */}
|
|
292
|
+
{cronData.jobs?.length > 0 ? (
|
|
293
|
+
<div className="space-y-2">
|
|
294
|
+
{cronData.jobs.map(job => (
|
|
295
|
+
<div key={job.id} className="card flex items-center gap-4">
|
|
296
|
+
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
|
297
|
+
job.status === 'active' ? 'bg-green-500' :
|
|
298
|
+
job.status === 'running' ? 'bg-blue-500 animate-pulse' :
|
|
299
|
+
job.status === 'paused' ? 'bg-yellow-500' :
|
|
300
|
+
job.status === 'error' ? 'bg-red-500' : 'bg-gray-500'
|
|
301
|
+
}`} />
|
|
302
|
+
<div className="flex-1 min-w-0">
|
|
303
|
+
<div className="text-sm font-medium truncate">{job.name}</div>
|
|
304
|
+
<div className="text-[10px] text-[var(--muted)] flex items-center gap-3 mt-0.5">
|
|
305
|
+
<span>📅 {job.cronExpression}</span>
|
|
306
|
+
<span>🔄 {t('systemSettings.cronDetail.runs')}: {job.runCount}</span>
|
|
307
|
+
{job.nextRun && <span>⏭ {new Date(job.nextRun).toLocaleTimeString()}</span>}
|
|
308
|
+
{job.lastError && <span className="text-red-400">⚠ {job.lastError.slice(0, 30)}</span>}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
312
|
+
{job.status === 'active' && (
|
|
313
|
+
<button onClick={() => handleJobAction('pause', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.pause')}>⏸</button>
|
|
314
|
+
)}
|
|
315
|
+
{job.status === 'paused' && (
|
|
316
|
+
<button onClick={() => handleJobAction('resume', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.resume')}>▶️</button>
|
|
317
|
+
)}
|
|
318
|
+
{job.status === 'error' && (
|
|
319
|
+
<button onClick={() => handleJobAction('resume', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.retry')}>🔁</button>
|
|
320
|
+
)}
|
|
321
|
+
<button onClick={() => handleJobAction('trigger', job.id)} className="btn-ghost text-[10px]" title={t('systemSettings.cronJobActions.runNow')}>🚀</button>
|
|
322
|
+
<button onClick={() => handleJobAction('delete', job.id)} className="btn-ghost text-[10px] text-red-400" title={t('systemSettings.cronJobActions.delete')}>🗑</button>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
) : (
|
|
328
|
+
<div className="text-center py-12 text-[var(--muted)]">
|
|
329
|
+
<div className="text-4xl mb-3">⏰</div>
|
|
330
|
+
<p className="text-sm">{t('systemSettings.cronDetail.noJobs')}</p>
|
|
331
|
+
<p className="text-xs mt-1">{t('systemSettings.cronDetail.noJobsHint')}</p>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{/* === PLUGINS SECTION === */}
|
|
338
|
+
{activeSection === 'plugins' && (
|
|
339
|
+
<div className="space-y-4">
|
|
340
|
+
<div className="grid grid-cols-3 gap-4">
|
|
341
|
+
<StatCard label={t('systemSettings.pluginStats.registered')} value={plugins.length} />
|
|
342
|
+
<StatCard label={t('systemSettings.pluginStats.enabled')} value={plugins.filter(p => p.state === 'enabled').length} color="text-green-400" />
|
|
343
|
+
<StatCard label={t('systemSettings.pluginDetail.totalTools')} value={plugins.reduce((s, p) => s + (p.toolCount || 0), 0)} />
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{plugins.length > 0 ? (
|
|
347
|
+
<div className="space-y-4">
|
|
348
|
+
{Object.entries(
|
|
349
|
+
plugins.reduce((acc, p) => {
|
|
350
|
+
const cat = PLUGIN_CATEGORY_MAP[p.id] || 'Other';
|
|
351
|
+
(acc[cat] = acc[cat] || []).push(p);
|
|
352
|
+
return acc;
|
|
353
|
+
}, {})
|
|
354
|
+
).map(([category, catPlugins]) => (
|
|
355
|
+
<div key={category} className="card">
|
|
356
|
+
<div className="flex items-center gap-2 mb-3">
|
|
357
|
+
<span className="text-lg">{PLUGIN_CATEGORY_ICONS[category] || '🧩'}</span>
|
|
358
|
+
<h3 className="text-sm font-semibold">{category}</h3>
|
|
359
|
+
<span className="text-[10px] text-[var(--muted)]">{t('providers.pluginsCount', { n: catPlugins.length })}</span>
|
|
360
|
+
</div>
|
|
361
|
+
<div className="space-y-2">
|
|
362
|
+
{catPlugins.map(plugin => (
|
|
363
|
+
<div key={plugin.id} className="flex items-center gap-4 p-2 rounded-lg hover:bg-white/5 transition-all">
|
|
364
|
+
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
|
365
|
+
plugin.state === 'enabled' ? 'bg-green-500' : 'bg-gray-500'
|
|
366
|
+
}`} />
|
|
367
|
+
<div className="flex-1 min-w-0">
|
|
368
|
+
<div className="flex items-center gap-2">
|
|
369
|
+
<span className="text-sm font-medium">{plugin.name}</span>
|
|
370
|
+
<span className="text-[10px] text-[var(--muted)]">v{plugin.version}</span>
|
|
371
|
+
</div>
|
|
372
|
+
<div className="text-[10px] text-[var(--muted)] mt-0.5">
|
|
373
|
+
{plugin.description}
|
|
374
|
+
{plugin.toolCount > 0 && <span className="ml-2">{t('providers.toolsCount', { n: plugin.toolCount })}</span>}
|
|
375
|
+
{plugin.hookCount > 0 && <span className="ml-2">{t('providers.hooksCount', { n: plugin.hookCount })}</span>}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<button
|
|
379
|
+
onClick={() => handlePluginToggle(plugin.id, plugin.state)}
|
|
380
|
+
className={`text-xs px-3 py-1 rounded-full transition-all ${
|
|
381
|
+
plugin.state === 'enabled'
|
|
382
|
+
? 'bg-green-900/30 text-green-400 hover:bg-red-900/30 hover:text-red-400'
|
|
383
|
+
: 'bg-white/10 text-[var(--muted)] hover:bg-green-900/30 hover:text-green-400'
|
|
384
|
+
}`}
|
|
385
|
+
>
|
|
386
|
+
{plugin.state === 'enabled' ? t('common.disable') : t('common.enable')}
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
))}
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
))}
|
|
393
|
+
</div>
|
|
394
|
+
) : (
|
|
395
|
+
<div className="text-center py-12 text-[var(--muted)]">
|
|
396
|
+
<div className="text-4xl mb-3">🧩</div>
|
|
397
|
+
<p className="text-sm">{t('systemSettings.pluginDetail.noPlugins')}</p>
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{/* === SKILLS SECTION === */}
|
|
404
|
+
{activeSection === 'skills' && (
|
|
405
|
+
<div className="space-y-4">
|
|
406
|
+
<div className="grid grid-cols-4 gap-4">
|
|
407
|
+
<StatCard label={t('systemSettings.skillStats.total')} value={skills.length} />
|
|
408
|
+
<StatCard label={t('systemSettings.skillStats.enabled')} value={skills.filter(s => s.state === 'enabled').length} color="text-green-400" />
|
|
409
|
+
<StatCard label={t('systemSettings.skillStats.categories')} value={[...new Set(skills.map(s => s.category))].length} />
|
|
410
|
+
<StatCard label={t('systemSettings.skillStats.installed')} value={skills.filter(s => s.state !== 'available').length} />
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
{/* Skills grouped by category */}
|
|
414
|
+
{Object.entries(
|
|
415
|
+
skills.reduce((acc, s) => {
|
|
416
|
+
(acc[s.category] = acc[s.category] || []).push(s);
|
|
417
|
+
return acc;
|
|
418
|
+
}, {})
|
|
419
|
+
).map(([category, catSkills]) => (
|
|
420
|
+
<div key={category} className="card">
|
|
421
|
+
<div className="flex items-center gap-2 mb-3">
|
|
422
|
+
<span className="text-lg">{SKILL_CATEGORY_ICONS[category] || '⚡'}</span>
|
|
423
|
+
<h3 className="text-sm font-semibold capitalize">{category}</h3>
|
|
424
|
+
<span className="text-[10px] text-[var(--muted)]">{t('providers.skillsCount', { n: catSkills.length })}</span>
|
|
425
|
+
</div>
|
|
426
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2">
|
|
427
|
+
{catSkills.map(skill => (
|
|
428
|
+
<div key={skill.id} className={`p-3 rounded-lg border transition-all ${
|
|
429
|
+
skill.state === 'enabled'
|
|
430
|
+
? 'border-green-500/30 bg-green-900/10'
|
|
431
|
+
: 'border-[var(--border)] bg-[var(--background)]'
|
|
432
|
+
}`}>
|
|
433
|
+
<div className="flex items-center justify-between mb-1">
|
|
434
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
435
|
+
<span>{skill.icon}</span>
|
|
436
|
+
<span className="text-sm font-medium truncate">{skill.name}</span>
|
|
437
|
+
</div>
|
|
438
|
+
<button
|
|
439
|
+
onClick={() => handleSkillToggle(skill.id, skill.state)}
|
|
440
|
+
className={`text-[10px] px-2 py-0.5 rounded-full transition-all ${
|
|
441
|
+
skill.state === 'enabled'
|
|
442
|
+
? 'bg-green-900/30 text-green-400 hover:bg-red-900/30 hover:text-red-400'
|
|
443
|
+
: 'bg-white/10 text-[var(--muted)] hover:bg-green-900/30 hover:text-green-400'
|
|
444
|
+
}`}
|
|
445
|
+
>
|
|
446
|
+
{skill.state === 'enabled' ? t('common.disable') : t('common.enable')}
|
|
447
|
+
</button>
|
|
448
|
+
</div>
|
|
449
|
+
<p className="text-[10px] text-[var(--muted)] line-clamp-2">{skill.description}</p>
|
|
450
|
+
{skill.tags?.length > 0 && (
|
|
451
|
+
<div className="flex gap-1 flex-wrap mt-1.5">
|
|
452
|
+
{skill.tags.slice(0, 4).map((tag, i) => (
|
|
453
|
+
<span key={i} className="text-[9px] bg-white/5 text-[var(--muted)] px-1.5 py-0.5 rounded">{tag}</span>
|
|
454
|
+
))}
|
|
455
|
+
</div>
|
|
456
|
+
)}
|
|
457
|
+
</div>
|
|
458
|
+
))}
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
))}
|
|
462
|
+
|
|
463
|
+
{skills.length === 0 && (
|
|
464
|
+
<div className="text-center py-12 text-[var(--muted)]">
|
|
465
|
+
<div className="text-4xl mb-3">📚</div>
|
|
466
|
+
<p className="text-sm">{t('systemSettings.skillDetail.noSkills')}</p>
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
|
|
472
|
+
{/* === KNOWLEDGE BASE SECTION === */}
|
|
473
|
+
{activeSection === 'knowledge' && (
|
|
474
|
+
<div className="space-y-4">
|
|
475
|
+
<div className="grid grid-cols-4 gap-4">
|
|
476
|
+
<StatCard label={t('systemSettings.kbStats.totalBases')} value={knowledge.stats?.totalBases || 0} />
|
|
477
|
+
<StatCard label={t('systemSettings.kbStats.enabled')} value={knowledge.stats?.enabledBases || 0} color="text-green-400" />
|
|
478
|
+
<StatCard label={t('systemSettings.kbStats.totalEntries')} value={knowledge.stats?.totalEntries || 0} />
|
|
479
|
+
<StatCard label={t('systemSettings.kbStats.globalBases')} value={knowledge.stats?.byType?.global || 0} />
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
{/* Search */}
|
|
483
|
+
<div className="card">
|
|
484
|
+
<div className="flex gap-2">
|
|
485
|
+
<input
|
|
486
|
+
className="input flex-1 text-sm"
|
|
487
|
+
placeholder={t('systemSettings.kbSearch.placeholder')}
|
|
488
|
+
value={kbSearchQuery}
|
|
489
|
+
onChange={e => setKbSearchQuery(e.target.value)}
|
|
490
|
+
onKeyDown={e => e.key === 'Enter' && handleKBSearch()}
|
|
491
|
+
/>
|
|
492
|
+
<button onClick={handleKBSearch} className="btn-primary text-sm">
|
|
493
|
+
🔍 {t('systemSettings.kbSearch.btn')}
|
|
494
|
+
</button>
|
|
495
|
+
</div>
|
|
496
|
+
{kbSearchResults && (
|
|
497
|
+
<div className="mt-3 space-y-2">
|
|
498
|
+
{kbSearchResults.length > 0 ? kbSearchResults.map((r, i) => (
|
|
499
|
+
<div key={i} className="p-2 rounded bg-white/5 text-sm">
|
|
500
|
+
<div className="flex items-center gap-2">
|
|
501
|
+
<span className="text-[10px] bg-blue-900/30 text-blue-400 px-1.5 py-0.5 rounded">{r.type}</span>
|
|
502
|
+
<span className="font-medium">{r.title}</span>
|
|
503
|
+
<span className="text-[10px] text-[var(--muted)] ml-auto">{r.knowledgeBaseName}</span>
|
|
504
|
+
</div>
|
|
505
|
+
<p className="text-[10px] text-[var(--muted)] mt-1 line-clamp-2">{r.content}</p>
|
|
506
|
+
</div>
|
|
507
|
+
)) : (
|
|
508
|
+
<p className="text-xs text-[var(--muted)]">{t('systemSettings.kbSearch.noResults')}</p>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
{/* Actions */}
|
|
515
|
+
<div className="flex justify-between items-center">
|
|
516
|
+
<h3 className="text-sm font-semibold">{t('systemSettings.kbDetail.listTitle')}</h3>
|
|
517
|
+
<button onClick={() => setShowCreateKB(!showCreateKB)} className="btn-primary text-xs">
|
|
518
|
+
{showCreateKB ? '✕ ' + t('common.cancel') : '+ ' + t('systemSettings.kbDetail.createKB')}
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
{/* Create KB Form */}
|
|
523
|
+
{showCreateKB && (
|
|
524
|
+
<div className="card space-y-3 animate-fade-in">
|
|
525
|
+
<h4 className="text-sm font-medium">{t('systemSettings.kbDetail.createKB')}</h4>
|
|
526
|
+
<input
|
|
527
|
+
className="input text-sm w-full"
|
|
528
|
+
placeholder={t('systemSettings.kbForm.name')}
|
|
529
|
+
value={newKB.name}
|
|
530
|
+
onChange={e => setNewKB({ ...newKB, name: e.target.value })}
|
|
531
|
+
/>
|
|
532
|
+
<input
|
|
533
|
+
className="input text-sm w-full"
|
|
534
|
+
placeholder={t('systemSettings.kbForm.description')}
|
|
535
|
+
value={newKB.description}
|
|
536
|
+
onChange={e => setNewKB({ ...newKB, description: e.target.value })}
|
|
537
|
+
/>
|
|
538
|
+
<select
|
|
539
|
+
className="input text-sm w-full"
|
|
540
|
+
value={newKB.type}
|
|
541
|
+
onChange={e => setNewKB({ ...newKB, type: e.target.value })}
|
|
542
|
+
>
|
|
543
|
+
<option value="global">{t('systemSettings.kbForm.typeGlobal')}</option>
|
|
544
|
+
<option value="department">{t('systemSettings.kbForm.typeDept')}</option>
|
|
545
|
+
<option value="agent">{t('systemSettings.kbForm.typeAgent')}</option>
|
|
546
|
+
</select>
|
|
547
|
+
<button onClick={handleCreateKB} className="btn-primary text-sm max-w-xs">
|
|
548
|
+
{t('systemSettings.kbDetail.createKB')}
|
|
549
|
+
</button>
|
|
550
|
+
</div>
|
|
551
|
+
)}
|
|
552
|
+
|
|
553
|
+
{/* KB List */}
|
|
554
|
+
{(knowledge.bases || []).length > 0 ? (
|
|
555
|
+
<div className="space-y-2">
|
|
556
|
+
{knowledge.bases.map(kb => (
|
|
557
|
+
<div key={kb.id} className="card">
|
|
558
|
+
<div className="flex items-center gap-3">
|
|
559
|
+
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${kb.enabled ? 'bg-green-500' : 'bg-gray-500'}`} />
|
|
560
|
+
<div className="flex-1 min-w-0">
|
|
561
|
+
<div className="flex items-center gap-2">
|
|
562
|
+
<span className="text-sm font-medium">{kb.name}</span>
|
|
563
|
+
<span className="text-[10px] bg-blue-900/20 text-blue-400 px-1.5 py-0.5 rounded">{kb.type}</span>
|
|
564
|
+
<span className="text-[10px] text-[var(--muted)]">{kb.entryCount} {t('systemSettings.kbStats.totalEntries').toLowerCase()}</span>
|
|
565
|
+
</div>
|
|
566
|
+
{kb.description && <p className="text-[10px] text-[var(--muted)] mt-0.5">{kb.description}</p>}
|
|
567
|
+
</div>
|
|
568
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
569
|
+
<button
|
|
570
|
+
onClick={() => { setShowAddEntry(showAddEntry === kb.id ? null : kb.id); setNewEntry({ title: '', content: '', entryType: 'note', tags: '' }); }}
|
|
571
|
+
className="btn-ghost text-[10px]"
|
|
572
|
+
title={t('systemSettings.kbDetail.addEntry')}
|
|
573
|
+
>➕</button>
|
|
574
|
+
<button
|
|
575
|
+
onClick={async () => { await manageKnowledge('toggle', { kbId: kb.id }); await refresh(); }}
|
|
576
|
+
className={`text-[10px] px-2 py-0.5 rounded-full ${kb.enabled ? 'bg-green-900/30 text-green-400' : 'bg-white/10 text-[var(--muted)]'}`}
|
|
577
|
+
>
|
|
578
|
+
{kb.enabled ? t('common.enable') : t('common.disable')}
|
|
579
|
+
</button>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{/* Add Entry Form */}
|
|
584
|
+
{showAddEntry === kb.id && (
|
|
585
|
+
<div className="mt-3 p-3 rounded-lg bg-white/5 space-y-2 animate-fade-in">
|
|
586
|
+
<input
|
|
587
|
+
className="input text-sm w-full"
|
|
588
|
+
placeholder={t('systemSettings.kbForm.entryTitle')}
|
|
589
|
+
value={newEntry.title}
|
|
590
|
+
onChange={e => setNewEntry({ ...newEntry, title: e.target.value })}
|
|
591
|
+
/>
|
|
592
|
+
<textarea
|
|
593
|
+
className="input text-sm w-full"
|
|
594
|
+
rows={3}
|
|
595
|
+
placeholder={t('systemSettings.kbForm.entryContent')}
|
|
596
|
+
value={newEntry.content}
|
|
597
|
+
onChange={e => setNewEntry({ ...newEntry, content: e.target.value })}
|
|
598
|
+
/>
|
|
599
|
+
<div className="flex gap-2">
|
|
600
|
+
<select
|
|
601
|
+
className="input text-sm flex-1"
|
|
602
|
+
value={newEntry.entryType}
|
|
603
|
+
onChange={e => setNewEntry({ ...newEntry, entryType: e.target.value })}
|
|
604
|
+
>
|
|
605
|
+
<option value="note">{t('systemSettings.kbEntryTypes.note')}</option>
|
|
606
|
+
<option value="fact">{t('systemSettings.kbEntryTypes.fact')}</option>
|
|
607
|
+
<option value="decision">{t('systemSettings.kbEntryTypes.decision')}</option>
|
|
608
|
+
<option value="procedure">{t('systemSettings.kbEntryTypes.procedure')}</option>
|
|
609
|
+
<option value="reference">{t('systemSettings.kbEntryTypes.reference')}</option>
|
|
610
|
+
<option value="faq">{t('systemSettings.kbEntryTypes.faq')}</option>
|
|
611
|
+
</select>
|
|
612
|
+
<input
|
|
613
|
+
className="input text-sm flex-1"
|
|
614
|
+
placeholder={t('systemSettings.kbForm.tags')}
|
|
615
|
+
value={newEntry.tags}
|
|
616
|
+
onChange={e => setNewEntry({ ...newEntry, tags: e.target.value })}
|
|
617
|
+
/>
|
|
618
|
+
</div>
|
|
619
|
+
<button onClick={handleAddEntry} className="btn-primary text-sm max-w-xs">
|
|
620
|
+
{t('systemSettings.kbDetail.addEntry')}
|
|
621
|
+
</button>
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
))}
|
|
626
|
+
</div>
|
|
627
|
+
) : (
|
|
628
|
+
<div className="text-center py-12 text-[var(--muted)]">
|
|
629
|
+
<div className="text-4xl mb-3">🧠</div>
|
|
630
|
+
<p className="text-sm">{t('systemSettings.kbDetail.noKB')}</p>
|
|
631
|
+
<p className="text-xs mt-1">{t('systemSettings.kbDetail.noKBHint')}</p>
|
|
632
|
+
</div>
|
|
633
|
+
)}
|
|
634
|
+
</div>
|
|
635
|
+
)}
|
|
636
|
+
|
|
637
|
+
{/* === HEALTH SECTION === */}
|
|
638
|
+
{activeSection === 'health' && systemStatus && (
|
|
639
|
+
<div className="space-y-4">
|
|
640
|
+
{/* Overview Grid */}
|
|
641
|
+
<div className="grid grid-cols-4 gap-4">
|
|
642
|
+
<StatCard icon="🛡️" label={t('systemSettings.cards.audit')} value={systemStatus.audit?.total || 0} sub={`${systemStatus.audit?.blocked || 0} ${t('systemSettings.auditStats.blocked')}`} />
|
|
643
|
+
<StatCard icon="🔀" label={t('systemSettings.cards.routing')} value={systemStatus.routing?.healthDashboard?.length || 0} sub={systemStatus.routing?.strategy || ''} />
|
|
644
|
+
<StatCard icon="🪝" label={t('systemSettings.cards.hooks')} value={systemStatus.hooks?.totalHandlers || 0} sub={`${systemStatus.hooks?.registeredKeys || 0} ${t('systemSettings.hookStats.eventKeys')}`} />
|
|
645
|
+
<StatCard icon="💬" label={t('systemSettings.cards.sessions')} value={systemStatus.sessions?.totalSessions || 0} sub={`${systemStatus.sessions?.totalMessages || 0} ${t('systemSettings.sessionStats.messages')}`} />
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
{/* Provider Health */}
|
|
649
|
+
{systemStatus.routing?.healthDashboard?.length > 0 && (
|
|
650
|
+
<div className="card">
|
|
651
|
+
<h3 className="text-sm font-semibold mb-3">{t('systemSettings.health.providerHealth')}</h3>
|
|
652
|
+
<div className="space-y-2">
|
|
653
|
+
{systemStatus.routing.healthDashboard.map((p, i) => (
|
|
654
|
+
<div key={i} className="flex items-center gap-3 p-2 rounded-lg bg-white/5">
|
|
655
|
+
<span className={`w-2 h-2 rounded-full ${
|
|
656
|
+
p.health === 'healthy' ? 'bg-green-500' : p.health === 'degraded' ? 'bg-yellow-500' : 'bg-red-500'
|
|
657
|
+
}`} />
|
|
658
|
+
<span className="text-sm flex-1">{p.id}</span>
|
|
659
|
+
<span className="text-xs text-[var(--muted)]">
|
|
660
|
+
{p.successRate !== undefined ? `${(p.successRate * 100).toFixed(0)}%` : '-'}
|
|
661
|
+
</span>
|
|
662
|
+
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
663
|
+
p.health === 'healthy' ? 'bg-green-900/30 text-green-400' :
|
|
664
|
+
p.health === 'degraded' ? 'bg-yellow-900/30 text-yellow-400' : 'bg-red-900/30 text-red-400'
|
|
665
|
+
}`}>{p.health}</span>
|
|
666
|
+
</div>
|
|
667
|
+
))}
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
)}
|
|
671
|
+
|
|
672
|
+
{/* Recent Audit Events */}
|
|
673
|
+
{systemStatus.recentAuditEvents?.length > 0 && (
|
|
674
|
+
<div className="card">
|
|
675
|
+
<h3 className="text-sm font-semibold mb-3">{t('systemSettings.health.recentAudit')}</h3>
|
|
676
|
+
<div className="space-y-1 max-h-60 overflow-auto">
|
|
677
|
+
{systemStatus.recentAuditEvents.slice(0, 15).map((evt, i) => (
|
|
678
|
+
<div key={i} className="flex items-center gap-3 text-xs py-1.5 px-2 rounded bg-white/5">
|
|
679
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ${
|
|
680
|
+
evt.level === 'critical' ? 'bg-red-500' : evt.level === 'warn' ? 'bg-yellow-500' : 'bg-blue-500'
|
|
681
|
+
}`} />
|
|
682
|
+
<span className="text-[var(--muted)] w-20 shrink-0">{evt.category}</span>
|
|
683
|
+
<span className="flex-1 truncate">{evt.action}</span>
|
|
684
|
+
{evt.blocked && <span className="text-red-400 shrink-0">🚫</span>}
|
|
685
|
+
</div>
|
|
686
|
+
))}
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
)}
|
|
690
|
+
</div>
|
|
691
|
+
)}
|
|
692
|
+
|
|
693
|
+
{/* === DANGER ZONE SECTION === */}
|
|
694
|
+
{activeSection === 'danger' && (
|
|
695
|
+
<div className="space-y-4">
|
|
696
|
+
{/* Warning banner */}
|
|
697
|
+
<div className="card bg-gradient-to-r from-red-900/20 to-orange-900/20 border-red-500/30">
|
|
698
|
+
<div className="flex items-start gap-3">
|
|
699
|
+
<span className="text-3xl">☢️</span>
|
|
700
|
+
<div>
|
|
701
|
+
<h3 className="font-semibold text-red-400">{t('systemSettings.dangerZone.title')}</h3>
|
|
702
|
+
<p className="text-sm text-[var(--muted)] mt-1">{t('systemSettings.dangerZone.subtitle')}</p>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
{/* Factory Reset card */}
|
|
708
|
+
<div className="card border-red-500/20">
|
|
709
|
+
<div className="flex items-center justify-between">
|
|
710
|
+
<div className="flex-1">
|
|
711
|
+
<h4 className="font-semibold text-red-400">{t('systemSettings.dangerZone.factoryReset')}</h4>
|
|
712
|
+
<p className="text-sm text-[var(--muted)] mt-1 max-w-xl">
|
|
713
|
+
{t('systemSettings.dangerZone.factoryResetDesc')}
|
|
714
|
+
</p>
|
|
715
|
+
</div>
|
|
716
|
+
<button
|
|
717
|
+
onClick={() => { setShowFactoryReset(true); setFactoryResetInput(''); }}
|
|
718
|
+
className="shrink-0 ml-4 px-4 py-2.5 rounded-lg bg-red-600/20 hover:bg-red-600/30 text-red-400 border border-red-500/30 hover:border-red-500/50 transition-all text-sm font-medium cursor-pointer"
|
|
719
|
+
>
|
|
720
|
+
{t('systemSettings.dangerZone.factoryResetBtn')}
|
|
721
|
+
</button>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
{/* Factory Reset confirmation modal */}
|
|
726
|
+
{showFactoryReset && (
|
|
727
|
+
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 !m-0" onClick={() => !factoryResetting && setShowFactoryReset(false)}>
|
|
728
|
+
<div className="card max-w-md w-full mx-4 space-y-4 border-red-500/30" onClick={e => e.stopPropagation()}>
|
|
729
|
+
<div className="flex items-center gap-3">
|
|
730
|
+
<span className="text-3xl">💀</span>
|
|
731
|
+
<h3 className="text-lg font-bold text-red-400">{t('systemSettings.dangerZone.factoryResetConfirm')}</h3>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
<p className="text-sm text-[var(--muted)]">
|
|
735
|
+
{t('systemSettings.dangerZone.factoryResetConfirmDesc')}
|
|
736
|
+
</p>
|
|
737
|
+
|
|
738
|
+
<ul className="space-y-1.5">
|
|
739
|
+
{t('systemSettings.dangerZone.factoryResetItems').map((item, i) => (
|
|
740
|
+
<li key={i} className="text-sm text-red-300/80 flex items-center gap-2">
|
|
741
|
+
<span className="w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" />
|
|
742
|
+
{item}
|
|
743
|
+
</li>
|
|
744
|
+
))}
|
|
745
|
+
</ul>
|
|
746
|
+
|
|
747
|
+
<div>
|
|
748
|
+
<label className="block text-sm mb-1.5 text-[var(--muted)]">
|
|
749
|
+
{t('systemSettings.dangerZone.factoryResetInput', { companyName: company?.name || 'Company' })}
|
|
750
|
+
</label>
|
|
751
|
+
<input
|
|
752
|
+
className="input w-full"
|
|
753
|
+
placeholder={t('systemSettings.dangerZone.factoryResetInputPlaceholder')}
|
|
754
|
+
value={factoryResetInput}
|
|
755
|
+
onChange={e => setFactoryResetInput(e.target.value)}
|
|
756
|
+
disabled={factoryResetting}
|
|
757
|
+
/>
|
|
758
|
+
</div>
|
|
759
|
+
|
|
760
|
+
<div className="flex gap-2">
|
|
761
|
+
<button
|
|
762
|
+
className="btn-secondary flex-1"
|
|
763
|
+
onClick={() => setShowFactoryReset(false)}
|
|
764
|
+
disabled={factoryResetting}
|
|
765
|
+
>
|
|
766
|
+
{t('common.cancel')}
|
|
767
|
+
</button>
|
|
768
|
+
<button
|
|
769
|
+
className="flex-1 px-4 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white font-medium transition-all disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
|
770
|
+
disabled={factoryResetInput !== (company?.name || 'Company') || factoryResetting}
|
|
771
|
+
onClick={async () => {
|
|
772
|
+
setFactoryResetting(true);
|
|
773
|
+
try {
|
|
774
|
+
await factoryReset();
|
|
775
|
+
setShowFactoryReset(false);
|
|
776
|
+
// Reload the page to go back to setup wizard
|
|
777
|
+
setTimeout(() => window.location.reload(), 800);
|
|
778
|
+
} catch (e) {
|
|
779
|
+
setFactoryResetting(false);
|
|
780
|
+
}
|
|
781
|
+
}}
|
|
782
|
+
>
|
|
783
|
+
{factoryResetting ? t('systemSettings.dangerZone.factoryResetting') : t('systemSettings.dangerZone.factoryResetExecute')}
|
|
784
|
+
</button>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
)}
|
|
789
|
+
</div>
|
|
790
|
+
)}
|
|
791
|
+
</div>
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ============================================================================
|
|
796
|
+
// Sub-components
|
|
797
|
+
// ============================================================================
|
|
798
|
+
|
|
799
|
+
function StatCard({ icon, label, value, sub, color = '' }) {
|
|
800
|
+
return (
|
|
801
|
+
<div className="card text-center">
|
|
802
|
+
{icon && <div className="text-xl mb-1">{icon}</div>}
|
|
803
|
+
<div className={`text-xl font-bold ${color || 'text-white'}`}>{value}</div>
|
|
804
|
+
<div className="text-[10px] text-[var(--muted)] mt-1">{label}</div>
|
|
805
|
+
{sub && <div className="text-[10px] text-[var(--muted)]">{sub}</div>}
|
|
806
|
+
</div>
|
|
807
|
+
);
|
|
808
|
+
}
|