opc-agent 4.0.35 → 4.0.37
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/dist/core/runtime.js +0 -14
- package/dist/studio-ui/index.html +31 -21
- package/package.json +1 -1
- package/serve-studio.js +13 -0
- package/src/core/runtime.ts +0 -14
- package/src/studio-ui/index.html +31 -21
- package/srv-err.txt +0 -0
- package/srv-out.txt +1 -0
- package/test-studio3.js +75 -0
- package/test-studio4.js +41 -0
- package/tmp-sc.js +1716 -0
package/tmp-sc.js
ADDED
|
@@ -0,0 +1,1716 @@
|
|
|
1
|
+
|
|
2
|
+
// === Debug: catch all JS errors ===
|
|
3
|
+
window.onerror = function(msg, url, line, col, err) {
|
|
4
|
+
console.error('JS ERROR:', msg, 'at line', line, ':', col);
|
|
5
|
+
const d = document.createElement('div');
|
|
6
|
+
d.style.cssText = 'position:fixed;bottom:0;left:0;right:0;background:red;color:white;padding:8px;z-index:9999;font-size:12px;';
|
|
7
|
+
d.textContent = 'JS Error: ' + msg + ' (line ' + line + ')';
|
|
8
|
+
document.body.appendChild(d);
|
|
9
|
+
};
|
|
10
|
+
// === State ===
|
|
11
|
+
let templates = [];
|
|
12
|
+
let industries = [];
|
|
13
|
+
let agents = [];
|
|
14
|
+
let selectedTemplate = null;
|
|
15
|
+
let currentAgent = null;
|
|
16
|
+
let chatMessages = [];
|
|
17
|
+
let wizardStep = 1;
|
|
18
|
+
let selectedIndustry = '';
|
|
19
|
+
let deleteTargetId = null;
|
|
20
|
+
|
|
21
|
+
const API = '';
|
|
22
|
+
|
|
23
|
+
// === Init ===
|
|
24
|
+
async function init() {
|
|
25
|
+
await Promise.all([loadTemplates(), loadAgents()]);
|
|
26
|
+
loadSidebarGroups();
|
|
27
|
+
handleRoute();
|
|
28
|
+
window.addEventListener('popstate', handleRoute);
|
|
29
|
+
checkFirstRun();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleRoute() {
|
|
33
|
+
const path = location.hash.slice(1) || '/dashboard';
|
|
34
|
+
const parts = path.split('/').filter(Boolean);
|
|
35
|
+
if (parts[0] === 'chat' && parts[1]) {
|
|
36
|
+
openChat(parts[1]);
|
|
37
|
+
} else if (parts[0] === 'agent' && parts[1]) {
|
|
38
|
+
loadSidebarAgents().then(() => navigateToAgent(parts[1]));
|
|
39
|
+
} else if (parts[0] === 'settings') {
|
|
40
|
+
if (parts[1]) currentSettingsTab = parts[1];
|
|
41
|
+
navigate('settings');
|
|
42
|
+
} else if (parts[0] === 'memory' && parts[1]) {
|
|
43
|
+
openMemoryPage(parts[1]);
|
|
44
|
+
} else if (parts[0] === 'create') {
|
|
45
|
+
if (parts[1]) {
|
|
46
|
+
// pre-select template
|
|
47
|
+
selectedTemplate = templates.find(t => t.id === parts[1]) || null;
|
|
48
|
+
if (selectedTemplate) {
|
|
49
|
+
wizardStep = 2;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
showPage('create');
|
|
53
|
+
renderWizard();
|
|
54
|
+
} else {
|
|
55
|
+
navigate(parts[0] || 'dashboard');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// === API ===
|
|
60
|
+
async function loadTemplates() {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(`${API}/api/templates`);
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
templates = data.templates || [];
|
|
65
|
+
industries = data.industries || [];
|
|
66
|
+
renderIndustryChips();
|
|
67
|
+
renderTemplates();
|
|
68
|
+
} catch(e) { console.error('Failed to load templates:', e); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function loadAgents() {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`${API}/api/agents`);
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
agents = data.agents || [];
|
|
76
|
+
renderAgents();
|
|
77
|
+
} catch(e) { console.error('Failed to load agents:', e); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// === Sidebar Agents ===
|
|
81
|
+
let selectedAgentId = null;
|
|
82
|
+
|
|
83
|
+
async function loadSidebarAgents() {
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch('/api/agents');
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
const agents = data.agents || data || [];
|
|
88
|
+
window._sidebarAgents = agents;
|
|
89
|
+
const container = document.getElementById('sidebar-agent-list');
|
|
90
|
+
if (!agents.length) {
|
|
91
|
+
container.innerHTML = '<div style="padding: 12px 16px; color: var(--text-dim); font-size: 13px;">暂无 Agent</div>';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
container.innerHTML = agents.map(a => {
|
|
95
|
+
const status = (a.status || 'offline').toLowerCase();
|
|
96
|
+
const icon = a.emoji || a.icon || '🤖';
|
|
97
|
+
const name = a.name || a.id;
|
|
98
|
+
return `<div class="agent-list-item${selectedAgentId === a.id ? ' active' : ''}" data-agent-id="${a.id}" onclick="navigateToAgent('${a.id}')">
|
|
99
|
+
<span class="agent-icon">${icon}</span>
|
|
100
|
+
<span class="agent-name">${name}</span>
|
|
101
|
+
<span class="status-dot ${status}"></span>
|
|
102
|
+
</div>`;
|
|
103
|
+
}).join('');
|
|
104
|
+
} catch(e) {
|
|
105
|
+
console.error('Failed to load sidebar agents:', e);
|
|
106
|
+
const container = document.getElementById('sidebar-agent-list');
|
|
107
|
+
if (container) container.innerHTML = '<div style="padding: 12px 16px; color: var(--text-dim); font-size: 13px;">加载失败</div>';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Collaboration Groups ---
|
|
112
|
+
let selectedPattern = 'debate';
|
|
113
|
+
function selectPattern(pat) {
|
|
114
|
+
selectedPattern = pat;
|
|
115
|
+
document.querySelectorAll('.pattern-card').forEach(c => c.classList.remove('active'));
|
|
116
|
+
const el = document.getElementById('pat-' + pat);
|
|
117
|
+
if (el) el.classList.add('active');
|
|
118
|
+
}
|
|
119
|
+
async function loadGroupAgentSelect() {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch('/api/agents');
|
|
122
|
+
const data = await res.json();
|
|
123
|
+
const agents = data.agents || data || [];
|
|
124
|
+
const container = document.getElementById('group-agent-select');
|
|
125
|
+
if (!agents.length) {
|
|
126
|
+
container.innerHTML = '<p style="color:var(--text-muted);font-size:13px;">请先创建 Agent,再拉入群组</p>';
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
container.innerHTML = agents.map(a => `<label style="display:flex;align-items:center;gap:8px;padding:6px 0;cursor:pointer;"><input type="checkbox" class="group-agent-cb" value="${a.id}"> <span>${a.templateIcon || a.icon || '🤖'}</span> <span style="font-size:14px;">${a.name}</span></label>`).join('');
|
|
130
|
+
} catch(e) { console.error('loadGroupAgentSelect error', e); }
|
|
131
|
+
}
|
|
132
|
+
async function createGroup() {
|
|
133
|
+
const name = document.getElementById('group-name').value.trim();
|
|
134
|
+
if (!name) { alert('请输入群组名称'); return; }
|
|
135
|
+
const members = [...document.querySelectorAll('.group-agent-cb:checked')].map(cb => cb.value);
|
|
136
|
+
if (members.length < 2) { alert('至少选择 2 个 Agent'); return; }
|
|
137
|
+
// TODO: POST to /api/groups
|
|
138
|
+
alert('群组 "' + name + '" 创建成功!模式: ' + selectedPattern + ', 成员: ' + members.length + ' 个 Agent');
|
|
139
|
+
navigate('dashboard');
|
|
140
|
+
}
|
|
141
|
+
async function loadSidebarGroups() {
|
|
142
|
+
// TODO: fetch /api/groups and render
|
|
143
|
+
const container = document.getElementById('groups-list');
|
|
144
|
+
if (container) container.innerHTML = '<div style="padding:6px 16px;color:var(--text-dim);font-size:12px;">暂无群组</div>';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function navigateToAgent(agentId) {
|
|
148
|
+
selectedAgentId = agentId;
|
|
149
|
+
// Update sidebar active state
|
|
150
|
+
document.querySelectorAll('.agent-list-item').forEach(el => el.classList.remove('active'));
|
|
151
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
152
|
+
const item = document.querySelector(`.agent-list-item[data-agent-id="${agentId}"]`);
|
|
153
|
+
if (item) item.classList.add('active');
|
|
154
|
+
|
|
155
|
+
// Find agent data
|
|
156
|
+
const agent = (window._sidebarAgents || []).find(a => a.id === agentId) || { id: agentId, name: agentId };
|
|
157
|
+
document.getElementById('agent-detail-icon').textContent = agent.emoji || agent.icon || '🤖';
|
|
158
|
+
document.getElementById('agent-detail-name').textContent = agent.name || agentId;
|
|
159
|
+
const statusDot = document.getElementById('agent-detail-status');
|
|
160
|
+
const status = (agent.status || 'offline').toLowerCase();
|
|
161
|
+
statusDot.className = 'status-dot ' + status;
|
|
162
|
+
|
|
163
|
+
// Reset to chat view
|
|
164
|
+
document.getElementById('agent-chat-view').style.display = '';
|
|
165
|
+
document.getElementById('agent-settings-view').style.display = 'none';
|
|
166
|
+
document.getElementById('agent-detail-toggle').classList.remove('active');
|
|
167
|
+
document.getElementById('agent-chat-messages').innerHTML = document.querySelector('.agent-chat-welcome').outerHTML || '<div class="agent-chat-welcome"><div style="font-size:48px;margin-bottom:16px">💬</div><div style="font-size:18px;font-weight:600;margin-bottom:8px">开始对话</div><div style="color:var(--text-muted);font-size:14px">向你的 Agent 发送第一条消息</div></div>';
|
|
168
|
+
document.getElementById('agent-chat-input').value = '';
|
|
169
|
+
|
|
170
|
+
// Show agent detail page
|
|
171
|
+
document.querySelectorAll('.page, .chat-container').forEach(p => { p.classList.remove('active'); p.style.display = ''; });
|
|
172
|
+
document.getElementById('page-agent-detail').classList.add('active');
|
|
173
|
+
location.hash = `/agent/${agentId}`;
|
|
174
|
+
toggleSidebar(false);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toggleAgentSettings() {
|
|
178
|
+
const chatView = document.getElementById('agent-chat-view');
|
|
179
|
+
const settingsView = document.getElementById('agent-settings-view');
|
|
180
|
+
const toggleBtn = document.getElementById('agent-detail-toggle');
|
|
181
|
+
const showSettings = chatView.style.display !== 'none';
|
|
182
|
+
chatView.style.display = showSettings ? 'none' : '';
|
|
183
|
+
settingsView.style.display = showSettings ? '' : 'none';
|
|
184
|
+
toggleBtn.classList.toggle('active', showSettings);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function switchAgentTab(tab) {
|
|
188
|
+
document.querySelectorAll('.agent-tab').forEach(t => t.classList.remove('active'));
|
|
189
|
+
document.querySelector(`.agent-tab[data-atab="${tab}"]`)?.classList.add('active');
|
|
190
|
+
document.querySelectorAll('.agent-tab-panel').forEach(p => p.classList.remove('active'));
|
|
191
|
+
document.getElementById(`atab-${tab}`)?.classList.add('active');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let agentChatHistory = [];
|
|
195
|
+
|
|
196
|
+
async function sendAgentChat() {
|
|
197
|
+
const input = document.getElementById('agent-chat-input');
|
|
198
|
+
const msg = input.value.trim();
|
|
199
|
+
if (!msg || !selectedAgentId) return;
|
|
200
|
+
input.value = '';
|
|
201
|
+
input.style.height = 'auto';
|
|
202
|
+
|
|
203
|
+
const messagesEl = document.getElementById('agent-chat-messages');
|
|
204
|
+
// Remove welcome screen
|
|
205
|
+
const welcome = messagesEl.querySelector('.agent-chat-welcome');
|
|
206
|
+
if (welcome) welcome.remove();
|
|
207
|
+
|
|
208
|
+
// Add user message
|
|
209
|
+
const userDiv = document.createElement('div');
|
|
210
|
+
userDiv.className = 'agent-chat-msg user';
|
|
211
|
+
userDiv.textContent = msg;
|
|
212
|
+
messagesEl.appendChild(userDiv);
|
|
213
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
214
|
+
|
|
215
|
+
agentChatHistory.push({ role: 'user', content: msg });
|
|
216
|
+
|
|
217
|
+
// Send to API
|
|
218
|
+
try {
|
|
219
|
+
const res = await fetch(`/api/agents/${selectedAgentId}/chat`, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'Content-Type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({ message: msg, history: agentChatHistory })
|
|
223
|
+
});
|
|
224
|
+
const data = await res.json();
|
|
225
|
+
const reply = data.reply || data.message || data.content || JSON.stringify(data);
|
|
226
|
+
const assistantDiv = document.createElement('div');
|
|
227
|
+
assistantDiv.className = 'agent-chat-msg assistant';
|
|
228
|
+
assistantDiv.textContent = reply;
|
|
229
|
+
messagesEl.appendChild(assistantDiv);
|
|
230
|
+
agentChatHistory.push({ role: 'assistant', content: reply });
|
|
231
|
+
} catch(e) {
|
|
232
|
+
const errDiv = document.createElement('div');
|
|
233
|
+
errDiv.className = 'agent-chat-msg assistant';
|
|
234
|
+
errDiv.style.borderColor = 'var(--red)';
|
|
235
|
+
errDiv.textContent = `⚠️ 发送失败: ${e.message}`;
|
|
236
|
+
messagesEl.appendChild(errDiv);
|
|
237
|
+
}
|
|
238
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function handleAgentChatKey(e) {
|
|
242
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAgentChat(); }
|
|
243
|
+
// Auto-resize textarea
|
|
244
|
+
e.target.style.height = 'auto';
|
|
245
|
+
e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// === Navigation ===
|
|
249
|
+
function navigate(page) {
|
|
250
|
+
document.querySelectorAll('.page, .chat-container').forEach(p => { p.classList.remove('active'); p.style.display = ''; });
|
|
251
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
252
|
+
const navItem = document.querySelector(`.nav-item[data-page="${page}"]`);
|
|
253
|
+
if (navItem) navItem.classList.add('active');
|
|
254
|
+
|
|
255
|
+
if (page === 'dashboard') { loadAgents(); loadHealthDashboard(); loadSidebarAgents(); }
|
|
256
|
+
if (page === 'create') { renderWizard(); renderWizardTemplates(); }
|
|
257
|
+
if (page === 'settings') { showSettings(currentSettingsTab || 'models'); }
|
|
258
|
+
if (page === 'global-runtime') { currentSettingsTab='status'; showSettings('status'); showPage('settings'); return; }
|
|
259
|
+
if (page === 'global-models') { currentSettingsTab='models'; showSettings('models'); showPage('settings'); return; }
|
|
260
|
+
if (page === 'global-memory') { currentSettingsTab='memory'; showSettings('memory'); showPage('settings'); return; }
|
|
261
|
+
if (page === 'global-templates') { navigate('templates'); return; }
|
|
262
|
+
if (page === 'create-group') { loadGroupAgentSelect(); }
|
|
263
|
+
if (page === 'schedules') { loadSchedules(); }
|
|
264
|
+
if (page === 'skills') { loadSkillsMarketplace(); }
|
|
265
|
+
|
|
266
|
+
showPage(page);
|
|
267
|
+
location.hash = `/${page}`;
|
|
268
|
+
toggleSidebar(false);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function showPage(page) {
|
|
272
|
+
document.querySelectorAll('.page, .chat-container').forEach(p => { p.classList.remove('active'); });
|
|
273
|
+
const el = document.getElementById(`page-${page}`);
|
|
274
|
+
if (el) el.classList.add('active');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function toggleSidebar(open) {
|
|
278
|
+
document.querySelector('.sidebar').classList.toggle('open', open);
|
|
279
|
+
document.querySelector('.sidebar-overlay').classList.toggle('show', open);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// === Templates Rendering ===
|
|
283
|
+
function renderIndustryChips() {
|
|
284
|
+
const html = `<span class="chip active" onclick="filterByIndustry('')">All</span>` +
|
|
285
|
+
industries.map(i => `<span class="chip" onclick="filterByIndustry('${i.id}')">${i.nameZh} ${i.name}</span>`).join('');
|
|
286
|
+
document.getElementById('industry-chips').innerHTML = html;
|
|
287
|
+
document.getElementById('wizard-industry-chips').innerHTML = html.replace(/filterByIndustry/g, 'filterWizardByIndustry');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function filterByIndustry(id) {
|
|
291
|
+
selectedIndustry = id;
|
|
292
|
+
document.querySelectorAll('#industry-chips .chip').forEach(c => c.classList.remove('active'));
|
|
293
|
+
event.target.classList.add('active');
|
|
294
|
+
renderTemplates();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function filterWizardByIndustry(id) {
|
|
298
|
+
selectedIndustry = id;
|
|
299
|
+
document.querySelectorAll('#wizard-industry-chips .chip').forEach(c => c.classList.remove('active'));
|
|
300
|
+
event.target.classList.add('active');
|
|
301
|
+
renderWizardTemplates();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function filterTemplates() {
|
|
305
|
+
renderTemplates();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// === Skills Marketplace ===
|
|
309
|
+
let allSkills = [];
|
|
310
|
+
let selectedSkillCategory = '';
|
|
311
|
+
const SKILL_CATEGORIES = [
|
|
312
|
+
{ id: '', label: 'All', labelZh: '全部' },
|
|
313
|
+
{ id: 'productivity', label: 'Productivity', labelZh: '效率' },
|
|
314
|
+
{ id: 'knowledge', label: 'Knowledge', labelZh: '知识' },
|
|
315
|
+
{ id: 'creative', label: 'Creative', labelZh: '创作' },
|
|
316
|
+
{ id: 'developer', label: 'Developer', labelZh: '开发' },
|
|
317
|
+
{ id: 'lifestyle', label: 'Lifestyle', labelZh: '生活' },
|
|
318
|
+
{ id: 'business', label: 'Business', labelZh: '业务' },
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
async function loadSkillsMarketplace() {
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch('/api/skills/marketplace');
|
|
324
|
+
allSkills = await res.json();
|
|
325
|
+
} catch(e) { console.error('Failed to load skills:', e); allSkills = []; }
|
|
326
|
+
renderSkillCategoryChips();
|
|
327
|
+
renderSkills();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function renderSkillCategoryChips() {
|
|
331
|
+
const el = document.getElementById('skill-category-chips');
|
|
332
|
+
if (!el) return;
|
|
333
|
+
el.innerHTML = SKILL_CATEGORIES.map(c =>
|
|
334
|
+
`<span class="chip ${selectedSkillCategory === c.id ? 'active' : ''}" onclick="selectSkillCategory('${c.id}')">${c.labelZh}</span>`
|
|
335
|
+
).join('');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function selectSkillCategory(cat) {
|
|
339
|
+
selectedSkillCategory = cat;
|
|
340
|
+
renderSkillCategoryChips();
|
|
341
|
+
renderSkills();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function filterSkills() {
|
|
345
|
+
renderSkills();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function renderSkills() {
|
|
349
|
+
const q = (document.getElementById('skills-search')?.value || '').toLowerCase();
|
|
350
|
+
let filtered = allSkills;
|
|
351
|
+
if (selectedSkillCategory) {
|
|
352
|
+
filtered = filtered.filter(s => s.category === selectedSkillCategory);
|
|
353
|
+
}
|
|
354
|
+
if (q) {
|
|
355
|
+
filtered = filtered.filter(s =>
|
|
356
|
+
s.name.toLowerCase().includes(q) || s.nameZh.includes(q) ||
|
|
357
|
+
s.description.toLowerCase().includes(q) || s.descriptionZh.includes(q)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const grid = document.getElementById('skills-grid');
|
|
361
|
+
if (!grid) return;
|
|
362
|
+
grid.innerHTML = filtered.map(s => `
|
|
363
|
+
<div class="card" style="cursor:default;position:relative;">
|
|
364
|
+
<div style="font-size:36px;margin-bottom:8px;">${s.icon}</div>
|
|
365
|
+
<div style="font-weight:600;font-size:15px;">${s.nameZh}</div>
|
|
366
|
+
<div style="font-size:12px;color:var(--text-dim);margin-bottom:4px;">${s.name}</div>
|
|
367
|
+
<div style="font-size:13px;color:var(--text-muted);margin-bottom:12px;line-height:1.4;">${s.descriptionZh}</div>
|
|
368
|
+
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
|
|
369
|
+
${s.tools.slice(0,3).map(t => `<span style="font-size:11px;padding:2px 6px;background:var(--bg-hover);border-radius:4px;color:var(--text-dim);">${t}</span>`).join('')}
|
|
370
|
+
${s.tools.length > 3 ? `<span style="font-size:11px;color:var(--text-dim);">+${s.tools.length-3}</span>` : ''}
|
|
371
|
+
</div>
|
|
372
|
+
${s.installed
|
|
373
|
+
? `<button class="btn" style="width:100%;background:var(--bg-hover);color:var(--text-muted);cursor:pointer;" onclick="uninstallSkill('${s.id}',this)">✓ Installed</button>`
|
|
374
|
+
: `<button class="btn btn-primary" style="width:100%;" onclick="installSkill('${s.id}',this)">Install</button>`
|
|
375
|
+
}
|
|
376
|
+
</div>
|
|
377
|
+
`).join('');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function installSkill(id, btn) {
|
|
381
|
+
btn.disabled = true; btn.textContent = 'Installing...';
|
|
382
|
+
try {
|
|
383
|
+
const res = await fetch(`/api/skills/marketplace/${id}/install`, { method: 'POST' });
|
|
384
|
+
const data = await res.json();
|
|
385
|
+
if (data.success) {
|
|
386
|
+
const skill = allSkills.find(s => s.id === id);
|
|
387
|
+
if (skill) skill.installed = true;
|
|
388
|
+
renderSkills();
|
|
389
|
+
}
|
|
390
|
+
} catch(e) { console.error(e); btn.disabled = false; btn.textContent = 'Install'; }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function uninstallSkill(id, btn) {
|
|
394
|
+
btn.disabled = true; btn.textContent = 'Removing...';
|
|
395
|
+
try {
|
|
396
|
+
const res = await fetch(`/api/skills/marketplace/${id}/uninstall`, { method: 'DELETE' });
|
|
397
|
+
const data = await res.json();
|
|
398
|
+
if (data.success) {
|
|
399
|
+
const skill = allSkills.find(s => s.id === id);
|
|
400
|
+
if (skill) skill.installed = false;
|
|
401
|
+
renderSkills();
|
|
402
|
+
}
|
|
403
|
+
} catch(e) { console.error(e); btn.disabled = false; btn.textContent = '✓ Installed'; }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function filterWizardTemplates() {
|
|
407
|
+
renderWizardTemplates();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function getFilteredTemplates(searchId) {
|
|
411
|
+
const q = (document.getElementById(searchId)?.value || '').toLowerCase();
|
|
412
|
+
return templates.filter(t => {
|
|
413
|
+
if (selectedIndustry && t.industry !== selectedIndustry) return false;
|
|
414
|
+
if (q && !t.name.toLowerCase().includes(q) && !t.nameZh.includes(q) && !t.description.toLowerCase().includes(q)) return false;
|
|
415
|
+
return true;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function renderTemplates() {
|
|
420
|
+
const filtered = getFilteredTemplates('tpl-search');
|
|
421
|
+
document.getElementById('templates-grid').innerHTML = filtered.map(t => `
|
|
422
|
+
<div class="card tpl-card" onclick="selectTemplateAndCreate('${t.id}')">
|
|
423
|
+
<div class="tpl-icon">${t.icon}</div>
|
|
424
|
+
<div class="tpl-name">${t.name}</div>
|
|
425
|
+
<div style="font-size:13px;color:var(--text-dim);margin-bottom:6px;">${t.nameZh}</div>
|
|
426
|
+
<div class="tpl-desc">${t.description}</div>
|
|
427
|
+
<div class="tpl-tags">
|
|
428
|
+
<span class="tpl-tag">${t.industryZh}</span>
|
|
429
|
+
${t.tags.map(tag => `<span class="tpl-tag">${tag}</span>`).join('')}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
`).join('');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function renderWizardTemplates() {
|
|
436
|
+
const filtered = getFilteredTemplates('wizard-tpl-search');
|
|
437
|
+
document.getElementById('wizard-tpl-grid').innerHTML = filtered.map(t => `
|
|
438
|
+
<div class="card tpl-card ${selectedTemplate?.id === t.id ? 'selected' : ''}" onclick="selectWizardTemplate('${t.id}')"
|
|
439
|
+
style="${selectedTemplate?.id === t.id ? 'border-color:var(--accent);background:var(--accent-light);' : ''}">
|
|
440
|
+
<div class="tpl-icon">${t.icon}</div>
|
|
441
|
+
<div class="tpl-name">${t.name}</div>
|
|
442
|
+
<div style="font-size:12px;color:var(--text-dim);">${t.nameZh} · ${t.industryZh}</div>
|
|
443
|
+
</div>
|
|
444
|
+
`).join('');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function selectTemplateAndCreate(id) {
|
|
448
|
+
selectedTemplate = templates.find(t => t.id === id);
|
|
449
|
+
wizardStep = 2;
|
|
450
|
+
navigate('create');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function selectWizardTemplate(id) {
|
|
454
|
+
selectedTemplate = templates.find(t => t.id === id);
|
|
455
|
+
renderWizardTemplates();
|
|
456
|
+
// Auto-advance after selection
|
|
457
|
+
setTimeout(() => wizardNext(), 300);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// === Wizard ===
|
|
461
|
+
function renderWizard() {
|
|
462
|
+
for (let i = 1; i <= 3; i++) {
|
|
463
|
+
const ws = document.getElementById(`ws-${i}`);
|
|
464
|
+
const wp = document.getElementById(`wp-${i}`);
|
|
465
|
+
ws.className = 'wizard-step' + (i < wizardStep ? ' done' : i === wizardStep ? ' active' : '');
|
|
466
|
+
wp.className = 'wizard-panel' + (i === wizardStep ? ' active' : '');
|
|
467
|
+
}
|
|
468
|
+
if (wizardStep === 2 && selectedTemplate) {
|
|
469
|
+
document.getElementById('agent-name').placeholder = selectedTemplate.name;
|
|
470
|
+
document.getElementById('agent-model').value = selectedTemplate.suggestedModel;
|
|
471
|
+
}
|
|
472
|
+
if (wizardStep === 3) {
|
|
473
|
+
renderConfirmCard();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function wizardNext() {
|
|
478
|
+
if (wizardStep === 1 && !selectedTemplate) { alert('Please select a template first'); return; }
|
|
479
|
+
if (wizardStep < 3) { wizardStep++; renderWizard(); }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function wizardBack() {
|
|
483
|
+
if (wizardStep > 1) { wizardStep--; renderWizard(); }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function renderConfirmCard() {
|
|
487
|
+
const name = document.getElementById('agent-name').value || selectedTemplate?.name || 'My Agent';
|
|
488
|
+
const model = document.getElementById('agent-model').value;
|
|
489
|
+
const lang = document.getElementById('agent-lang').selectedOptions[0]?.text || 'English';
|
|
490
|
+
document.getElementById('confirm-card').innerHTML = `
|
|
491
|
+
<div style="display:flex;align-items:center;gap:16px;margin-bottom:16px;">
|
|
492
|
+
<span style="font-size:48px;">${selectedTemplate?.icon || '🤖'}</span>
|
|
493
|
+
<div>
|
|
494
|
+
<div style="font-size:20px;font-weight:700;">${name}</div>
|
|
495
|
+
<div style="color:var(--text-muted);font-size:14px;">Based on: ${selectedTemplate?.name || 'Custom'} (${selectedTemplate?.nameZh || ''})</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:14px;">
|
|
499
|
+
<div><span style="color:var(--text-dim);">Model:</span> ${model}</div>
|
|
500
|
+
<div><span style="color:var(--text-dim);">Language:</span> ${lang}</div>
|
|
501
|
+
<div style="grid-column:span 2;"><span style="color:var(--text-dim);">Industry:</span> ${selectedTemplate?.industryZh || ''} (${selectedTemplate?.industry || ''})</div>
|
|
502
|
+
</div>
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function createAgent() {
|
|
507
|
+
const btn = document.getElementById('create-btn');
|
|
508
|
+
btn.textContent = '⏳ Creating...';
|
|
509
|
+
btn.disabled = true;
|
|
510
|
+
try {
|
|
511
|
+
const res = await fetch(`${API}/api/agents`, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
514
|
+
body: JSON.stringify({
|
|
515
|
+
name: document.getElementById('agent-name').value || selectedTemplate?.name,
|
|
516
|
+
templateId: selectedTemplate?.id,
|
|
517
|
+
description: document.getElementById('agent-desc').value,
|
|
518
|
+
model: document.getElementById('agent-model').value,
|
|
519
|
+
language: document.getElementById('agent-lang').value,
|
|
520
|
+
}),
|
|
521
|
+
});
|
|
522
|
+
const agent = await res.json();
|
|
523
|
+
// Reset wizard
|
|
524
|
+
wizardStep = 1;
|
|
525
|
+
selectedTemplate = null;
|
|
526
|
+
document.getElementById('agent-name').value = '';
|
|
527
|
+
document.getElementById('agent-desc').value = '';
|
|
528
|
+
// Navigate to chat
|
|
529
|
+
openChat(agent.id);
|
|
530
|
+
} catch(e) {
|
|
531
|
+
alert('Failed to create agent: ' + e.message);
|
|
532
|
+
}
|
|
533
|
+
btn.textContent = '🚀 Create Agent';
|
|
534
|
+
btn.disabled = false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// === Dashboard ===
|
|
538
|
+
function renderAgents() {
|
|
539
|
+
if (agents.length === 0) {
|
|
540
|
+
document.getElementById('agents-list').style.display = 'none';
|
|
541
|
+
document.getElementById('agents-empty').style.display = 'block';
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
document.getElementById('agents-list').style.display = '';
|
|
545
|
+
document.getElementById('agents-empty').style.display = 'none';
|
|
546
|
+
document.getElementById('agents-list').innerHTML = agents.map(a => {
|
|
547
|
+
const timeAgo = getTimeAgo(a.lastActive || a.created);
|
|
548
|
+
return `
|
|
549
|
+
<div class="card agent-card" onclick="openChat('${a.id}')">
|
|
550
|
+
<div class="agent-actions">
|
|
551
|
+
<button onclick="event.stopPropagation();openDeleteDialog('${a.id}')">🗑️</button>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="agent-icon">${a.templateIcon || '🤖'}</div>
|
|
554
|
+
<div class="agent-name">${a.name}</div>
|
|
555
|
+
<div class="agent-template">${a.templateName || 'Custom'}</div>
|
|
556
|
+
<div class="agent-stats">
|
|
557
|
+
<span>💬 ${a.messageCount || 0} messages</span>
|
|
558
|
+
<span>⏰ ${timeAgo}</span>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
`;
|
|
562
|
+
}).join('');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function getTimeAgo(dateStr) {
|
|
566
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
567
|
+
const mins = Math.floor(diff / 60000);
|
|
568
|
+
if (mins < 1) return 'just now';
|
|
569
|
+
if (mins < 60) return `${mins}m ago`;
|
|
570
|
+
const hours = Math.floor(mins / 60);
|
|
571
|
+
if (hours < 24) return `${hours}h ago`;
|
|
572
|
+
const days = Math.floor(hours / 24);
|
|
573
|
+
return `${days}d ago`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// === Delete ===
|
|
577
|
+
function openDeleteDialog(id) { deleteTargetId = id; document.getElementById('delete-dialog').classList.add('show'); }
|
|
578
|
+
function closeDeleteDialog() { deleteTargetId = null; document.getElementById('delete-dialog').classList.remove('show'); }
|
|
579
|
+
async function confirmDelete() {
|
|
580
|
+
if (!deleteTargetId) return;
|
|
581
|
+
await fetch(`${API}/api/agents/${deleteTargetId}`, { method: 'DELETE' });
|
|
582
|
+
closeDeleteDialog();
|
|
583
|
+
loadAgents();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// === Chat ===
|
|
587
|
+
async function openLastChat() {
|
|
588
|
+
if (currentAgent) { openChat(currentAgent.id); return; }
|
|
589
|
+
const agentsRes = await fetch(`${API}/api/agents`).catch(() => null);
|
|
590
|
+
if (agentsRes) {
|
|
591
|
+
const data = await agentsRes.json().catch(() => ({}));
|
|
592
|
+
const list = data.agents || [];
|
|
593
|
+
if (list.length > 0) { openChat(list[0].id); return; }
|
|
594
|
+
}
|
|
595
|
+
navigate('dashboard');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function switchChatAgent(agentId) {
|
|
599
|
+
if (agentId && agentId !== currentAgent?.id) openChat(agentId);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function openChat(agentId) {
|
|
603
|
+
try {
|
|
604
|
+
const res = await fetch(`${API}/api/agents/${agentId}`);
|
|
605
|
+
currentAgent = await res.json();
|
|
606
|
+
if (currentAgent.error) { navigate('dashboard'); return; }
|
|
607
|
+
} catch { navigate('dashboard'); return; }
|
|
608
|
+
|
|
609
|
+
// Load history from localStorage, fallback to empty
|
|
610
|
+
const stored = localStorage.getItem(`opc-chat-${agentId}`);
|
|
611
|
+
chatMessages = stored ? JSON.parse(stored) : [];
|
|
612
|
+
|
|
613
|
+
document.getElementById('chat-agent-icon').textContent = currentAgent.templateIcon || '🤖';
|
|
614
|
+
document.getElementById('chat-agent-name').textContent = currentAgent.name;
|
|
615
|
+
document.getElementById('chat-agent-status').textContent = `${currentAgent.templateName || 'Custom'} · ${currentAgent.model}`;
|
|
616
|
+
|
|
617
|
+
// Populate agent selector
|
|
618
|
+
const sel = document.getElementById('chat-agent-select');
|
|
619
|
+
sel.innerHTML = agents.map(a => `<option value="${a.id}" ${a.id === agentId ? 'selected' : ''}>${a.templateIcon || '🤖'} ${a.name}</option>`).join('');
|
|
620
|
+
|
|
621
|
+
// Render messages
|
|
622
|
+
const msgEl = document.getElementById('chat-messages');
|
|
623
|
+
if (chatMessages.length > 0) {
|
|
624
|
+
msgEl.innerHTML = chatMessages.map(m => `
|
|
625
|
+
<div class="msg ${m.role}">
|
|
626
|
+
<div class="msg-bubble">${m.content.replace(/</g,'<')}</div>
|
|
627
|
+
</div>
|
|
628
|
+
`).join('');
|
|
629
|
+
} else {
|
|
630
|
+
msgEl.innerHTML = `
|
|
631
|
+
<div class="msg assistant">
|
|
632
|
+
<div class="msg-bubble">Hello! I'm ${currentAgent.name}. ${currentAgent.description ? 'I specialize in: ' + currentAgent.description : 'How can I help you today?'}</div>
|
|
633
|
+
</div>
|
|
634
|
+
`;
|
|
635
|
+
}
|
|
636
|
+
document.getElementById('chat-input').value = '';
|
|
637
|
+
|
|
638
|
+
showPage('chat');
|
|
639
|
+
location.hash = `/chat/${agentId}`;
|
|
640
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
641
|
+
const chatNav = document.querySelector('.nav-item[data-page="chat"]');
|
|
642
|
+
if (chatNav) chatNav.classList.add('active');
|
|
643
|
+
msgEl.scrollTop = msgEl.scrollHeight;
|
|
644
|
+
document.getElementById('chat-input').focus();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function clearChat() {
|
|
648
|
+
if (!currentAgent) return;
|
|
649
|
+
chatMessages = [];
|
|
650
|
+
localStorage.removeItem(`opc-chat-${currentAgent.id}`);
|
|
651
|
+
document.getElementById('chat-messages').innerHTML = `
|
|
652
|
+
<div class="msg assistant">
|
|
653
|
+
<div class="msg-bubble">Hello! I'm ${currentAgent.name}. How can I help you today?</div>
|
|
654
|
+
</div>
|
|
655
|
+
`;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function handleDocUpload(input) {
|
|
659
|
+
const file = input.files[0];
|
|
660
|
+
if (!file || !currentAgent) return;
|
|
661
|
+
input.value = '';
|
|
662
|
+
|
|
663
|
+
// Show uploading status in chat
|
|
664
|
+
appendMessage('user', `📎 Uploading: ${file.name}`);
|
|
665
|
+
const statusEl = appendMessage('assistant', '⏳ Processing document...');
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const formData = new FormData();
|
|
669
|
+
formData.append('file', file);
|
|
670
|
+
|
|
671
|
+
const res = await fetch(`${API}/api/agents/${currentAgent.id}/upload`, {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
body: formData,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const data = await res.json();
|
|
677
|
+
if (data.error) {
|
|
678
|
+
statusEl.textContent = `❌ ${data.error}`;
|
|
679
|
+
} else {
|
|
680
|
+
statusEl.textContent = `✅ Learned ${data.learnedCount} knowledge chunks from "${file.name}"`;
|
|
681
|
+
}
|
|
682
|
+
} catch (e) {
|
|
683
|
+
statusEl.textContent = `❌ Upload failed: ${e.message}`;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function sendMessage() {
|
|
688
|
+
const input = document.getElementById('chat-input');
|
|
689
|
+
const text = input.value.trim();
|
|
690
|
+
if (!text || !currentAgent) return;
|
|
691
|
+
|
|
692
|
+
input.value = '';
|
|
693
|
+
chatMessages.push({ role: 'user', content: text });
|
|
694
|
+
|
|
695
|
+
// Render user message
|
|
696
|
+
appendMessage('user', text);
|
|
697
|
+
|
|
698
|
+
// Show typing + streaming indicator
|
|
699
|
+
document.getElementById('typing-indicator').classList.add('show');
|
|
700
|
+
document.getElementById('streaming-indicator').style.display = 'inline';
|
|
701
|
+
const msgContainer = document.getElementById('chat-messages');
|
|
702
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
const res = await fetch(`${API}/api/agents/${currentAgent.id}/chat`, {
|
|
706
|
+
method: 'POST',
|
|
707
|
+
headers: { 'Content-Type': 'application/json' },
|
|
708
|
+
body: JSON.stringify({ messages: chatMessages }),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
document.getElementById('typing-indicator').classList.remove('show');
|
|
712
|
+
|
|
713
|
+
if (res.headers.get('content-type')?.includes('text/event-stream')) {
|
|
714
|
+
// SSE streaming
|
|
715
|
+
const reader = res.body.getReader();
|
|
716
|
+
const decoder = new TextDecoder();
|
|
717
|
+
let assistantText = '';
|
|
718
|
+
const bubbleEl = appendMessage('assistant', '');
|
|
719
|
+
|
|
720
|
+
while (true) {
|
|
721
|
+
const { done, value } = await reader.read();
|
|
722
|
+
if (done) break;
|
|
723
|
+
const chunk = decoder.decode(value);
|
|
724
|
+
const lines = chunk.split('\n');
|
|
725
|
+
for (const line of lines) {
|
|
726
|
+
if (line.startsWith('data: ')) {
|
|
727
|
+
const data = line.slice(6);
|
|
728
|
+
if (data === '[DONE]') break;
|
|
729
|
+
try {
|
|
730
|
+
const parsed = JSON.parse(data);
|
|
731
|
+
const content = parsed.choices?.[0]?.delta?.content || parsed.content || '';
|
|
732
|
+
assistantText += content;
|
|
733
|
+
bubbleEl.textContent = assistantText;
|
|
734
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
735
|
+
} catch {}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
chatMessages.push({ role: 'assistant', content: assistantText });
|
|
740
|
+
} else {
|
|
741
|
+
const data = await res.json();
|
|
742
|
+
const reply = data.response || data.error || 'No response';
|
|
743
|
+
appendMessage('assistant', reply);
|
|
744
|
+
chatMessages.push({ role: 'assistant', content: reply });
|
|
745
|
+
}
|
|
746
|
+
// Persist to localStorage
|
|
747
|
+
if (currentAgent) {
|
|
748
|
+
try { localStorage.setItem(`opc-chat-${currentAgent.id}`, JSON.stringify(chatMessages.slice(-100))); } catch {}
|
|
749
|
+
}
|
|
750
|
+
} catch(e) {
|
|
751
|
+
document.getElementById('typing-indicator').classList.remove('show');
|
|
752
|
+
appendMessage('assistant', `Error: ${e.message}`);
|
|
753
|
+
} finally {
|
|
754
|
+
document.getElementById('streaming-indicator').style.display = 'none';
|
|
755
|
+
}
|
|
756
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function appendMessage(role, text) {
|
|
760
|
+
const msgContainer = document.getElementById('chat-messages');
|
|
761
|
+
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
762
|
+
const div = document.createElement('div');
|
|
763
|
+
div.className = `msg ${role}`;
|
|
764
|
+
const bubble = document.createElement('div');
|
|
765
|
+
bubble.className = 'msg-bubble';
|
|
766
|
+
bubble.textContent = text;
|
|
767
|
+
div.appendChild(bubble);
|
|
768
|
+
const timeEl = document.createElement('div');
|
|
769
|
+
timeEl.className = 'msg-time';
|
|
770
|
+
timeEl.textContent = time;
|
|
771
|
+
div.appendChild(timeEl);
|
|
772
|
+
msgContainer.appendChild(div);
|
|
773
|
+
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
774
|
+
return bubble;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// === Memory ===
|
|
778
|
+
function openMemory() {
|
|
779
|
+
if (currentAgent) openMemoryPage(currentAgent.id);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function openMemoryPage(agentId) {
|
|
783
|
+
showPage('memory');
|
|
784
|
+
location.hash = `/memory/${agentId}`;
|
|
785
|
+
try {
|
|
786
|
+
const res = await fetch(`${API}/api/agents/${agentId}/memory`);
|
|
787
|
+
const data = await res.json();
|
|
788
|
+
if (data.entries && data.entries.length > 0) {
|
|
789
|
+
document.getElementById('memory-empty').style.display = 'none';
|
|
790
|
+
document.getElementById('memory-timeline').innerHTML = `
|
|
791
|
+
<div class="timeline">
|
|
792
|
+
${data.entries.map(e => `
|
|
793
|
+
<div class="timeline-item">
|
|
794
|
+
<div class="timeline-date">${new Date(e.timestamp).toLocaleDateString()} ${new Date(e.timestamp).toLocaleTimeString()}</div>
|
|
795
|
+
<div class="timeline-content">${e.summary || e.content || 'Learned something new'}</div>
|
|
796
|
+
</div>
|
|
797
|
+
`).join('')}
|
|
798
|
+
</div>
|
|
799
|
+
`;
|
|
800
|
+
} else {
|
|
801
|
+
document.getElementById('memory-empty').style.display = 'block';
|
|
802
|
+
document.getElementById('memory-timeline').innerHTML = '';
|
|
803
|
+
}
|
|
804
|
+
} catch {
|
|
805
|
+
document.getElementById('memory-empty').style.display = 'block';
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function navigateToChat() {
|
|
810
|
+
if (currentAgent) openChat(currentAgent.id);
|
|
811
|
+
else navigate('dashboard');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// === Settings ===
|
|
815
|
+
let currentSettingsTab = 'models';
|
|
816
|
+
let currentProvider = null;
|
|
817
|
+
let currentChannel = null;
|
|
818
|
+
let modelConfig = {};
|
|
819
|
+
let statusRefreshTimer = null;
|
|
820
|
+
|
|
821
|
+
function showSettings(tab) {
|
|
822
|
+
currentSettingsTab = tab;
|
|
823
|
+
document.querySelectorAll('.settings-nav-item').forEach(n => n.classList.remove('active'));
|
|
824
|
+
document.querySelector(`.settings-nav-item[data-settings="${tab}"]`)?.classList.add('active');
|
|
825
|
+
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
|
|
826
|
+
document.getElementById(`sp-${tab}`)?.classList.add('active');
|
|
827
|
+
|
|
828
|
+
if (tab === 'models') initModelsPanel();
|
|
829
|
+
if (tab === 'channels') initChannelsPanel();
|
|
830
|
+
if (tab === 'memory') initMemoryPanel();
|
|
831
|
+
if (tab === 'role') initRolePanel();
|
|
832
|
+
if (tab === 'status') refreshStatus();
|
|
833
|
+
if (tab === 'usage') refreshUsage();
|
|
834
|
+
if (tab === 'search') initSearchPanel();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function switchModelTab(tab) {
|
|
838
|
+
document.querySelectorAll('#sp-models .tab').forEach(t => t.classList.remove('active'));
|
|
839
|
+
document.querySelectorAll('#sp-models .tab-panel').forEach(p => p.classList.remove('active'));
|
|
840
|
+
if (tab === 'local') {
|
|
841
|
+
document.querySelector('#sp-models .tab:first-child').classList.add('active');
|
|
842
|
+
document.getElementById('mt-local').classList.add('active');
|
|
843
|
+
} else {
|
|
844
|
+
document.querySelector('#sp-models .tab:last-child').classList.add('active');
|
|
845
|
+
document.getElementById('mt-cloud').classList.add('active');
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// --- Models Panel ---
|
|
850
|
+
async function initModelsPanel() {
|
|
851
|
+
try {
|
|
852
|
+
const res = await fetch(`${API}/api/settings/models`);
|
|
853
|
+
modelConfig = await res.json();
|
|
854
|
+
} catch { modelConfig = {}; }
|
|
855
|
+
detectOllama();
|
|
856
|
+
updateProviderStatuses();
|
|
857
|
+
updateModelDropdowns();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function detectOllama() {
|
|
861
|
+
const statusEl = document.getElementById('ollama-status');
|
|
862
|
+
const modelsEl = document.getElementById('ollama-models');
|
|
863
|
+
const tutorialEl = document.getElementById('ollama-tutorial');
|
|
864
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot yellow"></span> 正在检测本地 Ollama...</div>';
|
|
865
|
+
try {
|
|
866
|
+
const res = await fetch(`${API}/api/settings/models/local`);
|
|
867
|
+
const data = await res.json();
|
|
868
|
+
if (data.running) {
|
|
869
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot green"></span> <b>Ollama 运行中</b> — 本地模型可用,完全免费</div>';
|
|
870
|
+
tutorialEl.style.display = 'none';
|
|
871
|
+
if (data.models && data.models.length > 0) {
|
|
872
|
+
modelsEl.innerHTML = '<div class="card"><h3 style="font-size:15px;margin-bottom:12px;">已安装的模型</h3>' +
|
|
873
|
+
data.models.map(m => {
|
|
874
|
+
const size = m.size ? `${(m.size / 1e9).toFixed(1)}GB` : '';
|
|
875
|
+
const isChat = modelConfig.chatModel === m.name;
|
|
876
|
+
const isEmbed = modelConfig.embeddingModel === m.name;
|
|
877
|
+
const badge = isChat ? ' <span style="color:var(--accent);font-size:11px;">● 聊天</span>' : isEmbed ? ' <span style="color:var(--green);font-size:11px;">● 记忆</span>' : '';
|
|
878
|
+
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border);">
|
|
879
|
+
<div><span style="font-weight:500;">${m.name}</span>${badge}</div>
|
|
880
|
+
<span style="font-size:12px;color:var(--text-dim);">${size}</span>
|
|
881
|
+
</div>`;
|
|
882
|
+
}).join('') + '</div>';
|
|
883
|
+
// Update dropdowns with local models
|
|
884
|
+
updateModelDropdowns(data.models);
|
|
885
|
+
} else {
|
|
886
|
+
modelsEl.innerHTML = '<div class="card"><p style="color:var(--text-muted);font-size:14px;">Ollama 已运行但没有安装任何模型。请在终端运行:<br><code style="background:var(--bg-hover);padding:2px 8px;border-radius:4px;font-family:var(--mono);">ollama pull qwen2.5:7b</code></p></div>';
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> <b>Ollama 未运行</b> — 按照下面的教程安装</div>';
|
|
890
|
+
modelsEl.innerHTML = '';
|
|
891
|
+
tutorialEl.style.display = 'block';
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
statusEl.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> 无法检测 Ollama</div>';
|
|
895
|
+
tutorialEl.style.display = 'block';
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function updateModelDropdowns(localModels) {
|
|
900
|
+
const chatSel = document.getElementById('cfg-chat-model');
|
|
901
|
+
const embedSel = document.getElementById('cfg-embed-model');
|
|
902
|
+
if (localModels && localModels.length > 0) {
|
|
903
|
+
const chatOpts = localModels.map(m => `<option value="${m.name}" ${modelConfig.chatModel === m.name ? 'selected' : ''}>${m.name}${m.name === 'qwen2.5:7b' ? ' ⭐ 推荐' : ''}</option>`).join('');
|
|
904
|
+
const embedOpts = localModels.map(m => `<option value="${m.name}" ${modelConfig.embeddingModel === m.name ? 'selected' : ''}>${m.name}${m.name === 'nomic-embed-text' ? ' ⭐ 推荐' : ''}</option>`).join('');
|
|
905
|
+
chatSel.innerHTML = chatOpts;
|
|
906
|
+
embedSel.innerHTML = embedOpts;
|
|
907
|
+
}
|
|
908
|
+
// Add cloud models if configured
|
|
909
|
+
const providers = modelConfig.providers || {};
|
|
910
|
+
const cloudModels = [];
|
|
911
|
+
if (providers.openai?.apiKey) cloudModels.push({name:'gpt-4o',label:'GPT-4o (OpenAI)'},{name:'gpt-4o-mini',label:'GPT-4o Mini (OpenAI)'});
|
|
912
|
+
if (providers.deepseek?.apiKey) cloudModels.push({name:'deepseek-chat',label:'DeepSeek V3'},{name:'deepseek-reasoner',label:'DeepSeek R1'});
|
|
913
|
+
if (providers.anthropic?.apiKey) cloudModels.push({name:'claude-sonnet-4-20250514',label:'Claude Sonnet (Anthropic)'});
|
|
914
|
+
if (providers.openrouter?.apiKey) cloudModels.push({name:'openrouter/auto',label:'OpenRouter Auto'});
|
|
915
|
+
cloudModels.forEach(m => {
|
|
916
|
+
chatSel.innerHTML += `<option value="${m.name}" ${modelConfig.chatModel === m.name ? 'selected' : ''}>${m.label}</option>`;
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function updateProviderStatuses() {
|
|
921
|
+
const providers = modelConfig.providers || {};
|
|
922
|
+
['openai','deepseek','qwen','anthropic','openrouter'].forEach(p => {
|
|
923
|
+
const el = document.getElementById(`pv-${p}`);
|
|
924
|
+
if (!el) return;
|
|
925
|
+
if (providers[p]?.apiKey) {
|
|
926
|
+
el.innerHTML = '<span style="color:var(--green);">✅ 已配置</span>';
|
|
927
|
+
el.closest('.provider-card')?.classList.add('configured');
|
|
928
|
+
} else {
|
|
929
|
+
el.innerHTML = '未配置';
|
|
930
|
+
el.closest('.provider-card')?.classList.remove('configured');
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async function saveModelAssignment() {
|
|
936
|
+
const chatModel = document.getElementById('cfg-chat-model').value;
|
|
937
|
+
const embeddingModel = document.getElementById('cfg-embed-model').value;
|
|
938
|
+
try {
|
|
939
|
+
await fetch(`${API}/api/settings/models`, {
|
|
940
|
+
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
941
|
+
body: JSON.stringify({ chatModel, embeddingModel })
|
|
942
|
+
});
|
|
943
|
+
modelConfig.chatModel = chatModel;
|
|
944
|
+
modelConfig.embeddingModel = embeddingModel;
|
|
945
|
+
} catch {}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// --- Provider Dialog ---
|
|
949
|
+
const PROVIDER_INFO = {
|
|
950
|
+
openai: { name: 'OpenAI', desc: '需要 OpenAI 账号。获取 Key: platform.openai.com/api-keys', placeholder: 'sk-...' },
|
|
951
|
+
deepseek: { name: 'DeepSeek', desc: '国产大模型,性价比极高。获取 Key: platform.deepseek.com', placeholder: 'sk-...' },
|
|
952
|
+
qwen: { name: '通义千问', desc: '阿里云大模型。获取 Key: dashscope.console.aliyun.com', placeholder: 'sk-...' },
|
|
953
|
+
anthropic: { name: 'Anthropic', desc: 'Claude 系列模型。获取 Key: console.anthropic.com', placeholder: 'sk-ant-...' },
|
|
954
|
+
openrouter: { name: 'OpenRouter', desc: '100+ 模型聚合平台。获取 Key: openrouter.ai/keys', placeholder: 'sk-or-...' },
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
function configureProvider(provider) {
|
|
958
|
+
currentProvider = provider;
|
|
959
|
+
const info = PROVIDER_INFO[provider] || {};
|
|
960
|
+
document.getElementById('pd-title').textContent = `配置 ${info.name || provider}`;
|
|
961
|
+
document.getElementById('pd-desc').textContent = info.desc || '';
|
|
962
|
+
document.getElementById('pd-apikey').placeholder = info.placeholder || 'API Key';
|
|
963
|
+
document.getElementById('pd-apikey').value = modelConfig.providers?.[provider]?.apiKey || '';
|
|
964
|
+
document.getElementById('pd-baseurl').value = modelConfig.providers?.[provider]?.baseUrl || '';
|
|
965
|
+
document.getElementById('pd-test-result').innerHTML = '';
|
|
966
|
+
document.getElementById('pd-baseurl-group').style.display = (provider === 'qwen' || provider === 'openrouter') ? 'block' : 'none';
|
|
967
|
+
document.getElementById('provider-dialog').classList.add('show');
|
|
968
|
+
}
|
|
969
|
+
function closeProviderDialog() { document.getElementById('provider-dialog').classList.remove('show'); currentProvider = null; }
|
|
970
|
+
|
|
971
|
+
async function testProvider() {
|
|
972
|
+
const apiKey = document.getElementById('pd-apikey').value.trim();
|
|
973
|
+
const baseUrl = document.getElementById('pd-baseurl').value.trim();
|
|
974
|
+
const resultEl = document.getElementById('pd-test-result');
|
|
975
|
+
if (!apiKey) { resultEl.innerHTML = '<span style="color:var(--yellow);">请先填入 API Key</span>'; return; }
|
|
976
|
+
resultEl.innerHTML = '<span style="color:var(--text-muted);">⏳ 测试中...</span>';
|
|
977
|
+
try {
|
|
978
|
+
const res = await fetch(`${API}/api/settings/models/test`, {
|
|
979
|
+
method: 'POST', headers: {'Content-Type':'application/json'},
|
|
980
|
+
body: JSON.stringify({ provider: currentProvider, apiKey, baseUrl: baseUrl || undefined })
|
|
981
|
+
});
|
|
982
|
+
const data = await res.json();
|
|
983
|
+
resultEl.innerHTML = data.success ? '<span style="color:var(--green);">✅ 连接成功!</span>' : `<span style="color:var(--red);">❌ 连接失败 (${data.error || data.statusCode})</span>`;
|
|
984
|
+
} catch(e) {
|
|
985
|
+
resultEl.innerHTML = `<span style="color:var(--red);">❌ 网络错误</span>`;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function saveProvider() {
|
|
990
|
+
const apiKey = document.getElementById('pd-apikey').value.trim();
|
|
991
|
+
const baseUrl = document.getElementById('pd-baseurl').value.trim();
|
|
992
|
+
if (!modelConfig.providers) modelConfig.providers = {};
|
|
993
|
+
modelConfig.providers[currentProvider] = { apiKey, baseUrl: baseUrl || undefined };
|
|
994
|
+
try {
|
|
995
|
+
await fetch(`${API}/api/settings/models`, {
|
|
996
|
+
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
997
|
+
body: JSON.stringify({ providers: modelConfig.providers })
|
|
998
|
+
});
|
|
999
|
+
} catch {}
|
|
1000
|
+
updateProviderStatuses();
|
|
1001
|
+
updateModelDropdowns();
|
|
1002
|
+
closeProviderDialog();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// --- Channels Panel ---
|
|
1006
|
+
const CHANNELS = [
|
|
1007
|
+
{ id: 'telegram', name: 'Telegram', icon: '✈️', fields: [{key:'botToken',label:'Bot Token',placeholder:'123456:ABC-DEF...',help:'从 @BotFather 获取。<a href="https://t.me/botfather" target="_blank">打开 BotFather →</a>'}] },
|
|
1008
|
+
{ id: 'wechat', name: '微信', icon: '💬', fields: [], comingSoon: true },
|
|
1009
|
+
{ id: 'feishu', name: '飞书', icon: '🐦', fields: [{key:'appId',label:'App ID',placeholder:'cli_...'},{key:'appSecret',label:'App Secret',placeholder:'',type:'password'}] },
|
|
1010
|
+
{ id: 'discord', name: 'Discord', icon: '🎮', fields: [{key:'botToken',label:'Bot Token',placeholder:'',type:'password'}] },
|
|
1011
|
+
{ id: 'slack', name: 'Slack', icon: '💼', fields: [{key:'botToken',label:'Bot Token',placeholder:'xoxb-...',type:'password'}] },
|
|
1012
|
+
{ id: 'email', name: 'Email', icon: '📧', fields: [{key:'imapHost',label:'IMAP Host',placeholder:'imap.gmail.com'},{key:'smtpHost',label:'SMTP Host',placeholder:'smtp.gmail.com'},{key:'email',label:'Email',placeholder:'agent@example.com'},{key:'password',label:'Password',placeholder:'',type:'password'}] },
|
|
1013
|
+
{ id: 'web', name: 'Web', icon: '🌐', fields: [], alwaysOn: true },
|
|
1014
|
+
{ id: 'whatsapp', name: 'WhatsApp', icon: '📱', fields: [{key:'phoneId',label:'Phone Number ID',placeholder:''},{key:'accessToken',label:'Access Token',placeholder:'',type:'password'}] },
|
|
1015
|
+
];
|
|
1016
|
+
|
|
1017
|
+
let channelConfigs = {};
|
|
1018
|
+
|
|
1019
|
+
async function initChannelsPanel() {
|
|
1020
|
+
try {
|
|
1021
|
+
const res = await fetch(`${API}/api/settings/channels`);
|
|
1022
|
+
channelConfigs = await res.json();
|
|
1023
|
+
} catch { channelConfigs = {}; }
|
|
1024
|
+
renderChannels();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function renderChannels() {
|
|
1028
|
+
document.getElementById('channels-grid').innerHTML = CHANNELS.map(ch => {
|
|
1029
|
+
const cfg = channelConfigs[ch.id] || {};
|
|
1030
|
+
const connected = ch.alwaysOn || (cfg && Object.keys(cfg).some(k => k !== 'updated' && cfg[k]));
|
|
1031
|
+
const statusDot = ch.comingSoon ? 'yellow' : connected ? 'green' : 'red';
|
|
1032
|
+
const statusText = ch.comingSoon ? '即将支持' : connected ? '已连接' : '未配置';
|
|
1033
|
+
return `<div class="card channel-card" onclick="${ch.comingSoon ? '' : `configureChannel('${ch.id}')`}" style="${ch.comingSoon ? 'opacity:0.6;cursor:default;' : ''}">
|
|
1034
|
+
<div class="ch-icon">${ch.icon}</div>
|
|
1035
|
+
<div class="ch-info">
|
|
1036
|
+
<div class="ch-name">${ch.name}</div>
|
|
1037
|
+
<div class="ch-status"><span class="status-dot ${statusDot}"></span> ${statusText}</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
${!ch.comingSoon && !ch.alwaysOn ? '<span style="color:var(--text-dim);font-size:18px;">›</span>' : ''}
|
|
1040
|
+
${ch.alwaysOn ? '<span style="font-size:12px;color:var(--green);">默认开启</span>' : ''}
|
|
1041
|
+
</div>`;
|
|
1042
|
+
}).join('');
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function configureChannel(chId) {
|
|
1046
|
+
const ch = CHANNELS.find(c => c.id === chId);
|
|
1047
|
+
if (!ch || ch.comingSoon) return;
|
|
1048
|
+
if (ch.alwaysOn) return;
|
|
1049
|
+
currentChannel = chId;
|
|
1050
|
+
const cfg = channelConfigs[chId] || {};
|
|
1051
|
+
document.getElementById('cd-title').textContent = `配置 ${ch.name}`;
|
|
1052
|
+
document.getElementById('cd-desc').textContent = '';
|
|
1053
|
+
document.getElementById('cd-fields').innerHTML = ch.fields.map(f =>
|
|
1054
|
+
`<div class="form-group">
|
|
1055
|
+
<label class="label">${f.label}</label>
|
|
1056
|
+
<input class="input" id="cf-${f.key}" type="${f.type || 'text'}" placeholder="${f.placeholder || ''}" value="${cfg[f.key] || ''}">
|
|
1057
|
+
${f.help ? `<p style="font-size:12px;color:var(--text-dim);margin-top:4px;">${f.help}</p>` : ''}
|
|
1058
|
+
</div>`
|
|
1059
|
+
).join('');
|
|
1060
|
+
document.getElementById('channel-dialog').classList.add('show');
|
|
1061
|
+
}
|
|
1062
|
+
function closeChannelDialog() { document.getElementById('channel-dialog').classList.remove('show'); currentChannel = null; }
|
|
1063
|
+
|
|
1064
|
+
async function saveChannel() {
|
|
1065
|
+
const ch = CHANNELS.find(c => c.id === currentChannel);
|
|
1066
|
+
if (!ch) return;
|
|
1067
|
+
const cfg = {};
|
|
1068
|
+
ch.fields.forEach(f => { cfg[f.key] = document.getElementById(`cf-${f.key}`)?.value?.trim() || ''; });
|
|
1069
|
+
try {
|
|
1070
|
+
await fetch(`${API}/api/settings/channels/${currentChannel}`, {
|
|
1071
|
+
method: 'PUT', headers: {'Content-Type':'application/json'},
|
|
1072
|
+
body: JSON.stringify(cfg)
|
|
1073
|
+
});
|
|
1074
|
+
channelConfigs[currentChannel] = cfg;
|
|
1075
|
+
} catch {}
|
|
1076
|
+
renderChannels();
|
|
1077
|
+
closeChannelDialog();
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// --- Memory Panel (DeepBrain iframe) ---
|
|
1081
|
+
async function initMemoryPanel() {
|
|
1082
|
+
const container = document.getElementById('memory-module-frame');
|
|
1083
|
+
const running = await checkModulePort(4001);
|
|
1084
|
+
if (running) {
|
|
1085
|
+
container.innerHTML = `<div class="module-frame-container"><iframe src="http://localhost:4001" title="DeepBrain 记忆管理"></iframe></div>`;
|
|
1086
|
+
} else {
|
|
1087
|
+
container.innerHTML = `<div class="card module-frame-fallback">
|
|
1088
|
+
<div class="mf-icon">🧠</div>
|
|
1089
|
+
<h3 style="margin-bottom:8px;">DeepBrain 未运行</h3>
|
|
1090
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:16px;">记忆管理由 DeepBrain 模块提供(端口 4001)</p>
|
|
1091
|
+
<a href="http://localhost:4001" target="_blank" class="btn btn-primary">🔗 打开记忆管理</a>
|
|
1092
|
+
<p style="color:var(--text-dim);font-size:12px;margin-top:12px;">如果按钮无法打开,请先启动 DeepBrain 服务</p>
|
|
1093
|
+
</div>`;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// --- Role Panel (Workstation iframe) ---
|
|
1098
|
+
async function initRolePanel() {
|
|
1099
|
+
const container = document.getElementById('role-module-frame');
|
|
1100
|
+
const running = await checkModulePort(4003);
|
|
1101
|
+
if (running) {
|
|
1102
|
+
container.innerHTML = `<div class="module-frame-container"><iframe src="http://localhost:4003" title="Workstation 角色编辑"></iframe></div>`;
|
|
1103
|
+
} else {
|
|
1104
|
+
container.innerHTML = `<div class="card module-frame-fallback">
|
|
1105
|
+
<div class="mf-icon">👤</div>
|
|
1106
|
+
<h3 style="margin-bottom:8px;">Workstation 未运行</h3>
|
|
1107
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:16px;">角色编辑由 Workstation 模块提供(端口 4003)</p>
|
|
1108
|
+
<a href="http://localhost:4003" target="_blank" class="btn btn-primary">🔗 打开角色编辑</a>
|
|
1109
|
+
<p style="color:var(--text-dim);font-size:12px;margin-top:12px;">如果按钮无法打开,请先启动 Workstation 服务</p>
|
|
1110
|
+
</div>`;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function checkModulePort(port) {
|
|
1115
|
+
try {
|
|
1116
|
+
const res = await fetch(`${API}/api/modules`);
|
|
1117
|
+
const data = await res.json();
|
|
1118
|
+
const mod = (data.modules || []).find(m => m.port === port);
|
|
1119
|
+
return mod?.running || false;
|
|
1120
|
+
} catch { return false; }
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// --- Status Panel ---
|
|
1124
|
+
async function refreshStatus() {
|
|
1125
|
+
try {
|
|
1126
|
+
const res = await fetch(`${API}/api/settings/status`);
|
|
1127
|
+
const data = await res.json();
|
|
1128
|
+
|
|
1129
|
+
// Overview cards
|
|
1130
|
+
const upHrs = Math.floor(data.uptime / 3600);
|
|
1131
|
+
const upMins = Math.floor((data.uptime % 3600) / 60);
|
|
1132
|
+
const memMB = Math.round((data.memory?.rss || 0) / 1048576);
|
|
1133
|
+
|
|
1134
|
+
document.getElementById('status-overview').innerHTML = `
|
|
1135
|
+
<div class="card-grid" style="margin-bottom:16px;">
|
|
1136
|
+
<div class="card stat-card">
|
|
1137
|
+
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px;">
|
|
1138
|
+
<span class="status-dot green"></span><span style="font-size:14px;font-weight:600;">运行中</span>
|
|
1139
|
+
</div>
|
|
1140
|
+
<div class="stat-value">${upHrs}h ${upMins}m</div>
|
|
1141
|
+
<div class="stat-label">运行时间</div>
|
|
1142
|
+
</div>
|
|
1143
|
+
<div class="card stat-card">
|
|
1144
|
+
<div class="stat-value">${memMB} MB</div>
|
|
1145
|
+
<div class="stat-label">内存占用</div>
|
|
1146
|
+
</div>
|
|
1147
|
+
<div class="card stat-card">
|
|
1148
|
+
<div class="stat-value">${(data.modules || []).filter(m => m.running).length}/${(data.modules || []).length}</div>
|
|
1149
|
+
<div class="stat-label">模块在线</div>
|
|
1150
|
+
</div>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="card" style="margin-bottom:16px;">
|
|
1153
|
+
<h3 style="font-size:15px;margin-bottom:12px;">模块状态</h3>
|
|
1154
|
+
${(data.modules || []).map(m => `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;">
|
|
1155
|
+
<span class="status-dot ${m.running ? 'green' : 'red'}"></span>
|
|
1156
|
+
<span>${m.icon} ${m.name}</span>
|
|
1157
|
+
<span style="color:var(--text-dim);font-size:12px;margin-left:auto;">:${m.port}</span>
|
|
1158
|
+
</div>`).join('')}
|
|
1159
|
+
</div>
|
|
1160
|
+
`;
|
|
1161
|
+
|
|
1162
|
+
// Logs
|
|
1163
|
+
const logsEl = document.getElementById('status-logs');
|
|
1164
|
+
if (data.logs && data.logs.length > 0) {
|
|
1165
|
+
logsEl.textContent = data.logs.join('\n');
|
|
1166
|
+
logsEl.scrollTop = logsEl.scrollHeight;
|
|
1167
|
+
} else {
|
|
1168
|
+
logsEl.textContent = '暂无日志。Agent 运行后日志会显示在这里。';
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
document.getElementById('status-overview').innerHTML = '<div class="card"><p style="color:var(--text-muted);">无法获取状态信息</p></div>';
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// --- Usage Panel ---
|
|
1176
|
+
async function refreshUsage() {
|
|
1177
|
+
try {
|
|
1178
|
+
const res = await fetch(`${API}/api/settings/usage`);
|
|
1179
|
+
const data = await res.json();
|
|
1180
|
+
const totalTokens = data.totalTokens || 0;
|
|
1181
|
+
const totalCost = data.totalCost || 0;
|
|
1182
|
+
const byModel = data.byModel || {};
|
|
1183
|
+
const daily = data.daily || [];
|
|
1184
|
+
|
|
1185
|
+
document.getElementById('usage-stats').innerHTML = `
|
|
1186
|
+
<div class="card-grid" style="margin-bottom:24px;">
|
|
1187
|
+
<div class="card stat-card">
|
|
1188
|
+
<div class="stat-value">${totalTokens > 1000 ? (totalTokens/1000).toFixed(1) + 'K' : totalTokens}</div>
|
|
1189
|
+
<div class="stat-label">总 Token 消耗</div>
|
|
1190
|
+
</div>
|
|
1191
|
+
<div class="card stat-card">
|
|
1192
|
+
<div class="stat-value">$${totalCost.toFixed(4)}</div>
|
|
1193
|
+
<div class="stat-label">估算费用</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div class="card stat-card">
|
|
1196
|
+
<div class="stat-value">${Object.keys(byModel).length || 0}</div>
|
|
1197
|
+
<div class="stat-label">使用模型数</div>
|
|
1198
|
+
</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
${Object.keys(byModel).length > 0 ? `
|
|
1201
|
+
<div class="card" style="margin-bottom:16px;">
|
|
1202
|
+
<h3 style="font-size:15px;margin-bottom:12px;">按模型分布</h3>
|
|
1203
|
+
${Object.entries(byModel).map(([m, v]) => `<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border);">
|
|
1204
|
+
<span style="font-size:14px;">${m}</span>
|
|
1205
|
+
<span style="font-size:13px;color:var(--text-muted);">${v.tokens || 0} tokens · $${(v.cost || 0).toFixed(4)}</span>
|
|
1206
|
+
</div>`).join('')}
|
|
1207
|
+
</div>
|
|
1208
|
+
` : ''}
|
|
1209
|
+
${totalTokens === 0 ? `
|
|
1210
|
+
<div class="card" style="text-align:center;padding:40px;">
|
|
1211
|
+
<div style="font-size:36px;margin-bottom:12px;">📊</div>
|
|
1212
|
+
<p style="color:var(--text-muted);">还没有使用记录。开始和 Agent 聊天后,用量数据会自动记录在这里。</p>
|
|
1213
|
+
${modelConfig.mode === 'local' || !modelConfig.mode ? '<p style="color:var(--green);font-size:13px;margin-top:8px;">💡 使用本地模型完全免费,不产生费用</p>' : ''}
|
|
1214
|
+
</div>
|
|
1215
|
+
` : ''}
|
|
1216
|
+
`;
|
|
1217
|
+
} catch {
|
|
1218
|
+
document.getElementById('usage-stats').innerHTML = '<div class="card"><p style="color:var(--text-muted);">无法获取用量数据</p></div>';
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// === Web Search Settings ===
|
|
1223
|
+
async function initSearchPanel() {
|
|
1224
|
+
try {
|
|
1225
|
+
const res = await fetch(`${API}/api/settings/search`);
|
|
1226
|
+
const cfg = await res.json();
|
|
1227
|
+
document.getElementById('search-enabled').checked = cfg.enabled !== false;
|
|
1228
|
+
document.getElementById('search-engine').value = cfg.defaultEngine || 'duckduckgo';
|
|
1229
|
+
updateSearchEngineUI(cfg.defaultEngine || 'duckduckgo');
|
|
1230
|
+
if (cfg.engines) {
|
|
1231
|
+
const eng = cfg.engines[cfg.defaultEngine];
|
|
1232
|
+
if (eng?.apiKey) document.getElementById('search-apikey').value = eng.apiKey;
|
|
1233
|
+
if (eng?.baseUrl) document.getElementById('search-baseurl').value = eng.baseUrl;
|
|
1234
|
+
}
|
|
1235
|
+
} catch { /* defaults are fine */ }
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function updateSearchEngineUI(engine) {
|
|
1239
|
+
const needsKey = ['brave', 'google'].includes(engine);
|
|
1240
|
+
const needsUrl = engine === 'searxng';
|
|
1241
|
+
document.getElementById('search-apikey-group').style.display = needsKey ? '' : 'none';
|
|
1242
|
+
document.getElementById('search-baseurl-group').style.display = needsUrl ? '' : 'none';
|
|
1243
|
+
if (engine === 'brave') document.getElementById('search-apikey-label').textContent = 'Brave Search API Key';
|
|
1244
|
+
if (engine === 'google') document.getElementById('search-apikey-label').textContent = 'Google API Key:CX';
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
async function updateSearchConfig() {
|
|
1248
|
+
const engine = document.getElementById('search-engine').value;
|
|
1249
|
+
updateSearchEngineUI(engine);
|
|
1250
|
+
const cfg = {
|
|
1251
|
+
enabled: document.getElementById('search-enabled').checked,
|
|
1252
|
+
defaultEngine: engine,
|
|
1253
|
+
engines: {}
|
|
1254
|
+
};
|
|
1255
|
+
cfg.engines[engine] = { enabled: true };
|
|
1256
|
+
const apiKey = document.getElementById('search-apikey').value;
|
|
1257
|
+
const baseUrl = document.getElementById('search-baseurl').value;
|
|
1258
|
+
if (apiKey) cfg.engines[engine].apiKey = apiKey;
|
|
1259
|
+
if (baseUrl) cfg.engines[engine].baseUrl = baseUrl;
|
|
1260
|
+
cfg.engines.duckduckgo = { enabled: true };
|
|
1261
|
+
try {
|
|
1262
|
+
await fetch(`${API}/api/settings/search`, {
|
|
1263
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
1264
|
+
body: JSON.stringify(cfg)
|
|
1265
|
+
});
|
|
1266
|
+
} catch { /* silent */ }
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
async function testSearch() {
|
|
1270
|
+
const el = document.getElementById('search-test-result');
|
|
1271
|
+
el.innerHTML = '<span style="color:var(--yellow);">🔍 正在搜索...</span>';
|
|
1272
|
+
try {
|
|
1273
|
+
const res = await fetch(`${API}/api/settings/search/test`, {
|
|
1274
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1275
|
+
body: JSON.stringify({ query: 'hello world test' })
|
|
1276
|
+
});
|
|
1277
|
+
const data = await res.json();
|
|
1278
|
+
if (data.success && data.results?.length) {
|
|
1279
|
+
el.innerHTML = `<span style="color:var(--green);">✅ 搜索成功!找到 ${data.results.length} 条结果</span><br>` +
|
|
1280
|
+
data.results.map(r => `<div style="margin-top:8px;font-size:12px;"><a href="${r.url}" target="_blank">${r.title}</a><br><span style="color:var(--text-muted);">${r.snippet?.slice(0,100)}</span></div>`).join('');
|
|
1281
|
+
} else {
|
|
1282
|
+
el.innerHTML = `<span style="color:var(--red);">❌ ${data.error || '未找到结果'}</span>`;
|
|
1283
|
+
}
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
el.innerHTML = `<span style="color:var(--red);">❌ 测试失败: ${e.message}</span>`;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// === Health Dashboard ===
|
|
1290
|
+
async function loadHealthDashboard() {
|
|
1291
|
+
const el = document.getElementById('health-section');
|
|
1292
|
+
if (!el) return;
|
|
1293
|
+
try {
|
|
1294
|
+
const [modRes, ollamaRes] = await Promise.all([
|
|
1295
|
+
fetch(`${API}/api/modules`),
|
|
1296
|
+
fetch(`${API}/api/settings/models/local`),
|
|
1297
|
+
]);
|
|
1298
|
+
const modData = await modRes.json();
|
|
1299
|
+
const ollamaData = await ollamaRes.json();
|
|
1300
|
+
const modules = modData.modules || [];
|
|
1301
|
+
const runningCount = modules.filter(m => m.running).length;
|
|
1302
|
+
el.innerHTML = `
|
|
1303
|
+
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
|
|
1304
|
+
${modules.map(m => `
|
|
1305
|
+
<div class="card" style="flex:1;min-width:140px;display:flex;align-items:center;gap:10px;padding:12px 14px;">
|
|
1306
|
+
<span class="status-dot ${m.running ? 'green' : 'red'}"></span>
|
|
1307
|
+
<span style="font-size:13px;">${m.icon} ${m.name}</span>
|
|
1308
|
+
<span style="font-size:11px;color:var(--text-dim);margin-left:auto;">:${m.port}</span>
|
|
1309
|
+
</div>
|
|
1310
|
+
`).join('')}
|
|
1311
|
+
<div class="card" style="flex:1;min-width:140px;display:flex;align-items:center;gap:10px;padding:12px 14px;">
|
|
1312
|
+
<span class="status-dot ${ollamaData.running ? 'green' : 'red'}"></span>
|
|
1313
|
+
<span style="font-size:13px;">🦙 Ollama</span>
|
|
1314
|
+
<span style="font-size:11px;color:var(--text-dim);margin-left:auto;">${ollamaData.running ? (ollamaData.models?.length || 0) + ' models' : 'offline'}</span>
|
|
1315
|
+
</div>
|
|
1316
|
+
</div>
|
|
1317
|
+
`;
|
|
1318
|
+
} catch {
|
|
1319
|
+
el.innerHTML = '';
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// === First Run Wizard ===
|
|
1324
|
+
let frStep = 1;
|
|
1325
|
+
let frSelectedTemplate = null;
|
|
1326
|
+
let frCreatedAgentId = null;
|
|
1327
|
+
|
|
1328
|
+
async function checkFirstRun() {
|
|
1329
|
+
// Skip first-run wizard — agents are configured via init
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function showFirstRunWizard(data) {
|
|
1334
|
+
frStep = 1;
|
|
1335
|
+
const overlay = document.getElementById('first-run-overlay');
|
|
1336
|
+
overlay.style.display = 'flex';
|
|
1337
|
+
frRenderStep();
|
|
1338
|
+
if (data?.ollamaDetected) {
|
|
1339
|
+
const statusEl = document.getElementById('fr-ollama-status');
|
|
1340
|
+
if (statusEl) {
|
|
1341
|
+
statusEl.innerHTML = `<div style="display:flex;align-items:center;gap:8px;color:var(--green);"><span class="status-dot green"></span> <b>Ollama detected!</b> ${data.ollamaModels?.length ? data.ollamaModels.length + ' models available.' : ''} Local AI is free.</div>`;
|
|
1342
|
+
const choiceEl = document.getElementById('fr-model-choice');
|
|
1343
|
+
if (choiceEl) choiceEl.style.display = 'block';
|
|
1344
|
+
const sel = document.getElementById('fr-model-select');
|
|
1345
|
+
if (sel && data.ollamaModels?.length) {
|
|
1346
|
+
sel.innerHTML = data.ollamaModels.map(m => `<option value="${m.name}">${m.name} (local)</option>`).join('') + '<option value="gpt-4o-mini">GPT-4o Mini (cloud)</option>';
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
} else {
|
|
1350
|
+
detectFrOllama();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function detectFrOllama() {
|
|
1355
|
+
try {
|
|
1356
|
+
const res = await fetch(`${API}/api/settings/models/local`);
|
|
1357
|
+
const data = await res.json();
|
|
1358
|
+
const statusEl = document.getElementById('fr-ollama-status');
|
|
1359
|
+
const choiceEl = document.getElementById('fr-model-choice');
|
|
1360
|
+
if (!statusEl) return;
|
|
1361
|
+
if (data.running) {
|
|
1362
|
+
statusEl.innerHTML = `<div style="display:flex;align-items:center;gap:8px;color:var(--green);"><span class="status-dot green"></span> <b>Ollama running</b> — free local models available!</div>`;
|
|
1363
|
+
if (choiceEl) choiceEl.style.display = 'block';
|
|
1364
|
+
const sel = document.getElementById('fr-model-select');
|
|
1365
|
+
if (sel && data.models?.length) {
|
|
1366
|
+
sel.innerHTML = data.models.map(m => `<option value="${m.name}">${m.name} (local)</option>`).join('') + '<option value="gpt-4o-mini">GPT-4o Mini (cloud)</option>';
|
|
1367
|
+
}
|
|
1368
|
+
} else {
|
|
1369
|
+
statusEl.innerHTML = `<div style="display:flex;align-items:center;gap:8px;"><span class="status-dot red"></span> Ollama not detected — you can use cloud models or <a href="https://ollama.com" target="_blank">install Ollama</a> for free local AI.</div>`;
|
|
1370
|
+
if (choiceEl) choiceEl.style.display = 'block';
|
|
1371
|
+
}
|
|
1372
|
+
} catch {}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function frRenderStep() {
|
|
1376
|
+
for (let i = 1; i <= 4; i++) {
|
|
1377
|
+
const stepEl = document.getElementById(`fr-step-${i}`);
|
|
1378
|
+
const panelEl = document.getElementById(`fr-panel-${i}`);
|
|
1379
|
+
if (stepEl) stepEl.className = 'wizard-step' + (i < frStep ? ' done' : i === frStep ? ' active' : '');
|
|
1380
|
+
if (panelEl) panelEl.className = 'wizard-panel' + (i === frStep ? ' active' : '');
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function frNext() {
|
|
1385
|
+
if (frStep === 3 && !frSelectedTemplate) {
|
|
1386
|
+
frSelectedTemplate = 'customer-service';
|
|
1387
|
+
}
|
|
1388
|
+
if (frStep === 3) {
|
|
1389
|
+
frStep = 4;
|
|
1390
|
+
frRenderStep();
|
|
1391
|
+
frCreateAgent();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (frStep < 4) { frStep++; frRenderStep(); }
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function frBack() {
|
|
1398
|
+
if (frStep > 1) { frStep--; frRenderStep(); }
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function frSelectTemplate(id) {
|
|
1402
|
+
frSelectedTemplate = id;
|
|
1403
|
+
document.querySelectorAll('#fr-template-list .card').forEach(c => {
|
|
1404
|
+
c.style.borderColor = '';
|
|
1405
|
+
c.style.background = '';
|
|
1406
|
+
});
|
|
1407
|
+
const el = document.getElementById(`fr-tpl-${id}`);
|
|
1408
|
+
if (el) { el.style.borderColor = 'var(--accent)'; el.style.background = 'var(--accent-light)'; }
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function frCreateAgent() {
|
|
1412
|
+
const model = document.getElementById('fr-model-select')?.value || 'qwen2.5:7b';
|
|
1413
|
+
try {
|
|
1414
|
+
// Save first-run complete
|
|
1415
|
+
await fetch(`${API}/api/first-run/complete`, {
|
|
1416
|
+
method: 'POST',
|
|
1417
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1418
|
+
body: JSON.stringify({ templateId: frSelectedTemplate, model }),
|
|
1419
|
+
});
|
|
1420
|
+
// Create the agent
|
|
1421
|
+
const res = await fetch(`${API}/api/agents`, {
|
|
1422
|
+
method: 'POST',
|
|
1423
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1424
|
+
body: JSON.stringify({ name: '', templateId: frSelectedTemplate || 'customer-service', model }),
|
|
1425
|
+
});
|
|
1426
|
+
const agent = await res.json();
|
|
1427
|
+
frCreatedAgentId = agent.id;
|
|
1428
|
+
document.getElementById('fr-creating').style.display = 'none';
|
|
1429
|
+
document.getElementById('fr-done').style.display = 'block';
|
|
1430
|
+
await loadAgents();
|
|
1431
|
+
} catch(e) {
|
|
1432
|
+
document.getElementById('fr-creating').innerHTML = `<div style="color:var(--red);">Error: ${e.message}</div>`;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function frFinish() {
|
|
1437
|
+
document.getElementById('first-run-overlay').style.display = 'none';
|
|
1438
|
+
if (frCreatedAgentId) openChat(frCreatedAgentId);
|
|
1439
|
+
else navigate('dashboard');
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// === Drag & drop document upload ===
|
|
1443
|
+
const chatArea = document.getElementById('chat-messages');
|
|
1444
|
+
if (chatArea) {
|
|
1445
|
+
chatArea.addEventListener('dragover', (e) => { e.preventDefault(); chatArea.style.outline = '2px dashed var(--primary)'; });
|
|
1446
|
+
chatArea.addEventListener('dragleave', () => { chatArea.style.outline = ''; });
|
|
1447
|
+
chatArea.addEventListener('drop', (e) => {
|
|
1448
|
+
e.preventDefault();
|
|
1449
|
+
chatArea.style.outline = '';
|
|
1450
|
+
const file = e.dataTransfer?.files?.[0];
|
|
1451
|
+
if (file) {
|
|
1452
|
+
const dt = new DataTransfer();
|
|
1453
|
+
dt.items.add(file);
|
|
1454
|
+
const inp = document.getElementById('doc-upload-input');
|
|
1455
|
+
inp.files = dt.files;
|
|
1456
|
+
handleDocUpload(inp);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// === Start ===
|
|
1462
|
+
init();
|
|
1463
|
+
|
|
1464
|
+
// =============================================
|
|
1465
|
+
// === Schedules Management ===
|
|
1466
|
+
// =============================================
|
|
1467
|
+
let editingScheduleId = null;
|
|
1468
|
+
|
|
1469
|
+
async function loadSchedules() {
|
|
1470
|
+
try {
|
|
1471
|
+
const res = await fetch('/api/schedules');
|
|
1472
|
+
const tasks = await res.json();
|
|
1473
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
1474
|
+
renderSchedules(list);
|
|
1475
|
+
} catch(e) {
|
|
1476
|
+
console.error('Failed to load schedules:', e);
|
|
1477
|
+
renderSchedules([]);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function renderSchedules(tasks) {
|
|
1482
|
+
const listEl = document.getElementById('schedules-list');
|
|
1483
|
+
const emptyEl = document.getElementById('schedules-empty');
|
|
1484
|
+
if (!tasks.length) {
|
|
1485
|
+
listEl.innerHTML = '';
|
|
1486
|
+
emptyEl.style.display = '';
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
emptyEl.style.display = 'none';
|
|
1490
|
+
listEl.innerHTML = tasks.map(t => `
|
|
1491
|
+
<div class="card" style="margin-bottom:12px;display:flex;align-items:center;gap:16px;">
|
|
1492
|
+
<div style="font-size:28px;">⏰</div>
|
|
1493
|
+
<div style="flex:1;">
|
|
1494
|
+
<div style="font-size:15px;font-weight:600;">${esc(t.name)}</div>
|
|
1495
|
+
<div style="font-size:12px;color:var(--text-muted);">${esc(t.description || '')}</div>
|
|
1496
|
+
<div style="font-size:11px;color:var(--text-dim);margin-top:4px;">
|
|
1497
|
+
${esc(t.schedule)} · ${t.outputChannel || 'web'} · Next: ${t.nextRun ? new Date(t.nextRun).toLocaleString() : 'N/A'}
|
|
1498
|
+
</div>
|
|
1499
|
+
</div>
|
|
1500
|
+
<div style="display:flex;gap:8px;align-items:center;">
|
|
1501
|
+
<label style="position:relative;display:inline-block;width:40px;height:22px;cursor:pointer;">
|
|
1502
|
+
<input type="checkbox" ${t.enabled ? 'checked' : ''} onchange="toggleSchedule('${t.id}', this.checked)" style="opacity:0;width:0;height:0;">
|
|
1503
|
+
<span style="position:absolute;inset:0;border-radius:11px;background:${t.enabled ? 'var(--green)' : 'var(--border)'};transition:0.3s;"></span>
|
|
1504
|
+
<span style="position:absolute;top:2px;left:${t.enabled ? '20px' : '2px'};width:18px;height:18px;border-radius:50%;background:white;transition:0.3s;"></span>
|
|
1505
|
+
</label>
|
|
1506
|
+
<button class="btn btn-sm btn-secondary" onclick="runScheduleNow('${t.id}')" title="Run now">▶️</button>
|
|
1507
|
+
<button class="btn btn-sm btn-secondary" onclick="editSchedule('${t.id}')" title="Edit">✏️</button>
|
|
1508
|
+
<button class="btn btn-sm btn-danger" onclick="deleteSchedule('${t.id}')" title="Delete">🗑</button>
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
`).join('');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
1515
|
+
|
|
1516
|
+
function showScheduleForm(task) {
|
|
1517
|
+
editingScheduleId = task ? task.id : null;
|
|
1518
|
+
document.getElementById('schedule-form').style.display = '';
|
|
1519
|
+
document.getElementById('schedule-form-title').textContent = task ? 'Edit Task' : 'New Scheduled Task';
|
|
1520
|
+
document.getElementById('sched-name').value = task ? task.name : '';
|
|
1521
|
+
document.getElementById('sched-frequency').value = task ? task.frequency : 'daily';
|
|
1522
|
+
document.getElementById('sched-time').value = task ? (task.time || '09:00') : '09:00';
|
|
1523
|
+
document.getElementById('sched-cron').value = task ? task.schedule : '';
|
|
1524
|
+
document.getElementById('sched-desc').value = task ? task.description : '';
|
|
1525
|
+
document.getElementById('sched-channel').value = task ? task.outputChannel : 'web';
|
|
1526
|
+
onSchedFreqChange();
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function hideScheduleForm() {
|
|
1530
|
+
document.getElementById('schedule-form').style.display = 'none';
|
|
1531
|
+
editingScheduleId = null;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function onSchedFreqChange() {
|
|
1535
|
+
const freq = document.getElementById('sched-frequency').value;
|
|
1536
|
+
document.getElementById('sched-time-group').style.display = freq === 'custom' ? 'none' : '';
|
|
1537
|
+
document.getElementById('sched-cron-group').style.display = freq === 'custom' ? '' : 'none';
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
async function saveSchedule() {
|
|
1541
|
+
const data = {
|
|
1542
|
+
name: document.getElementById('sched-name').value.trim(),
|
|
1543
|
+
frequency: document.getElementById('sched-frequency').value,
|
|
1544
|
+
time: document.getElementById('sched-time').value,
|
|
1545
|
+
schedule: document.getElementById('sched-frequency').value === 'custom' ? document.getElementById('sched-cron').value.trim() : '',
|
|
1546
|
+
description: document.getElementById('sched-desc').value.trim(),
|
|
1547
|
+
outputChannel: document.getElementById('sched-channel').value,
|
|
1548
|
+
enabled: true,
|
|
1549
|
+
};
|
|
1550
|
+
if (!data.name) { alert('Task name is required'); return; }
|
|
1551
|
+
try {
|
|
1552
|
+
if (editingScheduleId) {
|
|
1553
|
+
await fetch(`/api/schedules/${editingScheduleId}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
1554
|
+
} else {
|
|
1555
|
+
await fetch('/api/schedules', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
1556
|
+
}
|
|
1557
|
+
hideScheduleForm();
|
|
1558
|
+
loadSchedules();
|
|
1559
|
+
} catch(e) { alert('Failed to save: ' + e.message); }
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async function toggleSchedule(id, enabled) {
|
|
1563
|
+
await fetch(`/api/schedules/${id}`, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ enabled }) });
|
|
1564
|
+
loadSchedules();
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
async function deleteSchedule(id) {
|
|
1568
|
+
if (!confirm('Delete this task?')) return;
|
|
1569
|
+
await fetch(`/api/schedules/${id}`, { method: 'DELETE' });
|
|
1570
|
+
loadSchedules();
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
async function runScheduleNow(id) {
|
|
1574
|
+
await fetch(`/api/schedules/${id}/run`, { method: 'POST' });
|
|
1575
|
+
alert('Task executed!');
|
|
1576
|
+
loadSchedules();
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
async function editSchedule(id) {
|
|
1580
|
+
const res = await fetch('/api/schedules');
|
|
1581
|
+
const tasks = await res.json();
|
|
1582
|
+
const list = Array.isArray(tasks) ? tasks : [];
|
|
1583
|
+
const task = list.find(t => t.id === id);
|
|
1584
|
+
if (task) showScheduleForm(task);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// =============================================
|
|
1588
|
+
// === Voice Interaction ===
|
|
1589
|
+
// =============================================
|
|
1590
|
+
let voiceRecognition = null;
|
|
1591
|
+
let isRecording = false;
|
|
1592
|
+
|
|
1593
|
+
function toggleVoiceInput() {
|
|
1594
|
+
if (isRecording) {
|
|
1595
|
+
stopVoiceInput();
|
|
1596
|
+
} else {
|
|
1597
|
+
startVoiceInput();
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function startVoiceInput() {
|
|
1602
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1603
|
+
if (!SpeechRecognition) {
|
|
1604
|
+
alert('Speech recognition is not supported in this browser. Try Chrome.');
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
voiceRecognition = new SpeechRecognition();
|
|
1608
|
+
voiceRecognition.continuous = false;
|
|
1609
|
+
voiceRecognition.interimResults = true;
|
|
1610
|
+
voiceRecognition.lang = navigator.language || 'en-US';
|
|
1611
|
+
|
|
1612
|
+
voiceRecognition.onstart = () => {
|
|
1613
|
+
isRecording = true;
|
|
1614
|
+
const btn = document.getElementById('voice-btn');
|
|
1615
|
+
btn.style.background = 'var(--red)';
|
|
1616
|
+
btn.style.color = 'white';
|
|
1617
|
+
btn.style.borderColor = 'var(--red)';
|
|
1618
|
+
btn.textContent = '⏹';
|
|
1619
|
+
};
|
|
1620
|
+
|
|
1621
|
+
voiceRecognition.onresult = (event) => {
|
|
1622
|
+
let transcript = '';
|
|
1623
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
1624
|
+
transcript += event.results[i][0].transcript;
|
|
1625
|
+
}
|
|
1626
|
+
document.getElementById('chat-input').value = transcript;
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
voiceRecognition.onend = () => {
|
|
1630
|
+
isRecording = false;
|
|
1631
|
+
const btn = document.getElementById('voice-btn');
|
|
1632
|
+
btn.style.background = 'transparent';
|
|
1633
|
+
btn.style.color = '';
|
|
1634
|
+
btn.style.borderColor = 'var(--border)';
|
|
1635
|
+
btn.textContent = '🎤';
|
|
1636
|
+
// Auto-send if we got text
|
|
1637
|
+
const input = document.getElementById('chat-input');
|
|
1638
|
+
if (input.value.trim()) {
|
|
1639
|
+
sendMessage();
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
|
|
1643
|
+
voiceRecognition.onerror = (event) => {
|
|
1644
|
+
console.error('Speech recognition error:', event.error);
|
|
1645
|
+
isRecording = false;
|
|
1646
|
+
const btn = document.getElementById('voice-btn');
|
|
1647
|
+
btn.style.background = 'transparent';
|
|
1648
|
+
btn.style.color = '';
|
|
1649
|
+
btn.style.borderColor = 'var(--border)';
|
|
1650
|
+
btn.textContent = '🎤';
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
voiceRecognition.start();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function stopVoiceInput() {
|
|
1657
|
+
if (voiceRecognition) {
|
|
1658
|
+
voiceRecognition.stop();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function speakText(text) {
|
|
1663
|
+
if (!window.speechSynthesis) return;
|
|
1664
|
+
window.speechSynthesis.cancel();
|
|
1665
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
1666
|
+
utterance.lang = navigator.language || 'en-US';
|
|
1667
|
+
utterance.rate = 1.0;
|
|
1668
|
+
window.speechSynthesis.speak(utterance);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Patch message rendering to add TTS button to assistant messages
|
|
1672
|
+
const _origAppendMsg = typeof appendMessage === 'function' ? appendMessage : null;
|
|
1673
|
+
if (typeof window._patchedMsgRender === 'undefined') {
|
|
1674
|
+
window._patchedMsgRender = true;
|
|
1675
|
+
const observer = new MutationObserver((mutations) => {
|
|
1676
|
+
for (const m of mutations) {
|
|
1677
|
+
for (const node of m.addedNodes) {
|
|
1678
|
+
if (node.nodeType === 1 && node.classList?.contains('msg') && node.classList?.contains('assistant')) {
|
|
1679
|
+
const bubble = node.querySelector('.msg-bubble');
|
|
1680
|
+
if (bubble && !node.querySelector('.tts-btn')) {
|
|
1681
|
+
const btn = document.createElement('button');
|
|
1682
|
+
btn.className = 'tts-btn';
|
|
1683
|
+
btn.textContent = '🔊';
|
|
1684
|
+
btn.title = 'Read aloud';
|
|
1685
|
+
btn.style.cssText = 'background:none;border:1px solid var(--border);border-radius:50%;padding:4px 6px;cursor:pointer;font-size:14px;margin-top:4px;color:var(--text-muted);';
|
|
1686
|
+
btn.onclick = () => speakText(bubble.textContent);
|
|
1687
|
+
node.appendChild(btn);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
const chatMsgs = document.getElementById('chat-messages');
|
|
1694
|
+
if (chatMsgs) observer.observe(chatMsgs, { childList: true });
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// =============================================
|
|
1698
|
+
// === Image Generation Config ===
|
|
1699
|
+
// =============================================
|
|
1700
|
+
async function saveImageGenConfig() {
|
|
1701
|
+
const data = {
|
|
1702
|
+
openaiApiKey: document.getElementById('ig-openai-key').value.trim(),
|
|
1703
|
+
sdApiUrl: document.getElementById('ig-sd-url').value.trim(),
|
|
1704
|
+
replicateApiKey: document.getElementById('ig-replicate-key').value.trim(),
|
|
1705
|
+
};
|
|
1706
|
+
try {
|
|
1707
|
+
await fetch('/api/image-gen/config', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
1708
|
+
document.getElementById('ig-status').textContent = '✅ Configuration saved!';
|
|
1709
|
+
document.getElementById('ig-status').style.color = 'var(--green)';
|
|
1710
|
+
} catch(e) {
|
|
1711
|
+
document.getElementById('ig-status').textContent = '❌ Failed: ' + e.message;
|
|
1712
|
+
document.getElementById('ig-status').style.color = 'var(--red)';
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
|