crewswarm 0.9.2 → 0.9.3
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/README.md +22 -9
- package/apps/dashboard/dist/assets/{chat-core-Cx4sTxDd.js → chat-core-3KirthZA.js} +1 -1
- package/apps/dashboard/dist/assets/index-GSWxxEPO.js +2 -0
- package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
- package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +1 -0
- package/apps/dashboard/dist/index.html +82 -11
- package/apps/vibe/README.md +2 -2
- package/apps/vibe/package.json +1 -1
- package/apps/vibe/server.mjs +3 -3
- package/crew-lead.mjs +34 -4
- package/lib/bridges/gateway-ws.mjs +4 -0
- package/lib/crew-lead/chat-handler.mjs +34 -0
- package/lib/crew-lead/http-server.mjs +55 -14
- package/lib/crew-lead/llm-caller.mjs +24 -8
- package/lib/crew-lead/prompts.mjs +7 -0
- package/lib/crew-lead/wave-dispatcher.mjs +15 -3
- package/lib/crew-lead/ws-router.mjs +219 -27
- package/lib/engines/engine-registry.mjs +9 -0
- package/lib/engines/rt-envelope.mjs +1 -0
- package/lib/engines/runners.mjs +5 -2
- package/lib/runtime/paths.mjs +12 -8
- package/package.json +35 -15
- package/scripts/capture-build-flow.mjs +118 -0
- package/scripts/coverage-report.mjs +209 -0
- package/scripts/coverage-summary.mjs +47 -0
- package/scripts/dashboard-validation.mjs +74 -0
- package/scripts/dashboard.mjs +560 -70
- package/scripts/live-bridge-matrix.mjs +79 -0
- package/scripts/live-cli-matrix.mjs +166 -0
- package/scripts/live-crewchat-check.mjs +42 -0
- package/scripts/live-engine-matrix.mjs +50 -0
- package/scripts/live-provider-failover-matrix.mjs +107 -0
- package/scripts/live-provider-matrix.mjs +228 -0
- package/scripts/restart-all-from-repo.sh +4 -4
- package/scripts/smoke-dispatch.mjs +4 -1
- package/scripts/test-blast-radius.mjs +204 -0
- package/scripts/test-report-summary.mjs +88 -0
- package/scripts/test-reporter.mjs +651 -0
- package/scripts/test-rerun.mjs +136 -0
- package/scripts/tmux-bridge +130 -0
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
- package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
- package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
- package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
- package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/dashboard/index.html +0 -6529
- package/apps/dashboard/package.json +0 -15
- package/apps/dashboard/src/app.js +0 -2828
- package/apps/dashboard/src/app.js.br +0 -0
- package/apps/dashboard/src/app.js.gz +0 -0
- package/apps/dashboard/src/chat/chat-actions.js +0 -1847
- package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
- package/apps/dashboard/src/chat/unified-messages.js +0 -327
- package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
- package/apps/dashboard/src/cli-process.js +0 -208
- package/apps/dashboard/src/cli-process.js.br +0 -0
- package/apps/dashboard/src/cli-process.js.gz +0 -0
- package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
- package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
- package/apps/dashboard/src/core/api.js +0 -18
- package/apps/dashboard/src/core/api.js.br +0 -0
- package/apps/dashboard/src/core/dom.js +0 -228
- package/apps/dashboard/src/core/dom.js.br +0 -0
- package/apps/dashboard/src/core/state.js +0 -91
- package/apps/dashboard/src/core/state.js.br +0 -0
- package/apps/dashboard/src/core/task-manager.js +0 -134
- package/apps/dashboard/src/core/task-manager.js.br +0 -0
- package/apps/dashboard/src/orchestration-status.js +0 -127
- package/apps/dashboard/src/orchestration-status.js.br +0 -0
- package/apps/dashboard/src/setup-wizard.js +0 -562
- package/apps/dashboard/src/setup-wizard.js.br +0 -0
- package/apps/dashboard/src/styles.css +0 -2085
- package/apps/dashboard/src/styles.css.br +0 -0
- package/apps/dashboard/src/styles.css.gz +0 -0
- package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
- package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
- package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/comms-tab.js +0 -955
- package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
- package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/engines-tab.js +0 -175
- package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/memory-tab.js +0 -182
- package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/models-tab.js +0 -450
- package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
- package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js +0 -663
- package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
- package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
- package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/services-tab.js +0 -202
- package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/settings-tab.js +0 -861
- package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/skills-tab.js +0 -284
- package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/spending-tab.js +0 -173
- package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
- package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
- package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/usage-tab.js +0 -390
- package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/waves-tab.js +0 -238
- package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
- package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
- package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
- package/apps/vibe/.crew/cost.json +0 -17
- package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
- package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
- package/apps/vibe/.crew/sandbox.json +0 -7
- package/apps/vibe/.crew/session.json +0 -330
- package/apps/vibe/.crew/training-data.jsonl +0 -0
- package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
- package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
- package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
- package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
- package/apps/vibe/ARCHITECTURE.md +0 -3393
- package/apps/vibe/QUICK-REFERENCE.md +0 -211
- package/apps/vibe/ROADMAP.md +0 -41
- package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
- package/apps/vibe/VISUAL-GUIDE.md +0 -378
- package/apps/vibe/capture-demo.mjs +0 -160
- package/apps/vibe/capture-full-demo.mjs +0 -255
- package/apps/vibe/capture-quickstart.mjs +0 -256
- package/apps/vibe/capture-vibe-assets.mjs +0 -71
- package/apps/vibe/capture-vibe-video.mjs +0 -260
- package/apps/vibe/check-buttons.js +0 -41
- package/apps/vibe/diagnose.html +0 -106
- package/apps/vibe/fix-buttons.js +0 -103
- package/apps/vibe/index.html +0 -3404
- package/apps/vibe/package-lock.json +0 -920
- package/apps/vibe/scripts/studio-pty-host.py +0 -117
- package/apps/vibe/src/main.js +0 -2940
- package/apps/vibe/src/register-all-languages.js +0 -98
- package/apps/vibe/start-studio.sh +0 -11
- package/apps/vibe/test/accessibility-tests.js +0 -77
- package/apps/vibe/test/browser-performance-audit.mjs +0 -205
- package/apps/vibe/test/performance-tests.js +0 -120
- package/apps/vibe/test/security-tests.js +0 -213
- package/apps/vibe/tests/e2e.local.mjs +0 -54
- package/apps/vibe/tests/server.smoke.mjs +0 -106
- package/apps/vibe/update_website.mjs +0 -74
- package/apps/vibe/vite.config.js +0 -19
- package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
- package/lib/engines/rt-envelope.mjs.backup-current +0 -870
|
@@ -1,955 +0,0 @@
|
|
|
1
|
-
import { getJSON, postJSON } from '../core/api.js';
|
|
2
|
-
import { escHtml, showNotification } from '../core/dom.js';
|
|
3
|
-
|
|
4
|
-
let showSettings = () => {};
|
|
5
|
-
let showSettingsTab = () => {};
|
|
6
|
-
let _waSavedContactNames = {};
|
|
7
|
-
let _waSavedUserRouting = {};
|
|
8
|
-
let _tgSavedContactNames = {};
|
|
9
|
-
let _tgSavedUserRouting = {};
|
|
10
|
-
let _tgSavedTopicRouting = {};
|
|
11
|
-
|
|
12
|
-
export function initCommsTab(deps = {}) {
|
|
13
|
-
showSettings = deps.showSettings || showSettings;
|
|
14
|
-
showSettingsTab = deps.showSettingsTab || showSettingsTab;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function showMessaging() {
|
|
18
|
-
showSettings();
|
|
19
|
-
showSettingsTab('comms');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function loadCommsTabData() {
|
|
23
|
-
await Promise.allSettled([
|
|
24
|
-
loadTgStatus(),
|
|
25
|
-
loadTelegramSessions(),
|
|
26
|
-
loadTgMessages(),
|
|
27
|
-
loadTgConfig(),
|
|
28
|
-
loadWaStatus(),
|
|
29
|
-
loadWaConfig(),
|
|
30
|
-
loadWaMessages(),
|
|
31
|
-
]);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function loadTgStatus() {
|
|
35
|
-
try {
|
|
36
|
-
const d = await getJSON('/api/telegram/status');
|
|
37
|
-
const badge = document.getElementById('tgStatusBadge');
|
|
38
|
-
if (!badge) return;
|
|
39
|
-
if (d.running) {
|
|
40
|
-
badge.textContent = d.botName ? '● @' + d.botName : '● running';
|
|
41
|
-
badge.className = 'status-badge status-active';
|
|
42
|
-
} else {
|
|
43
|
-
badge.textContent = '● stopped';
|
|
44
|
-
badge.className = 'status-badge status-stopped';
|
|
45
|
-
}
|
|
46
|
-
} catch {}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function renderTgContactRows() {
|
|
50
|
-
const listEl = document.getElementById('tgContactNamesList');
|
|
51
|
-
if (!listEl) return;
|
|
52
|
-
const raw = (document.getElementById('tgAllowedIds')?.value || '').trim();
|
|
53
|
-
const ids = raw ? raw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)) : [];
|
|
54
|
-
listEl.innerHTML = '';
|
|
55
|
-
if (!ids.length) return;
|
|
56
|
-
const title = document.createElement('label');
|
|
57
|
-
title.style.cssText = 'display:block;margin-bottom:6px;font-size:12px;color:var(--text-2);';
|
|
58
|
-
title.textContent = 'Contact names & routing';
|
|
59
|
-
listEl.appendChild(title);
|
|
60
|
-
ids.forEach(id => {
|
|
61
|
-
// Container for 2-line layout
|
|
62
|
-
const container = document.createElement('div');
|
|
63
|
-
container.style.cssText = 'margin-bottom:12px;padding:12px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;';
|
|
64
|
-
|
|
65
|
-
// Line 1: Chat ID + Name
|
|
66
|
-
const row1 = document.createElement('div');
|
|
67
|
-
row1.style.cssText = 'display:grid;grid-template-columns:100px 1fr;gap:8px;margin-bottom:8px;align-items:center;';
|
|
68
|
-
|
|
69
|
-
const span = document.createElement('span');
|
|
70
|
-
span.style.cssText = 'font-size:11px;color:var(--text-3);font-family:monospace;';
|
|
71
|
-
span.textContent = String(id);
|
|
72
|
-
|
|
73
|
-
const input = document.createElement('input');
|
|
74
|
-
input.id = 'tgContact-' + id;
|
|
75
|
-
input.placeholder = 'Name (e.g. Jeff)';
|
|
76
|
-
input.value = _tgSavedContactNames[String(id)] || '';
|
|
77
|
-
input.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
|
|
78
|
-
|
|
79
|
-
row1.appendChild(span);
|
|
80
|
-
row1.appendChild(input);
|
|
81
|
-
|
|
82
|
-
// Line 2: Routing label + dropdown
|
|
83
|
-
const row2 = document.createElement('div');
|
|
84
|
-
row2.style.cssText = 'display:grid;grid-template-columns:100px 1fr;gap:8px;align-items:center;';
|
|
85
|
-
|
|
86
|
-
const routeLabel = document.createElement('span');
|
|
87
|
-
routeLabel.style.cssText = 'font-size:10px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em;';
|
|
88
|
-
routeLabel.textContent = 'Routes to →';
|
|
89
|
-
|
|
90
|
-
const routeSelect = document.createElement('select');
|
|
91
|
-
routeSelect.id = 'tgRoute-' + id;
|
|
92
|
-
routeSelect.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
|
|
93
|
-
|
|
94
|
-
// Get current routing for this chat ID
|
|
95
|
-
const currentRoute = _tgSavedUserRouting[String(id)] || '';
|
|
96
|
-
|
|
97
|
-
const agents = [
|
|
98
|
-
'crew-lead', 'crew-main', 'crew-coder', 'crew-pm', 'crew-qa',
|
|
99
|
-
'crew-fixer', 'crew-security', 'crew-frontend', 'crew-coder-front',
|
|
100
|
-
'crew-coder-back', 'crew-github', 'crew-copywriter', 'crew-researcher',
|
|
101
|
-
'crew-architect', 'crew-seo', 'crew-ml', 'crew-mega', 'crew-loco'
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
// Default option
|
|
105
|
-
const defaultOpt = document.createElement('option');
|
|
106
|
-
defaultOpt.value = '';
|
|
107
|
-
defaultOpt.textContent = '— default (crew-lead) —';
|
|
108
|
-
routeSelect.appendChild(defaultOpt);
|
|
109
|
-
|
|
110
|
-
// Agent options
|
|
111
|
-
agents.forEach(agent => {
|
|
112
|
-
const opt = document.createElement('option');
|
|
113
|
-
opt.value = agent;
|
|
114
|
-
opt.textContent = agent;
|
|
115
|
-
if (agent === currentRoute) opt.selected = true;
|
|
116
|
-
routeSelect.appendChild(opt);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Update state immediately when routing changes
|
|
120
|
-
routeSelect.addEventListener('change', (e) => {
|
|
121
|
-
const newAgent = e.target.value;
|
|
122
|
-
if (newAgent) {
|
|
123
|
-
_tgSavedUserRouting[String(id)] = newAgent;
|
|
124
|
-
} else {
|
|
125
|
-
delete _tgSavedUserRouting[String(id)];
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
row2.appendChild(routeLabel);
|
|
130
|
-
row2.appendChild(routeSelect);
|
|
131
|
-
|
|
132
|
-
container.appendChild(row1);
|
|
133
|
-
container.appendChild(row2);
|
|
134
|
-
listEl.appendChild(container);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export async function loadTgConfig() {
|
|
139
|
-
try {
|
|
140
|
-
const d = await getJSON('/api/telegram/config');
|
|
141
|
-
if (d.token) document.getElementById('tgTokenInput').value = d.token;
|
|
142
|
-
const ids = d.allowedChatIds && d.allowedChatIds.length ? d.allowedChatIds : [];
|
|
143
|
-
document.getElementById('tgAllowedIds').value = ids.join(', ');
|
|
144
|
-
_tgSavedContactNames = d.contactNames || {};
|
|
145
|
-
_tgSavedUserRouting = d.userRouting || {};
|
|
146
|
-
_tgSavedTopicRouting = d.topicRouting || {};
|
|
147
|
-
renderTgContactRows();
|
|
148
|
-
renderTgTopicRouting();
|
|
149
|
-
} catch {}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function renderTgTopicRouting() {
|
|
153
|
-
const container = document.getElementById('tgTopicRoutingContainer');
|
|
154
|
-
if (!container) return;
|
|
155
|
-
|
|
156
|
-
// Clear existing content
|
|
157
|
-
container.innerHTML = '';
|
|
158
|
-
|
|
159
|
-
// Title and explanation
|
|
160
|
-
const header = document.createElement('div');
|
|
161
|
-
header.style.cssText = 'margin-bottom:12px;';
|
|
162
|
-
header.innerHTML = `
|
|
163
|
-
<div style="font-size:13px;font-weight:600;margin-bottom:4px;">📌 Topic Routing (Optional)</div>
|
|
164
|
-
<div style="font-size:11px;color:var(--text-3);line-height:1.4;">
|
|
165
|
-
Route different topics in Forum groups to different agents.
|
|
166
|
-
</div>
|
|
167
|
-
`;
|
|
168
|
-
container.appendChild(header);
|
|
169
|
-
|
|
170
|
-
// Action buttons
|
|
171
|
-
const btnRow = document.createElement('div');
|
|
172
|
-
btnRow.style.cssText = 'display:flex;gap:8px;margin-bottom:12px;';
|
|
173
|
-
|
|
174
|
-
const discoverBtn = document.createElement('button');
|
|
175
|
-
discoverBtn.textContent = '🔍 Auto-discover Topics';
|
|
176
|
-
discoverBtn.className = 'btn-ghost';
|
|
177
|
-
discoverBtn.style.cssText = 'flex:1;font-size:12px;';
|
|
178
|
-
discoverBtn.onclick = () => discoverTgTopics();
|
|
179
|
-
|
|
180
|
-
const addGroupBtn = document.createElement('button');
|
|
181
|
-
addGroupBtn.textContent = '➕ Add New Group';
|
|
182
|
-
addGroupBtn.className = 'btn-ghost';
|
|
183
|
-
addGroupBtn.style.cssText = 'flex:1;font-size:12px;';
|
|
184
|
-
addGroupBtn.onclick = () => addTgNewGroup();
|
|
185
|
-
|
|
186
|
-
btnRow.appendChild(discoverBtn);
|
|
187
|
-
btnRow.appendChild(addGroupBtn);
|
|
188
|
-
container.appendChild(btnRow);
|
|
189
|
-
|
|
190
|
-
// Topics list container
|
|
191
|
-
const listDiv = document.createElement('div');
|
|
192
|
-
listDiv.id = 'tgTopicsList';
|
|
193
|
-
listDiv.style.cssText = 'margin-bottom:12px;';
|
|
194
|
-
container.appendChild(listDiv);
|
|
195
|
-
|
|
196
|
-
// Render existing topics
|
|
197
|
-
renderTgTopicsList();
|
|
198
|
-
|
|
199
|
-
// Advanced: JSON editor (collapsed)
|
|
200
|
-
const advancedToggle = document.createElement('details');
|
|
201
|
-
advancedToggle.style.cssText = 'margin-top:12px;';
|
|
202
|
-
advancedToggle.innerHTML = `
|
|
203
|
-
<summary style="cursor:pointer;font-size:11px;color:var(--text-3);padding:6px 0;">
|
|
204
|
-
⚙️ Advanced: Edit JSON directly
|
|
205
|
-
</summary>
|
|
206
|
-
`;
|
|
207
|
-
|
|
208
|
-
const textarea = document.createElement('textarea');
|
|
209
|
-
textarea.id = 'tgTopicRoutingJson';
|
|
210
|
-
textarea.placeholder = `{
|
|
211
|
-
"-100123456789": {
|
|
212
|
-
"5": "crew-coder",
|
|
213
|
-
"8": "crew-copywriter"
|
|
214
|
-
}
|
|
215
|
-
}`;
|
|
216
|
-
textarea.value = Object.keys(_tgSavedTopicRouting).length ? JSON.stringify(_tgSavedTopicRouting, null, 2) : '';
|
|
217
|
-
textarea.style.cssText = 'width:100%;min-height:100px;font-family:monospace;font-size:11px;padding:8px;background:var(--bg-1);border:1px solid var(--border);border-radius:4px;color:var(--text-1);resize:vertical;margin-top:8px;';
|
|
218
|
-
advancedToggle.appendChild(textarea);
|
|
219
|
-
|
|
220
|
-
container.appendChild(advancedToggle);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function renderTgTopicsList() {
|
|
224
|
-
const listDiv = document.getElementById('tgTopicsList');
|
|
225
|
-
if (!listDiv) return;
|
|
226
|
-
|
|
227
|
-
listDiv.innerHTML = '';
|
|
228
|
-
|
|
229
|
-
const agents = [
|
|
230
|
-
'crew-lead', 'crew-main', 'crew-coder', 'crew-pm', 'crew-qa',
|
|
231
|
-
'crew-fixer', 'crew-security', 'crew-frontend', 'crew-coder-front',
|
|
232
|
-
'crew-coder-back', 'crew-github', 'crew-copywriter', 'crew-researcher',
|
|
233
|
-
'crew-architect', 'crew-seo', 'crew-ml', 'crew-mega', 'crew-loco'
|
|
234
|
-
];
|
|
235
|
-
|
|
236
|
-
// Group topics by chatId
|
|
237
|
-
const groupedTopics = {};
|
|
238
|
-
Object.entries(_tgSavedTopicRouting).forEach(([key, value]) => {
|
|
239
|
-
if (key.startsWith('_')) return; // Skip comment fields
|
|
240
|
-
if (typeof value === 'object') {
|
|
241
|
-
Object.entries(value).forEach(([topicId, agent]) => {
|
|
242
|
-
if (!groupedTopics[key]) groupedTopics[key] = [];
|
|
243
|
-
groupedTopics[key].push({ topicId, agent });
|
|
244
|
-
});
|
|
245
|
-
} else {
|
|
246
|
-
const [chatId, topicId] = key.split(':');
|
|
247
|
-
if (!groupedTopics[chatId]) groupedTopics[chatId] = [];
|
|
248
|
-
groupedTopics[chatId].push({ topicId, agent: value });
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
if (Object.keys(groupedTopics).length === 0) {
|
|
253
|
-
const emptyMsg = document.createElement('div');
|
|
254
|
-
emptyMsg.style.cssText = 'padding:12px;text-align:center;color:var(--text-3);font-size:11px;background:var(--bg-1);border:1px dashed var(--border);border-radius:4px;';
|
|
255
|
-
emptyMsg.textContent = 'No topics configured. Click "Auto-discover" or "Add Manually" above.';
|
|
256
|
-
listDiv.appendChild(emptyMsg);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
let globalIdx = 0;
|
|
261
|
-
|
|
262
|
-
// Render each group
|
|
263
|
-
Object.entries(groupedTopics).forEach(([chatId, topics]) => {
|
|
264
|
-
// Group container (class for saveTgConfig selector)
|
|
265
|
-
const groupContainer = document.createElement('div');
|
|
266
|
-
groupContainer.className = 'tg-topic-group';
|
|
267
|
-
groupContainer.style.cssText = 'margin-bottom:16px;padding:12px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;';
|
|
268
|
-
|
|
269
|
-
// Group header with chat ID
|
|
270
|
-
const groupHeader = document.createElement('div');
|
|
271
|
-
groupHeader.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border);';
|
|
272
|
-
|
|
273
|
-
const groupLabel = document.createElement('span');
|
|
274
|
-
groupLabel.textContent = 'Group ID:';
|
|
275
|
-
groupLabel.style.cssText = 'font-size:11px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em;';
|
|
276
|
-
|
|
277
|
-
const chatIdInput = document.createElement('input');
|
|
278
|
-
chatIdInput.value = chatId;
|
|
279
|
-
chatIdInput.className = 'tg-topic-group-chatid';
|
|
280
|
-
chatIdInput.dataset.groupChatId = chatId;
|
|
281
|
-
chatIdInput.style.cssText = 'flex:1;font-size:11px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:3px;color:var(--text-1);font-family:monospace;';
|
|
282
|
-
|
|
283
|
-
// Add topic to this group button
|
|
284
|
-
const addTopicBtn = document.createElement('button');
|
|
285
|
-
addTopicBtn.textContent = '➕';
|
|
286
|
-
addTopicBtn.className = 'btn-ghost';
|
|
287
|
-
addTopicBtn.title = 'Add topic to this group';
|
|
288
|
-
addTopicBtn.style.cssText = 'font-size:14px;padding:4px 8px;width:32px;height:28px;';
|
|
289
|
-
addTopicBtn.addEventListener('click', (e) => {
|
|
290
|
-
e.preventDefault();
|
|
291
|
-
addTgTopicToGroup(chatId);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// Delete group button
|
|
295
|
-
const deleteGroupBtn = document.createElement('button');
|
|
296
|
-
deleteGroupBtn.textContent = '🗑';
|
|
297
|
-
deleteGroupBtn.className = 'btn-ghost';
|
|
298
|
-
deleteGroupBtn.title = 'Delete entire group and all topics';
|
|
299
|
-
deleteGroupBtn.style.cssText = 'font-size:14px;padding:4px 8px;width:32px;height:28px;';
|
|
300
|
-
deleteGroupBtn.addEventListener('click', (e) => {
|
|
301
|
-
e.preventDefault();
|
|
302
|
-
e.stopPropagation();
|
|
303
|
-
if (window.confirm(`Delete group ${chatId} and all topics?`)) {
|
|
304
|
-
delete _tgSavedTopicRouting[String(chatId)];
|
|
305
|
-
renderTgTopicsList();
|
|
306
|
-
showNotification('Group removed - click Save to persist');
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
groupHeader.appendChild(groupLabel);
|
|
311
|
-
groupHeader.appendChild(chatIdInput);
|
|
312
|
-
groupHeader.appendChild(addTopicBtn);
|
|
313
|
-
groupHeader.appendChild(deleteGroupBtn);
|
|
314
|
-
groupContainer.appendChild(groupHeader);
|
|
315
|
-
|
|
316
|
-
// Topic rows under this group
|
|
317
|
-
topics.forEach(topic => {
|
|
318
|
-
const row = document.createElement('div');
|
|
319
|
-
row.style.cssText = 'display:grid;grid-template-columns:80px 1fr 36px;gap:8px;align-items:center;padding:6px 8px;margin-bottom:4px;';
|
|
320
|
-
row.dataset.idx = globalIdx;
|
|
321
|
-
row.dataset.chatId = chatId;
|
|
322
|
-
row.dataset.originalTopicId = topic.topicId; // Track for renames
|
|
323
|
-
|
|
324
|
-
// Topic ID input
|
|
325
|
-
const topicIdInput = document.createElement('input');
|
|
326
|
-
topicIdInput.value = topic.topicId;
|
|
327
|
-
topicIdInput.placeholder = 'Topic 5';
|
|
328
|
-
topicIdInput.className = 'tg-topic-id';
|
|
329
|
-
topicIdInput.style.cssText = 'font-size:11px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:3px;color:var(--text-1);font-family:monospace;text-align:center;';
|
|
330
|
-
|
|
331
|
-
// Agent dropdown
|
|
332
|
-
const agentSelect = document.createElement('select');
|
|
333
|
-
agentSelect.className = 'tg-topic-agent';
|
|
334
|
-
agentSelect.style.cssText = 'font-size:11px;padding:5px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:3px;color:var(--text-1);';
|
|
335
|
-
agents.forEach(agent => {
|
|
336
|
-
const opt = document.createElement('option');
|
|
337
|
-
opt.value = agent;
|
|
338
|
-
opt.textContent = agent;
|
|
339
|
-
if (agent === topic.agent) opt.selected = true;
|
|
340
|
-
agentSelect.appendChild(opt);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// Update state immediately when agent changes
|
|
344
|
-
agentSelect.addEventListener('change', (e) => {
|
|
345
|
-
const newAgent = e.target.value;
|
|
346
|
-
const topicIdInput = row.querySelector('.tg-topic-id');
|
|
347
|
-
const topicId = topicIdInput ? topicIdInput.value.trim() : topic.topicId;
|
|
348
|
-
|
|
349
|
-
if (topicId && _tgSavedTopicRouting[chatId]) {
|
|
350
|
-
_tgSavedTopicRouting[chatId][topicId] = newAgent;
|
|
351
|
-
|
|
352
|
-
// Update JSON editor too
|
|
353
|
-
const topicJsonEl = document.getElementById('tgTopicRoutingJson');
|
|
354
|
-
if (topicJsonEl) {
|
|
355
|
-
topicJsonEl.value = JSON.stringify(_tgSavedTopicRouting, null, 2);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Remove button
|
|
361
|
-
const removeBtn = document.createElement('button');
|
|
362
|
-
removeBtn.textContent = '🗑';
|
|
363
|
-
removeBtn.className = 'btn-ghost';
|
|
364
|
-
removeBtn.title = 'Remove this topic';
|
|
365
|
-
removeBtn.style.cssText = 'font-size:14px;padding:4px;width:28px;height:28px;';
|
|
366
|
-
removeBtn.addEventListener('click', (e) => {
|
|
367
|
-
e.preventDefault();
|
|
368
|
-
e.stopPropagation();
|
|
369
|
-
if (window.confirm(`Remove topic ${topic.topicId}?`)) {
|
|
370
|
-
// Direct delete from nested structure (fast - no rebuild)
|
|
371
|
-
if (_tgSavedTopicRouting[chatId] && _tgSavedTopicRouting[chatId][topic.topicId]) {
|
|
372
|
-
delete _tgSavedTopicRouting[chatId][topic.topicId];
|
|
373
|
-
|
|
374
|
-
// If group is now empty, remove the group key
|
|
375
|
-
if (Object.keys(_tgSavedTopicRouting[chatId]).length === 0) {
|
|
376
|
-
delete _tgSavedTopicRouting[chatId];
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Re-render and update JSON editor
|
|
380
|
-
renderTgTopicsList();
|
|
381
|
-
showNotification(`Topic ${topic.topicId} removed - click Save to persist`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
row.appendChild(topicIdInput);
|
|
387
|
-
row.appendChild(agentSelect);
|
|
388
|
-
row.appendChild(removeBtn);
|
|
389
|
-
groupContainer.appendChild(row);
|
|
390
|
-
|
|
391
|
-
globalIdx++;
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
listDiv.appendChild(groupContainer);
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// Keep JSON editor in sync when state changes (e.g. after delete/add)
|
|
398
|
-
const topicJsonEl = document.getElementById('tgTopicRoutingJson');
|
|
399
|
-
if (topicJsonEl) {
|
|
400
|
-
topicJsonEl.value = Object.keys(_tgSavedTopicRouting).length ? JSON.stringify(_tgSavedTopicRouting, null, 2) : '';
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export function addTgNewGroup() {
|
|
405
|
-
// Add a brand new group
|
|
406
|
-
const newGroupId = prompt('Enter Group ID (e.g., -100123456789):');
|
|
407
|
-
if (!newGroupId || !newGroupId.trim()) return;
|
|
408
|
-
|
|
409
|
-
const groupId = newGroupId.trim();
|
|
410
|
-
if (!_tgSavedTopicRouting[groupId]) {
|
|
411
|
-
_tgSavedTopicRouting[groupId] = {};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Add first topic to the new group
|
|
415
|
-
const newTopicId = '1';
|
|
416
|
-
_tgSavedTopicRouting[groupId][newTopicId] = 'crew-lead';
|
|
417
|
-
renderTgTopicsList();
|
|
418
|
-
showNotification(`Added group ${groupId}`);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
export function addTgTopicToGroup(chatId) {
|
|
422
|
-
// Add topic to specific group
|
|
423
|
-
if (!_tgSavedTopicRouting[chatId]) {
|
|
424
|
-
_tgSavedTopicRouting[chatId] = {};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Generate new topic ID (find highest + 1)
|
|
428
|
-
const existingIds = Object.keys(_tgSavedTopicRouting[chatId]).map(id => parseInt(id, 10)).filter(n => !isNaN(n));
|
|
429
|
-
const newTopicId = existingIds.length > 0 ? String(Math.max(...existingIds) + 1) : '1';
|
|
430
|
-
|
|
431
|
-
_tgSavedTopicRouting[chatId][newTopicId] = 'crew-lead';
|
|
432
|
-
renderTgTopicsList();
|
|
433
|
-
showNotification(`Added topic ${newTopicId} to group`);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
export function addTgTopicRow() {
|
|
437
|
-
// Legacy: Add to first existing group
|
|
438
|
-
const existingChatIds = Object.keys(_tgSavedTopicRouting).filter(k => !k.startsWith('_'));
|
|
439
|
-
if (existingChatIds.length === 0) {
|
|
440
|
-
// No groups exist, create one
|
|
441
|
-
addTgNewGroup();
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Add to first group
|
|
446
|
-
addTgTopicToGroup(existingChatIds[0]);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
export function removeTgGroup(chatId) {
|
|
450
|
-
// Remove entire group from topic routing
|
|
451
|
-
delete _tgSavedTopicRouting[String(chatId)];
|
|
452
|
-
renderTgTopicsList();
|
|
453
|
-
showNotification(`Removed group ${chatId} - don't forget to Save config!`);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export function removeTgTopicRow(idx) {
|
|
457
|
-
// Rebuild topic routing without this entry
|
|
458
|
-
const topics = [];
|
|
459
|
-
Object.entries(_tgSavedTopicRouting).forEach(([key, value]) => {
|
|
460
|
-
if (key.startsWith('_')) return;
|
|
461
|
-
if (typeof value === 'object') {
|
|
462
|
-
Object.entries(value).forEach(([topicId, agent]) => {
|
|
463
|
-
topics.push({ chatId: key, topicId, agent });
|
|
464
|
-
});
|
|
465
|
-
} else {
|
|
466
|
-
const [chatId, topicId] = key.split(':');
|
|
467
|
-
topics.push({ chatId, topicId, agent: value });
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
topics.splice(idx, 1);
|
|
472
|
-
|
|
473
|
-
// Rebuild nested format
|
|
474
|
-
_tgSavedTopicRouting = {};
|
|
475
|
-
topics.forEach(t => {
|
|
476
|
-
if (!_tgSavedTopicRouting[t.chatId]) _tgSavedTopicRouting[t.chatId] = {};
|
|
477
|
-
_tgSavedTopicRouting[t.chatId][t.topicId] = t.agent;
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
renderTgTopicsList();
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export async function discoverTgTopics() {
|
|
484
|
-
try {
|
|
485
|
-
const allTopics = await getJSON('/api/telegram/discover-topics');
|
|
486
|
-
if (!allTopics.length) {
|
|
487
|
-
showNotification('No topics found in logs. Send messages to topics first.', true);
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Group discovered topics by chatId
|
|
492
|
-
const groupedDiscovered = {};
|
|
493
|
-
allTopics.forEach(t => {
|
|
494
|
-
if (!groupedDiscovered[t.chatId]) {
|
|
495
|
-
groupedDiscovered[t.chatId] = [];
|
|
496
|
-
}
|
|
497
|
-
groupedDiscovered[t.chatId].push(t);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
// Build modal with checkboxes per-TOPIC (not per-group)
|
|
501
|
-
const existingGroups = Object.keys(_tgSavedTopicRouting).filter(k => !k.startsWith('_'));
|
|
502
|
-
|
|
503
|
-
let modalHtml = '<div style="max-height:400px;overflow-y:auto;">';
|
|
504
|
-
Object.entries(groupedDiscovered).forEach(([chatId, topics]) => {
|
|
505
|
-
modalHtml += `
|
|
506
|
-
<div style="background:var(--bg-1);border:1px solid var(--border);border-radius:6px;padding:12px;margin-bottom:12px;">
|
|
507
|
-
<div style="font-weight:600;margin-bottom:8px;font-family:monospace;font-size:12px;">
|
|
508
|
-
Group ${chatId}
|
|
509
|
-
</div>
|
|
510
|
-
<div style="margin-left:12px;">
|
|
511
|
-
`;
|
|
512
|
-
|
|
513
|
-
// Checkbox for each topic
|
|
514
|
-
topics.forEach(t => {
|
|
515
|
-
const topicId = String(t.threadId);
|
|
516
|
-
const alreadyExists = _tgSavedTopicRouting[chatId]?.[topicId];
|
|
517
|
-
const checked = !alreadyExists ? 'checked' : '';
|
|
518
|
-
const disabledLabel = alreadyExists ? ' <span style="font-size:10px;color:var(--text-3);">(already configured)</span>' : '';
|
|
519
|
-
|
|
520
|
-
modalHtml += `
|
|
521
|
-
<div style="margin-bottom:6px;display:flex;align-items:center;gap:8px;">
|
|
522
|
-
<input type="checkbox" class="discover-topic-check" data-chat-id="${chatId}" data-thread-id="${topicId}" ${checked} ${alreadyExists ? 'disabled' : ''}>
|
|
523
|
-
<span style="font-size:11px;color:var(--text-2);">Topic ${topicId}${disabledLabel}</span>
|
|
524
|
-
</div>
|
|
525
|
-
`;
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
modalHtml += `
|
|
529
|
-
</div>
|
|
530
|
-
</div>
|
|
531
|
-
`;
|
|
532
|
-
});
|
|
533
|
-
modalHtml += '</div>';
|
|
534
|
-
|
|
535
|
-
// Simple confirmation with HTML
|
|
536
|
-
const modal = document.createElement('div');
|
|
537
|
-
modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
|
538
|
-
modal.innerHTML = `
|
|
539
|
-
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:20px;max-width:600px;width:90%;">
|
|
540
|
-
<h3 style="margin:0 0 16px;font-size:16px;">Discovered Topics</h3>
|
|
541
|
-
<div style="font-size:11px;color:var(--text-3);margin-bottom:12px;">
|
|
542
|
-
✓ Select which topics to add. Already configured topics are disabled.
|
|
543
|
-
</div>
|
|
544
|
-
${modalHtml}
|
|
545
|
-
<div style="display:flex;gap:8px;margin-top:16px;justify-content:flex-end;">
|
|
546
|
-
<button class="btn-ghost" id="discoverCancel">Cancel</button>
|
|
547
|
-
<button class="btn-primary" id="discoverConfirm">Add Selected</button>
|
|
548
|
-
</div>
|
|
549
|
-
</div>
|
|
550
|
-
`;
|
|
551
|
-
document.body.appendChild(modal);
|
|
552
|
-
|
|
553
|
-
document.getElementById('discoverCancel').onclick = () => {
|
|
554
|
-
document.body.removeChild(modal);
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
document.getElementById('discoverConfirm').onclick = () => {
|
|
558
|
-
const checkedTopics = Array.from(modal.querySelectorAll('.discover-topic-check:checked'));
|
|
559
|
-
let addedCount = 0;
|
|
560
|
-
|
|
561
|
-
checkedTopics.forEach(checkbox => {
|
|
562
|
-
const chatId = checkbox.dataset.chatId;
|
|
563
|
-
const threadId = checkbox.dataset.threadId;
|
|
564
|
-
|
|
565
|
-
if (!_tgSavedTopicRouting[chatId]) {
|
|
566
|
-
_tgSavedTopicRouting[chatId] = {};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (!_tgSavedTopicRouting[chatId][threadId]) {
|
|
570
|
-
_tgSavedTopicRouting[chatId][threadId] = 'crew-lead';
|
|
571
|
-
addedCount++;
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
document.body.removeChild(modal);
|
|
576
|
-
renderTgTopicsList();
|
|
577
|
-
showNotification(`Added ${addedCount} new topic${addedCount !== 1 ? 's' : ''}! Set agents and click Save.`);
|
|
578
|
-
};
|
|
579
|
-
} catch (e) {
|
|
580
|
-
showNotification('Error discovering topics: ' + e.message, true);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
export async function saveTgConfig() {
|
|
585
|
-
const token = document.getElementById('tgTokenInput').value.trim();
|
|
586
|
-
const idsRaw = document.getElementById('tgAllowedIds').value.trim();
|
|
587
|
-
const allowedChatIds = idsRaw
|
|
588
|
-
? idsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n))
|
|
589
|
-
: [];
|
|
590
|
-
if (!token) { showNotification('Enter a bot token first', true); return; }
|
|
591
|
-
const contactNames = {};
|
|
592
|
-
const userRouting = {};
|
|
593
|
-
|
|
594
|
-
allowedChatIds.forEach(id => {
|
|
595
|
-
// Contact name
|
|
596
|
-
const nameEl = document.getElementById('tgContact-' + id);
|
|
597
|
-
if (nameEl && nameEl.value.trim()) contactNames[String(id)] = nameEl.value.trim();
|
|
598
|
-
|
|
599
|
-
// Per-user routing
|
|
600
|
-
const routeEl = document.getElementById('tgRoute-' + id);
|
|
601
|
-
if (routeEl && routeEl.value) {
|
|
602
|
-
userRouting[String(id)] = routeEl.value;
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
// Collect topic routing from in-memory state (already updated by delete/add)
|
|
607
|
-
// Sync any edits from DOM inputs back to state (only update values, don't rebuild)
|
|
608
|
-
const listDiv = document.getElementById('tgTopicsList');
|
|
609
|
-
if (listDiv) {
|
|
610
|
-
const groups = listDiv.querySelectorAll('.tg-topic-group');
|
|
611
|
-
groups.forEach(groupContainer => {
|
|
612
|
-
const chatIdInput = groupContainer.querySelector('[data-group-chat-id]');
|
|
613
|
-
if (!chatIdInput) return;
|
|
614
|
-
const originalChatId = chatIdInput.dataset.groupChatId;
|
|
615
|
-
const newChatId = chatIdInput.value.trim();
|
|
616
|
-
|
|
617
|
-
// If chatId changed, rename the key in state
|
|
618
|
-
if (originalChatId && newChatId && originalChatId !== newChatId) {
|
|
619
|
-
if (_tgSavedTopicRouting[originalChatId]) {
|
|
620
|
-
_tgSavedTopicRouting[newChatId] = _tgSavedTopicRouting[originalChatId];
|
|
621
|
-
delete _tgSavedTopicRouting[originalChatId];
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// Update topic IDs and agents from DOM (sync all visible rows)
|
|
626
|
-
const targetChatId = newChatId || originalChatId;
|
|
627
|
-
if (!targetChatId) return;
|
|
628
|
-
|
|
629
|
-
// Make sure group exists in state
|
|
630
|
-
if (!_tgSavedTopicRouting[targetChatId]) {
|
|
631
|
-
_tgSavedTopicRouting[targetChatId] = {};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const rows = groupContainer.querySelectorAll('[data-chat-id]');
|
|
635
|
-
rows.forEach(row => {
|
|
636
|
-
const topicIdEl = row.querySelector('.tg-topic-id');
|
|
637
|
-
const agentEl = row.querySelector('.tg-topic-agent');
|
|
638
|
-
if (topicIdEl && agentEl) {
|
|
639
|
-
const oldTopicId = row.dataset.originalTopicId || topicIdEl.value.trim();
|
|
640
|
-
const newTopicId = topicIdEl.value.trim();
|
|
641
|
-
const newAgent = agentEl.value;
|
|
642
|
-
|
|
643
|
-
if (!newTopicId) return; // Skip empty topic IDs
|
|
644
|
-
|
|
645
|
-
// If topic ID changed, update the key
|
|
646
|
-
if (oldTopicId && newTopicId && oldTopicId !== newTopicId) {
|
|
647
|
-
if (_tgSavedTopicRouting[targetChatId][oldTopicId]) {
|
|
648
|
-
delete _tgSavedTopicRouting[targetChatId][oldTopicId];
|
|
649
|
-
}
|
|
650
|
-
_tgSavedTopicRouting[targetChatId][newTopicId] = newAgent;
|
|
651
|
-
} else {
|
|
652
|
-
// Update or create the agent mapping
|
|
653
|
-
_tgSavedTopicRouting[targetChatId][newTopicId] = newAgent;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Use the in-memory state (respects deletions)
|
|
661
|
-
const topicRouting = { ..._tgSavedTopicRouting };
|
|
662
|
-
|
|
663
|
-
// Also check JSON editor (advanced mode) if it has content
|
|
664
|
-
const topicJsonEl = document.getElementById('tgTopicRoutingJson');
|
|
665
|
-
if (topicJsonEl && topicJsonEl.value.trim()) {
|
|
666
|
-
try {
|
|
667
|
-
const jsonParsed = JSON.parse(topicJsonEl.value.trim());
|
|
668
|
-
// Merge with form data (JSON takes precedence)
|
|
669
|
-
Object.assign(topicRouting, jsonParsed);
|
|
670
|
-
} catch (e) {
|
|
671
|
-
// Ignore JSON parse errors if form data exists
|
|
672
|
-
if (Object.keys(topicRouting).length === 0) {
|
|
673
|
-
showNotification('Invalid topic routing JSON: ' + e.message, true);
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
_tgSavedContactNames = contactNames;
|
|
680
|
-
_tgSavedUserRouting = userRouting;
|
|
681
|
-
_tgSavedTopicRouting = topicRouting;
|
|
682
|
-
|
|
683
|
-
await postJSON('/api/telegram/config', { token, targetAgent: 'crew-lead', allowedChatIds, contactNames, userRouting, topicRouting });
|
|
684
|
-
showNotification('Telegram config saved');
|
|
685
|
-
renderTgContactRows();
|
|
686
|
-
renderTgTopicsList();
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
export async function startTgBridge() {
|
|
690
|
-
const token = document.getElementById('tgTokenInput').value.trim();
|
|
691
|
-
const body = { targetAgent: 'crew-lead' };
|
|
692
|
-
if (token) body.token = token;
|
|
693
|
-
const r = await postJSON('/api/telegram/start', body);
|
|
694
|
-
if (r && r.error) { showNotification(r.error, true); return; }
|
|
695
|
-
showNotification(r && r.message === 'Already running' ? 'Already running' : 'Telegram bridge starting...');
|
|
696
|
-
setTimeout(loadTgStatus, 2000);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
export async function stopTgBridge() {
|
|
700
|
-
await postJSON('/api/telegram/stop', {});
|
|
701
|
-
showNotification('Telegram bridge stopped');
|
|
702
|
-
setTimeout(loadTgStatus, 1000);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
export async function loadWaStatus() {
|
|
706
|
-
try {
|
|
707
|
-
const d = await getJSON('/api/whatsapp/status');
|
|
708
|
-
const badge = document.getElementById('waStatusBadge');
|
|
709
|
-
if (!badge) return;
|
|
710
|
-
if (d.running) {
|
|
711
|
-
badge.textContent = d.number ? '● +' + d.number : '● running';
|
|
712
|
-
badge.className = 'status-badge status-active';
|
|
713
|
-
} else {
|
|
714
|
-
badge.textContent = '● stopped';
|
|
715
|
-
badge.className = 'status-badge status-stopped';
|
|
716
|
-
}
|
|
717
|
-
const authEl = document.getElementById('waAuthStatus');
|
|
718
|
-
if (authEl) {
|
|
719
|
-
authEl.textContent = d.authSaved
|
|
720
|
-
? '✅ Auth saved — no QR scan needed on restart'
|
|
721
|
-
: '⚠️ No auth saved — run npm run whatsapp in terminal to scan QR';
|
|
722
|
-
}
|
|
723
|
-
} catch {}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
export function renderWaContactRows() {
|
|
727
|
-
const listEl = document.getElementById('waContactNamesList');
|
|
728
|
-
if (!listEl) return;
|
|
729
|
-
const raw = (document.getElementById('waAllowedNumbers')?.value || '').trim();
|
|
730
|
-
const numbers = raw ? raw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
731
|
-
listEl.innerHTML = '';
|
|
732
|
-
if (!numbers.length) return;
|
|
733
|
-
const title = document.createElement('label');
|
|
734
|
-
title.style.cssText = 'display:block;margin-bottom:6px;font-size:12px;color:var(--text-2);';
|
|
735
|
-
title.textContent = 'Contact names (address book)';
|
|
736
|
-
listEl.appendChild(title);
|
|
737
|
-
numbers.forEach(num => {
|
|
738
|
-
const key = num.replace(/\D/g, '');
|
|
739
|
-
|
|
740
|
-
// Container for 2-line layout
|
|
741
|
-
const container = document.createElement('div');
|
|
742
|
-
container.style.cssText = 'margin-bottom:12px;padding:12px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;';
|
|
743
|
-
|
|
744
|
-
// Line 1: Number + Name
|
|
745
|
-
const row1 = document.createElement('div');
|
|
746
|
-
row1.style.cssText = 'display:grid;grid-template-columns:140px 1fr;gap:8px;margin-bottom:8px;align-items:center;';
|
|
747
|
-
|
|
748
|
-
const span = document.createElement('span');
|
|
749
|
-
span.style.cssText = 'font-size:11px;color:var(--text-3);font-family:monospace;';
|
|
750
|
-
span.textContent = num;
|
|
751
|
-
|
|
752
|
-
const input = document.createElement('input');
|
|
753
|
-
input.id = 'waContact-' + key;
|
|
754
|
-
input.placeholder = 'Name (e.g. Jeff)';
|
|
755
|
-
input.value = _waSavedContactNames[key] || _waSavedContactNames[num] || '';
|
|
756
|
-
input.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
|
|
757
|
-
|
|
758
|
-
row1.appendChild(span);
|
|
759
|
-
row1.appendChild(input);
|
|
760
|
-
|
|
761
|
-
// Line 2: Routing label + dropdown
|
|
762
|
-
const row2 = document.createElement('div');
|
|
763
|
-
row2.style.cssText = 'display:grid;grid-template-columns:140px 1fr;gap:8px;align-items:center;';
|
|
764
|
-
|
|
765
|
-
const routeLabel = document.createElement('span');
|
|
766
|
-
routeLabel.style.cssText = 'font-size:10px;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em;';
|
|
767
|
-
routeLabel.textContent = 'Routes to →';
|
|
768
|
-
|
|
769
|
-
const routeSelect = document.createElement('select');
|
|
770
|
-
routeSelect.id = 'waRoute-' + key;
|
|
771
|
-
routeSelect.style.cssText = 'font-size:12px;padding:6px 8px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;color:var(--text-1);';
|
|
772
|
-
|
|
773
|
-
// Get current routing for this number (check multiple formats)
|
|
774
|
-
const currentRoute = _waSavedUserRouting[num] || _waSavedUserRouting['+' + key] || _waSavedUserRouting[key] || '';
|
|
775
|
-
|
|
776
|
-
const agents = [
|
|
777
|
-
'crew-lead', 'crew-main', 'crew-coder', 'crew-pm', 'crew-qa',
|
|
778
|
-
'crew-fixer', 'crew-security', 'crew-frontend', 'crew-coder-front',
|
|
779
|
-
'crew-coder-back', 'crew-github', 'crew-copywriter', 'crew-researcher',
|
|
780
|
-
'crew-architect', 'crew-seo', 'crew-ml', 'crew-mega', 'crew-loco'
|
|
781
|
-
];
|
|
782
|
-
|
|
783
|
-
// Default option
|
|
784
|
-
const defaultOpt = document.createElement('option');
|
|
785
|
-
defaultOpt.value = '';
|
|
786
|
-
defaultOpt.textContent = '— default (see above) —';
|
|
787
|
-
routeSelect.appendChild(defaultOpt);
|
|
788
|
-
|
|
789
|
-
// Agent options
|
|
790
|
-
agents.forEach(agent => {
|
|
791
|
-
const opt = document.createElement('option');
|
|
792
|
-
opt.value = agent;
|
|
793
|
-
opt.textContent = agent;
|
|
794
|
-
if (agent === currentRoute) opt.selected = true;
|
|
795
|
-
routeSelect.appendChild(opt);
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
// Update state immediately when routing changes
|
|
799
|
-
routeSelect.addEventListener('change', (e) => {
|
|
800
|
-
const newAgent = e.target.value;
|
|
801
|
-
if (newAgent) {
|
|
802
|
-
_waSavedUserRouting[num] = newAgent;
|
|
803
|
-
} else {
|
|
804
|
-
delete _waSavedUserRouting[num];
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
row2.appendChild(routeLabel);
|
|
809
|
-
row2.appendChild(routeSelect);
|
|
810
|
-
|
|
811
|
-
container.appendChild(row1);
|
|
812
|
-
container.appendChild(row2);
|
|
813
|
-
listEl.appendChild(container);
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
export async function loadWaConfig() {
|
|
818
|
-
try {
|
|
819
|
-
const d = await getJSON('/api/whatsapp/config');
|
|
820
|
-
const n = document.getElementById('waAllowedNumbers');
|
|
821
|
-
const t = document.getElementById('waTargetAgent');
|
|
822
|
-
_waSavedContactNames = d.contactNames || {};
|
|
823
|
-
_waSavedUserRouting = d.userRouting || {};
|
|
824
|
-
if (n) n.value = (d.allowedNumbers || []).join(', ');
|
|
825
|
-
if (t) t.value = d.targetAgent || 'crew-lead';
|
|
826
|
-
renderWaContactRows();
|
|
827
|
-
} catch {}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
export async function saveWaConfig() {
|
|
831
|
-
const numbersRaw = document.getElementById('waAllowedNumbers').value.trim();
|
|
832
|
-
const allowedNumbers = numbersRaw ? numbersRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
833
|
-
const targetAgent = (document.getElementById('waTargetAgent').value.trim()) || 'crew-lead';
|
|
834
|
-
const contactNames = {};
|
|
835
|
-
const userRouting = {};
|
|
836
|
-
|
|
837
|
-
allowedNumbers.forEach(num => {
|
|
838
|
-
const key = num.replace(/\D/g, '');
|
|
839
|
-
|
|
840
|
-
// Contact name
|
|
841
|
-
const nameEl = document.getElementById('waContact-' + key);
|
|
842
|
-
if (nameEl && nameEl.value.trim()) contactNames[key] = nameEl.value.trim();
|
|
843
|
-
|
|
844
|
-
// Per-user routing
|
|
845
|
-
const routeEl = document.getElementById('waRoute-' + key);
|
|
846
|
-
if (routeEl && routeEl.value) {
|
|
847
|
-
userRouting[num] = routeEl.value;
|
|
848
|
-
}
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
_waSavedContactNames = contactNames;
|
|
852
|
-
_waSavedUserRouting = userRouting;
|
|
853
|
-
|
|
854
|
-
await postJSON('/api/whatsapp/config', { allowedNumbers, targetAgent, contactNames, userRouting });
|
|
855
|
-
showNotification('WhatsApp config saved');
|
|
856
|
-
renderWaContactRows();
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
export async function startWaBridge() {
|
|
860
|
-
const r = await postJSON('/api/whatsapp/start', {});
|
|
861
|
-
if (r && r.error) { showNotification(r.error, true); return; }
|
|
862
|
-
showNotification(r && r.message === 'Already running' ? 'Already running' : 'WhatsApp bridge starting…');
|
|
863
|
-
setTimeout(loadWaStatus, 2000);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
export async function stopWaBridge() {
|
|
867
|
-
await postJSON('/api/whatsapp/stop', {});
|
|
868
|
-
showNotification('WhatsApp bridge stopped');
|
|
869
|
-
setTimeout(loadWaStatus, 1000);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
export async function loadWaMessages() {
|
|
873
|
-
const feed = document.getElementById('waMessageFeed');
|
|
874
|
-
if (!feed) return;
|
|
875
|
-
try {
|
|
876
|
-
const msgs = await getJSON('/api/whatsapp/messages');
|
|
877
|
-
if (!msgs.length) {
|
|
878
|
-
feed.innerHTML = '<div class="meta" style="padding:20px;text-align:center;">No messages yet. Send a WhatsApp message to your linked number.</div>';
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
feed.innerHTML = msgs.slice(-50).reverse().map(m => {
|
|
882
|
-
const isIn = m.direction === 'inbound';
|
|
883
|
-
const time = m.ts ? new Date(m.ts).toLocaleTimeString() : '';
|
|
884
|
-
const number = (m.jid || '').split('@')[0] || '';
|
|
885
|
-
return '<div style="display:flex;gap:10px;padding:8px;background:var(--bg-2);border-radius:6px;align-items:flex-start;">' +
|
|
886
|
-
'<span style="font-size:18px;">' + (isIn ? '📲' : '🤖') + '</span>' +
|
|
887
|
-
'<div style="flex:1;min-width:0;">' +
|
|
888
|
-
'<div style="font-size:11px;color:var(--text-3);margin-bottom:2px;">' +
|
|
889
|
-
escHtml(isIn ? ('+' + number) : 'crewswarm') + (time ? ' · ' + time : '') +
|
|
890
|
-
'</div>' +
|
|
891
|
-
'<div style="font-size:13px;word-break:break-word;">' + escHtml((m.text || '').slice(0, 300)) + '</div>' +
|
|
892
|
-
'</div>' +
|
|
893
|
-
'</div>';
|
|
894
|
-
}).join('');
|
|
895
|
-
} catch {
|
|
896
|
-
feed.innerHTML = '<div style="color:var(--text-3);font-size:12px;padding:8px;">Could not load messages.</div>';
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
export async function loadTgMessages() {
|
|
901
|
-
const feed = document.getElementById('tgMessageFeed');
|
|
902
|
-
if (!feed) return;
|
|
903
|
-
try {
|
|
904
|
-
const msgs = await getJSON('/api/telegram/messages');
|
|
905
|
-
if (!msgs.length) {
|
|
906
|
-
feed.innerHTML = '<div class="meta" style="padding:20px;text-align:center;">No messages yet. Send something to your bot on Telegram.</div>';
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
feed.innerHTML = msgs.slice(-50).reverse().map(m => {
|
|
910
|
-
const isIn = m.direction === 'inbound';
|
|
911
|
-
const time = m.ts ? new Date(m.ts).toLocaleTimeString() : '';
|
|
912
|
-
const who = isIn ? (m.firstName || m.username || 'User') : 'crewswarm';
|
|
913
|
-
const icon = isIn ? '👤' : '⚡';
|
|
914
|
-
return '<div class="card" style="padding:12px;gap:4px;display:flex;flex-direction:column;">' +
|
|
915
|
-
'<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text-3);">' +
|
|
916
|
-
'<span>' + icon + ' ' + escHtml(who) + (m.username ? ' @' + escHtml(m.username) : '') + '</span>' +
|
|
917
|
-
'<span>' + time + '</span></div>' +
|
|
918
|
-
'<div style="font-size:13px;white-space:pre-wrap;">' + escHtml(m.text || '') + '</div>' +
|
|
919
|
-
'</div>';
|
|
920
|
-
}).join('');
|
|
921
|
-
} catch {
|
|
922
|
-
feed.innerHTML = '<div class="meta" style="padding:20px;color:var(--red-hi);">Error loading messages</div>';
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
export async function loadTelegramSessions() {
|
|
927
|
-
const box = document.getElementById('tgSessionsList');
|
|
928
|
-
if (!box) return;
|
|
929
|
-
const sessions = await getJSON('/api/telegram-sessions').catch(() => []);
|
|
930
|
-
box.innerHTML = '';
|
|
931
|
-
if (!sessions.length) {
|
|
932
|
-
box.innerHTML = '<div style="color:var(--text-3);font-size:12px;padding:8px;">No Telegram sessions yet — send a message to your bot to start one.</div>';
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
for (const s of sessions) {
|
|
936
|
-
const card = document.createElement('div');
|
|
937
|
-
card.style.cssText = 'background:var(--bg-1);border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:10px;';
|
|
938
|
-
const ago = s.lastTs ? Math.round((Date.now() - s.lastTs) / 60000) + 'm ago' : 'unknown';
|
|
939
|
-
const msgLines = s.messages.slice(-6).map(m => {
|
|
940
|
-
const color = m.role === 'user' ? 'var(--accent)' : 'var(--green)';
|
|
941
|
-
const icon = m.role === 'user' ? '👤' : '🤖';
|
|
942
|
-
const txt = String(m.content || '').slice(0, 100).replace(/</g, '<');
|
|
943
|
-
return '<div style="margin-bottom:4px;"><span style="color:' + color + ';">' + icon + '</span> <span>' + txt + '</span></div>';
|
|
944
|
-
}).join('');
|
|
945
|
-
card.innerHTML =
|
|
946
|
-
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">' +
|
|
947
|
-
'<span style="font-size:13px;font-weight:600;">chat ' + s.chatId + '</span>' +
|
|
948
|
-
'<span style="font-size:11px;color:var(--text-3);">' + s.messageCount + ' msgs · ' + ago + '</span>' +
|
|
949
|
-
'</div>' +
|
|
950
|
-
'<div style="font-size:12px;color:var(--text-2);border-top:1px solid var(--border);padding-top:8px;max-height:120px;overflow-y:auto;">' +
|
|
951
|
-
msgLines +
|
|
952
|
-
'</div>';
|
|
953
|
-
box.appendChild(card);
|
|
954
|
-
}
|
|
955
|
-
}
|