@jungjaehoon/mama-os 0.8.3 → 0.9.0
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/CHANGELOG.md +10 -0
- package/dist/agent/agent-loop.d.ts +1 -8
- package/dist/agent/agent-loop.d.ts.map +1 -1
- package/dist/agent/agent-loop.js +44 -159
- package/dist/agent/agent-loop.js.map +1 -1
- package/dist/agent/claude-cli-wrapper.d.ts +6 -0
- package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
- package/dist/agent/claude-cli-wrapper.js +6 -0
- package/dist/agent/claude-cli-wrapper.js.map +1 -1
- package/dist/agent/codex-mcp-process.d.ts +85 -0
- package/dist/agent/codex-mcp-process.d.ts.map +1 -0
- package/dist/agent/codex-mcp-process.js +357 -0
- package/dist/agent/codex-mcp-process.js.map +1 -0
- package/dist/agent/session-pool.d.ts +17 -2
- package/dist/agent/session-pool.d.ts.map +1 -1
- package/dist/agent/session-pool.js +51 -26
- package/dist/agent/session-pool.js.map +1 -1
- package/dist/agent/types.d.ts +9 -24
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/api/graph-api.d.ts.map +1 -1
- package/dist/api/graph-api.js +133 -45
- package/dist/api/graph-api.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +14 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -10
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +143 -54
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +2 -7
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/config-manager.d.ts.map +1 -1
- package/dist/cli/config/config-manager.js +9 -17
- package/dist/cli/config/config-manager.js.map +1 -1
- package/dist/cli/config/types.d.ts +19 -25
- package/dist/cli/config/types.d.ts.map +1 -1
- package/dist/cli/config/types.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/gateways/context-injector.d.ts.map +1 -1
- package/dist/gateways/context-injector.js +6 -3
- package/dist/gateways/context-injector.js.map +1 -1
- package/dist/gateways/discord.d.ts +4 -0
- package/dist/gateways/discord.d.ts.map +1 -1
- package/dist/gateways/discord.js +39 -16
- package/dist/gateways/discord.js.map +1 -1
- package/dist/gateways/message-router.d.ts +6 -1
- package/dist/gateways/message-router.d.ts.map +1 -1
- package/dist/gateways/message-router.js +92 -7
- package/dist/gateways/message-router.js.map +1 -1
- package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
- package/dist/multi-agent/agent-process-manager.js +36 -9
- package/dist/multi-agent/agent-process-manager.js.map +1 -1
- package/dist/multi-agent/runtime-process.d.ts +4 -4
- package/dist/multi-agent/runtime-process.d.ts.map +1 -1
- package/dist/multi-agent/runtime-process.js +9 -20
- package/dist/multi-agent/runtime-process.js.map +1 -1
- package/dist/multi-agent/types.d.ts +13 -8
- package/dist/multi-agent/types.d.ts.map +1 -1
- package/dist/multi-agent/types.js.map +1 -1
- package/dist/setup/setup-prompt.d.ts +1 -1
- package/dist/setup/setup-prompt.d.ts.map +1 -1
- package/dist/setup/setup-prompt.js +19 -0
- package/dist/setup/setup-prompt.js.map +1 -1
- package/dist/setup/setup-server.d.ts.map +1 -1
- package/dist/setup/setup-server.js +39 -16
- package/dist/setup/setup-server.js.map +1 -1
- package/dist/skills/skill-registry.d.ts.map +1 -1
- package/dist/skills/skill-registry.js +5 -2
- package/dist/skills/skill-registry.js.map +1 -1
- package/package.json +5 -3
- package/public/setup.html +12 -1
- package/public/viewer/js/modules/chat.js +1760 -1976
- package/public/viewer/js/modules/dashboard.js +613 -695
- package/public/viewer/js/modules/graph.js +857 -970
- package/public/viewer/js/modules/memory.js +357 -312
- package/public/viewer/js/modules/settings.js +1009 -1026
- package/public/viewer/js/modules/skills.js +336 -355
- package/public/viewer/js/utils/api.js +255 -255
- package/public/viewer/js/utils/debug-logger.js +20 -26
- package/public/viewer/js/utils/dom.js +73 -60
- package/public/viewer/js/utils/format.js +182 -228
- package/public/viewer/js/utils/markdown.js +40 -0
- package/public/viewer/src/modules/chat.ts +2258 -0
- package/public/viewer/src/modules/dashboard.ts +1052 -0
- package/public/viewer/src/modules/graph.ts +1080 -0
- package/public/viewer/src/modules/memory.ts +453 -0
- package/public/viewer/src/modules/settings.ts +1398 -0
- package/public/viewer/src/modules/skills.ts +457 -0
- package/public/viewer/src/types/global.d.ts +168 -0
- package/public/viewer/src/utils/api.ts +650 -0
- package/public/viewer/src/utils/debug-logger.ts +36 -0
- package/public/viewer/src/utils/dom.ts +138 -0
- package/public/viewer/src/utils/format.ts +331 -0
- package/public/viewer/src/utils/markdown.ts +46 -0
- package/public/viewer/tsconfig.viewer.json +18 -0
- package/public/viewer/viewer.html +214 -311
- package/dist/agent/codex-cli-wrapper.d.ts +0 -85
- package/dist/agent/codex-cli-wrapper.d.ts.map +0 -1
- package/dist/agent/codex-cli-wrapper.js +0 -295
- package/dist/agent/codex-cli-wrapper.js.map +0 -1
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Module - MAMA OS Dashboard
|
|
3
|
+
* @module modules/dashboard
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*
|
|
6
|
+
* Handles Dashboard tab functionality including:
|
|
7
|
+
* - Gateway status display (Discord, Slack, Telegram, Chatwork)
|
|
8
|
+
* - Memory statistics
|
|
9
|
+
* - Agent configuration display
|
|
10
|
+
* - Top topics
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* eslint-env browser */
|
|
14
|
+
|
|
15
|
+
import { escapeAttr, escapeHtml, getElementByIdOrNull, getErrorMessage } from '../utils/dom.js';
|
|
16
|
+
import { formatModelName } from '../utils/format.js';
|
|
17
|
+
import {
|
|
18
|
+
API,
|
|
19
|
+
type McpServer,
|
|
20
|
+
type McpServersResponse,
|
|
21
|
+
type MultiAgentAgent,
|
|
22
|
+
type MultiAgentDashboardStatus,
|
|
23
|
+
type TokenSummaryResponse,
|
|
24
|
+
type TokensByAgentResponse,
|
|
25
|
+
} from '../utils/api.js';
|
|
26
|
+
import { DebugLogger } from '../utils/debug-logger.js';
|
|
27
|
+
|
|
28
|
+
const logger = new DebugLogger('Dashboard');
|
|
29
|
+
|
|
30
|
+
type DashboardGateway = {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
configured?: boolean;
|
|
33
|
+
channel?: string;
|
|
34
|
+
chats?: string[];
|
|
35
|
+
rooms?: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type DashboardSessionChannel = {
|
|
39
|
+
source: string;
|
|
40
|
+
channelId: string;
|
|
41
|
+
channelName?: string;
|
|
42
|
+
messageCount?: number;
|
|
43
|
+
lastActive?: number | string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type DashboardSessions = {
|
|
47
|
+
total: number;
|
|
48
|
+
bySource: Record<string, number>;
|
|
49
|
+
channels: DashboardSessionChannel[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type DashboardMemoryStats = {
|
|
53
|
+
total?: number;
|
|
54
|
+
thisWeek?: number;
|
|
55
|
+
thisMonth?: number;
|
|
56
|
+
checkpoints?: number;
|
|
57
|
+
topTopics?: Array<{ topic: string; count: number }>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type DashboardAgentConfig = {
|
|
61
|
+
model?: string;
|
|
62
|
+
maxTurns?: number;
|
|
63
|
+
timeout?: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type DashboardData = {
|
|
67
|
+
gateways?: Record<string, DashboardGateway>;
|
|
68
|
+
sessions?: DashboardSessions;
|
|
69
|
+
memory?: DashboardMemoryStats;
|
|
70
|
+
agent?: DashboardAgentConfig;
|
|
71
|
+
heartbeat?: {
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
interval?: number;
|
|
74
|
+
quiet_start?: number;
|
|
75
|
+
quiet_end?: number;
|
|
76
|
+
quietStart?: number;
|
|
77
|
+
quietEnd?: number;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type MultiAgentDashboardData = {
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
agents: MultiAgentAgent[];
|
|
84
|
+
activeChains?: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type DashboardDelegation = {
|
|
88
|
+
status: string;
|
|
89
|
+
claimedBy?: string;
|
|
90
|
+
description?: string;
|
|
91
|
+
wave?: number;
|
|
92
|
+
completedAt?: number | string;
|
|
93
|
+
claimedAt?: number | string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
type DashboardDelegationsData = {
|
|
97
|
+
delegations: DashboardDelegation[];
|
|
98
|
+
count?: number;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type DashboardTokenData = {
|
|
102
|
+
summary?: TokenSummaryResponse;
|
|
103
|
+
byAgent?: TokensByAgentResponse;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Dashboard Module Class
|
|
108
|
+
*/
|
|
109
|
+
export class DashboardModule {
|
|
110
|
+
data: DashboardData | null = null;
|
|
111
|
+
updateInterval: ReturnType<typeof setInterval> | null = null;
|
|
112
|
+
initialized = false;
|
|
113
|
+
onCronClick: ((event: MouseEvent) => void) | null = null;
|
|
114
|
+
mcpServers: McpServer[] = [];
|
|
115
|
+
multiAgentData: MultiAgentDashboardData = { enabled: false, agents: [] };
|
|
116
|
+
delegationsData: DashboardDelegationsData = { delegations: [], count: 0 };
|
|
117
|
+
cronData: {
|
|
118
|
+
jobs?: Array<{
|
|
119
|
+
id: string;
|
|
120
|
+
name: string;
|
|
121
|
+
schedule?: string;
|
|
122
|
+
cron?: string;
|
|
123
|
+
nextRun?: string;
|
|
124
|
+
enabled?: boolean;
|
|
125
|
+
}>;
|
|
126
|
+
} | null = null;
|
|
127
|
+
tokenData: DashboardTokenData | null = null;
|
|
128
|
+
|
|
129
|
+
constructor() {
|
|
130
|
+
this.data = null;
|
|
131
|
+
this.updateInterval = null;
|
|
132
|
+
this.initialized = false;
|
|
133
|
+
this.onCronClick = null;
|
|
134
|
+
this.mcpServers = [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Initialize dashboard
|
|
139
|
+
*/
|
|
140
|
+
async init(): Promise<void> {
|
|
141
|
+
if (this.initialized) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.initialized = true;
|
|
145
|
+
|
|
146
|
+
// Event delegation for dashboard actions
|
|
147
|
+
this.onCronClick = (e: MouseEvent) => {
|
|
148
|
+
const target = e.target as HTMLElement | null;
|
|
149
|
+
if (!target) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cronButton = target.closest<HTMLElement>('[data-action="run-cron"]');
|
|
154
|
+
if (cronButton) {
|
|
155
|
+
const jobId = cronButton.getAttribute('data-cron-id');
|
|
156
|
+
if (jobId) {
|
|
157
|
+
this.runCronJob(jobId);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const settingsLink = target.closest<HTMLElement>('[data-action="open-settings"]');
|
|
163
|
+
if (settingsLink) {
|
|
164
|
+
const settingsTab = document.querySelector<HTMLElement>('[data-tab="settings"]');
|
|
165
|
+
if (settingsTab) {
|
|
166
|
+
settingsTab.click();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
document.addEventListener('click', this.onCronClick);
|
|
171
|
+
|
|
172
|
+
await this.loadStatus();
|
|
173
|
+
|
|
174
|
+
// Auto-refresh every 30 seconds
|
|
175
|
+
this.updateInterval = setInterval(() => this.loadStatus(), 30000);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Load dashboard status from API
|
|
180
|
+
*/
|
|
181
|
+
async loadStatus(): Promise<void> {
|
|
182
|
+
try {
|
|
183
|
+
this.data = await API.get<DashboardData>('/api/dashboard/status');
|
|
184
|
+
|
|
185
|
+
// Load multi-agent status (Sprint 3 F2)
|
|
186
|
+
try {
|
|
187
|
+
this.multiAgentData = await API.get<MultiAgentDashboardStatus>('/api/multi-agent/status');
|
|
188
|
+
} catch (e) {
|
|
189
|
+
logger.warn('[Dashboard] Multi-agent status unavailable:', e);
|
|
190
|
+
this.multiAgentData = { enabled: false, agents: [] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Load delegations (F4 endpoint)
|
|
194
|
+
try {
|
|
195
|
+
this.delegationsData = await API.get<DashboardDelegationsData>(
|
|
196
|
+
'/api/multi-agent/delegations?limit=10'
|
|
197
|
+
);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
logger.warn('[Dashboard] Delegations unavailable:', e);
|
|
200
|
+
this.delegationsData = { delegations: [], count: 0 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Load cron jobs
|
|
204
|
+
try {
|
|
205
|
+
this.cronData = await API.getCronJobs();
|
|
206
|
+
} catch (e) {
|
|
207
|
+
logger.warn('[Dashboard] Cron data unavailable:', e);
|
|
208
|
+
this.cronData = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Load token summary
|
|
212
|
+
try {
|
|
213
|
+
const [summary, byAgent] = await Promise.all([
|
|
214
|
+
API.getTokenSummary(),
|
|
215
|
+
API.getTokensByAgent(),
|
|
216
|
+
]);
|
|
217
|
+
this.tokenData = { summary, byAgent };
|
|
218
|
+
} catch (e) {
|
|
219
|
+
logger.warn('[Dashboard] Token data unavailable:', e);
|
|
220
|
+
this.tokenData = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Load MCP servers
|
|
224
|
+
await this.loadMCPServers();
|
|
225
|
+
|
|
226
|
+
this.render();
|
|
227
|
+
this.setStatus(`Last updated: ${new Date().toLocaleTimeString()}`);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error('[Dashboard] Load error:', error);
|
|
230
|
+
this.setStatus(`Error: ${getErrorMessage(error)}`, 'error');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Load MCP servers from API
|
|
236
|
+
*/
|
|
237
|
+
async loadMCPServers(): Promise<void> {
|
|
238
|
+
try {
|
|
239
|
+
const data = await API.get<McpServersResponse>('/api/mcp-servers');
|
|
240
|
+
if (data && 'servers' in data) {
|
|
241
|
+
this.mcpServers = data.servers || [];
|
|
242
|
+
this.renderMCPServers();
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
logger.error('Failed to load MCP servers:', error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Render all dashboard sections
|
|
251
|
+
*/
|
|
252
|
+
render(): void {
|
|
253
|
+
if (!this.data) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.renderGateways();
|
|
258
|
+
this.renderMCPServers();
|
|
259
|
+
this.renderSessions();
|
|
260
|
+
this.renderAgentSwarm();
|
|
261
|
+
this.renderMemoryStats();
|
|
262
|
+
this.renderAgentConfig();
|
|
263
|
+
this.renderCronJobs();
|
|
264
|
+
this.renderTokenSummary();
|
|
265
|
+
this.renderTopTopics();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Render gateway status cards
|
|
270
|
+
*/
|
|
271
|
+
renderGateways(): void {
|
|
272
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-gateways');
|
|
273
|
+
if (!container || !this.data.gateways) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const gateways = [
|
|
278
|
+
{ key: 'discord', name: 'Discord', icon: '💬', color: 'indigo' },
|
|
279
|
+
{ key: 'slack', name: 'Slack', icon: '📱', color: 'green' },
|
|
280
|
+
{ key: 'telegram', name: 'Telegram', icon: '✈️', color: 'blue' },
|
|
281
|
+
{ key: 'chatwork', name: 'Chatwork', icon: '💼', color: 'orange' },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
// Count active bots
|
|
285
|
+
const enabledCount = gateways.filter((gw) => this.data.gateways[gw.key]?.enabled).length;
|
|
286
|
+
const configuredCount = gateways.filter((gw) => this.data.gateways[gw.key]?.configured).length;
|
|
287
|
+
|
|
288
|
+
// Update header with bot count
|
|
289
|
+
const header = container.previousElementSibling;
|
|
290
|
+
if (header && header.tagName === 'H2') {
|
|
291
|
+
header.innerHTML = `Gateway Status <span class="text-sm font-normal text-gray-500">(${enabledCount}/${configuredCount} active)</span>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const html = gateways
|
|
295
|
+
.map((gw) => {
|
|
296
|
+
const status = this.data.gateways[gw.key] || {};
|
|
297
|
+
const isConfigured = status.configured;
|
|
298
|
+
const isEnabled = status.enabled;
|
|
299
|
+
|
|
300
|
+
const statusBadge = isConfigured
|
|
301
|
+
? isEnabled
|
|
302
|
+
? `<span class="text-xs px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400">Enabled</span>`
|
|
303
|
+
: `<span class="text-xs px-2 py-0.5 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400">Disabled</span>`
|
|
304
|
+
: `<span class="text-xs px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-500">Not Configured</span>`;
|
|
305
|
+
|
|
306
|
+
// Get channel info based on gateway type
|
|
307
|
+
let channelInfo = '';
|
|
308
|
+
if (isConfigured) {
|
|
309
|
+
if (gw.key === 'discord' && status.channel) {
|
|
310
|
+
channelInfo = `<span class="text-[10px] bg-indigo-50 text-indigo-600 px-1.5 py-0.5 rounded">#${escapeHtml(status.channel)}</span>`;
|
|
311
|
+
} else if (gw.key === 'telegram' && status.chats?.length > 0) {
|
|
312
|
+
channelInfo = `<span class="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded">${status.chats.length} chat(s)</span>`;
|
|
313
|
+
} else if (gw.key === 'slack' && status.channel) {
|
|
314
|
+
channelInfo = `<span class="text-[10px] bg-green-50 text-green-600 px-1.5 py-0.5 rounded">#${escapeHtml(status.channel)}</span>`;
|
|
315
|
+
} else if (gw.key === 'chatwork' && status.rooms?.length > 0) {
|
|
316
|
+
channelInfo = `<span class="text-[10px] bg-orange-50 text-orange-600 px-1.5 py-0.5 rounded">${status.rooms.length} room(s)</span>`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return `
|
|
321
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow">
|
|
322
|
+
<div class="flex items-center justify-between mb-2">
|
|
323
|
+
<span class="text-2xl">${gw.icon}</span>
|
|
324
|
+
${statusBadge}
|
|
325
|
+
</div>
|
|
326
|
+
<h3 class="font-semibold text-gray-900 dark:text-gray-100">${gw.name}</h3>
|
|
327
|
+
<div class="flex items-center gap-2 mt-1">
|
|
328
|
+
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
329
|
+
${isConfigured ? 'Token ✓' : 'No token'}
|
|
330
|
+
</p>
|
|
331
|
+
${channelInfo}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
`;
|
|
335
|
+
})
|
|
336
|
+
.join('');
|
|
337
|
+
|
|
338
|
+
container.innerHTML = html;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Render MCP servers
|
|
343
|
+
*/
|
|
344
|
+
renderMCPServers(): void {
|
|
345
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-mcp');
|
|
346
|
+
if (!container) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (this.mcpServers.length === 0) {
|
|
351
|
+
container.innerHTML = `
|
|
352
|
+
<p class="text-gray-500 text-sm col-span-full py-4 text-center">
|
|
353
|
+
No MCP servers configured
|
|
354
|
+
</p>
|
|
355
|
+
`;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const icons = {
|
|
360
|
+
'brave-devtools': '🌐',
|
|
361
|
+
'brave-search': '🔍',
|
|
362
|
+
mama: '🧠',
|
|
363
|
+
slack: '💬',
|
|
364
|
+
notion: '📝',
|
|
365
|
+
linear: '📊',
|
|
366
|
+
asana: '✅',
|
|
367
|
+
atlassian: '🔷',
|
|
368
|
+
ms365: '📧',
|
|
369
|
+
monday: '📅',
|
|
370
|
+
clickup: '✓',
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const html = this.mcpServers
|
|
374
|
+
.map((server) => {
|
|
375
|
+
const icon = icons[server.name] || '🔌';
|
|
376
|
+
const isHttp = server.type === 'http';
|
|
377
|
+
const statusBadge = isHttp
|
|
378
|
+
? 'bg-yellow-100 text-yellow-700'
|
|
379
|
+
: 'bg-green-100 text-green-700';
|
|
380
|
+
const statusText = isHttp ? 'OAuth' : 'Ready';
|
|
381
|
+
|
|
382
|
+
return `
|
|
383
|
+
<div class="bg-white border border-gray-200 rounded-lg p-2.5 hover:shadow-md transition-shadow">
|
|
384
|
+
<div class="flex items-center justify-between mb-1">
|
|
385
|
+
<div class="flex items-center gap-2">
|
|
386
|
+
<span class="text-lg">${icon}</span>
|
|
387
|
+
<h4 class="font-semibold text-sm text-gray-900">${escapeHtml(server.name)}</h4>
|
|
388
|
+
</div>
|
|
389
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded ${statusBadge}">${statusText}</span>
|
|
390
|
+
</div>
|
|
391
|
+
<p class="text-[10px] text-gray-500 truncate">${escapeHtml(server.type === 'http' ? server.url : server.command)}</p>
|
|
392
|
+
</div>
|
|
393
|
+
`;
|
|
394
|
+
})
|
|
395
|
+
.join('');
|
|
396
|
+
|
|
397
|
+
container.innerHTML = html;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Render session statistics
|
|
402
|
+
*/
|
|
403
|
+
renderSessions(): void {
|
|
404
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-sessions');
|
|
405
|
+
if (!container) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const sessions = this.data.sessions || { total: 0, bySource: {}, channels: [] };
|
|
410
|
+
|
|
411
|
+
if (sessions.total === 0) {
|
|
412
|
+
container.innerHTML = `
|
|
413
|
+
<p class="text-gray-500 dark:text-gray-400 text-sm text-center py-4">
|
|
414
|
+
No active sessions yet. Start chatting to create sessions.
|
|
415
|
+
</p>
|
|
416
|
+
`;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Source icons and labels
|
|
421
|
+
const sourceInfo = {
|
|
422
|
+
discord: { icon: '🎮', label: 'Discord', color: 'bg-indigo-100 text-indigo-700' },
|
|
423
|
+
telegram: { icon: '✈️', label: 'Telegram', color: 'bg-sky-100 text-sky-700' },
|
|
424
|
+
slack: { icon: '📱', label: 'Slack', color: 'bg-purple-100 text-purple-700' },
|
|
425
|
+
chatwork: { icon: '💼', label: 'Chatwork', color: 'bg-green-100 text-green-700' },
|
|
426
|
+
viewer: { icon: '🖥️', label: 'OS Viewer', color: 'bg-gray-100 text-gray-700' },
|
|
427
|
+
mobile: { icon: '📲', label: 'Mobile', color: 'bg-orange-100 text-orange-700' },
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Build source summary
|
|
431
|
+
const sourceSummary = Object.entries(sessions.bySource)
|
|
432
|
+
.map(([source, count]) => {
|
|
433
|
+
const info = sourceInfo[source] || {
|
|
434
|
+
icon: '📝',
|
|
435
|
+
label: source,
|
|
436
|
+
color: 'bg-gray-100 text-gray-700',
|
|
437
|
+
};
|
|
438
|
+
const safeLabel = escapeHtml(info.label);
|
|
439
|
+
return `<span class="inline-flex items-center gap-1 ${info.color} px-2 py-1 rounded text-xs font-medium">
|
|
440
|
+
${info.icon} ${safeLabel}: ${count}
|
|
441
|
+
</span>`;
|
|
442
|
+
})
|
|
443
|
+
.join('');
|
|
444
|
+
|
|
445
|
+
// Build recent channels list
|
|
446
|
+
const channelList = sessions.channels
|
|
447
|
+
.slice(0, 5)
|
|
448
|
+
.map((ch) => {
|
|
449
|
+
const info = sourceInfo[ch.source] || {
|
|
450
|
+
icon: '📝',
|
|
451
|
+
label: ch.source,
|
|
452
|
+
color: 'bg-gray-100 text-gray-700',
|
|
453
|
+
};
|
|
454
|
+
const lastActive = this.formatRelativeTime(ch.lastActive);
|
|
455
|
+
|
|
456
|
+
// Use channel name if available, otherwise use meaningful fallbacks
|
|
457
|
+
let channelDisplay;
|
|
458
|
+
if (ch.channelName) {
|
|
459
|
+
// Show channel name (already human-readable)
|
|
460
|
+
channelDisplay =
|
|
461
|
+
ch.channelName.length > 25 ? ch.channelName.slice(0, 22) + '...' : ch.channelName;
|
|
462
|
+
} else if (ch.source === 'viewer' || ch.channelId === 'mama_os_main') {
|
|
463
|
+
// OS Viewer - shared channel
|
|
464
|
+
channelDisplay = 'MAMA OS';
|
|
465
|
+
} else if (ch.source === 'mobile') {
|
|
466
|
+
// Mobile app - show user-friendly name
|
|
467
|
+
channelDisplay = 'Mobile App';
|
|
468
|
+
} else {
|
|
469
|
+
// Fallback: truncate channel ID (Discord channels before update)
|
|
470
|
+
channelDisplay =
|
|
471
|
+
ch.channelId.length > 12
|
|
472
|
+
? ch.channelId.slice(0, 6) + '...' + ch.channelId.slice(-4)
|
|
473
|
+
: ch.channelId;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return `
|
|
477
|
+
<div class="flex items-center justify-between py-1.5 border-b border-gray-100 dark:border-gray-700 last:border-0">
|
|
478
|
+
<div class="flex items-center gap-2">
|
|
479
|
+
<span class="${info.color} px-1.5 py-0.5 rounded text-[10px] font-medium" title="${escapeHtml(info.label)}">${info.icon}</span>
|
|
480
|
+
<span class="text-xs text-gray-700 dark:text-gray-300" title="${escapeHtml(ch.channelId)}">${escapeHtml(channelDisplay)}</span>
|
|
481
|
+
</div>
|
|
482
|
+
<div class="flex items-center gap-2">
|
|
483
|
+
<span class="text-[10px] bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 px-1.5 py-0.5 rounded">${ch.messageCount} turns</span>
|
|
484
|
+
<span class="text-[10px] text-gray-400">${lastActive}</span>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
`;
|
|
488
|
+
})
|
|
489
|
+
.join('');
|
|
490
|
+
|
|
491
|
+
container.innerHTML = `
|
|
492
|
+
<div class="mb-3">
|
|
493
|
+
<div class="flex items-center justify-between mb-2">
|
|
494
|
+
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">Sessions by Platform</span>
|
|
495
|
+
<span class="text-xs bg-indigo-100 text-indigo-600 px-2 py-0.5 rounded-full">${sessions.total} total</span>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="flex flex-wrap gap-2">
|
|
498
|
+
${sourceSummary}
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
<div>
|
|
502
|
+
<p class="text-xs text-gray-500 mb-2">Recent Channels:</p>
|
|
503
|
+
${channelList || '<p class="text-xs text-gray-400">No recent activity</p>'}
|
|
504
|
+
</div>
|
|
505
|
+
`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Format relative time (e.g., "2h ago", "3d ago")
|
|
510
|
+
*/
|
|
511
|
+
formatRelativeTime(timestamp: number | string | Date | undefined): string {
|
|
512
|
+
if (timestamp === undefined || timestamp === null || timestamp === '') {
|
|
513
|
+
return 'Never';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
const target = typeof timestamp === 'number' ? timestamp : new Date(timestamp).getTime();
|
|
518
|
+
const diff = now - target;
|
|
519
|
+
const minutes = Math.floor(diff / 60000);
|
|
520
|
+
const hours = Math.floor(diff / 3600000);
|
|
521
|
+
const days = Math.floor(diff / 86400000);
|
|
522
|
+
|
|
523
|
+
if (minutes < 1) {
|
|
524
|
+
return 'Just now';
|
|
525
|
+
}
|
|
526
|
+
if (minutes < 60) {
|
|
527
|
+
return `${minutes}m ago`;
|
|
528
|
+
}
|
|
529
|
+
if (hours < 24) {
|
|
530
|
+
return `${hours}h ago`;
|
|
531
|
+
}
|
|
532
|
+
return `${days}d ago`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Render memory statistics
|
|
537
|
+
*/
|
|
538
|
+
renderMemoryStats(): void {
|
|
539
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-memory');
|
|
540
|
+
if (!container || !this.data.memory) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const memory = this.data.memory;
|
|
545
|
+
|
|
546
|
+
const stats = [
|
|
547
|
+
{ label: 'Total Decisions', value: memory.total || 0, icon: '🧠' },
|
|
548
|
+
{ label: 'This Week', value: memory.thisWeek || 0, icon: '📅' },
|
|
549
|
+
{ label: 'This Month', value: memory.thisMonth || 0, icon: '📆' },
|
|
550
|
+
{ label: 'Checkpoints', value: memory.checkpoints || 0, icon: '💾' },
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
const html = stats
|
|
554
|
+
.map(
|
|
555
|
+
(stat) => `
|
|
556
|
+
<div class="bg-white border border-gray-200 rounded-lg p-2.5 text-center">
|
|
557
|
+
<span class="text-lg">${stat.icon}</span>
|
|
558
|
+
<p class="text-xl font-bold text-gray-900 mt-1">${stat.value}</p>
|
|
559
|
+
<p class="text-[10px] text-gray-500">${stat.label}</p>
|
|
560
|
+
</div>
|
|
561
|
+
`
|
|
562
|
+
)
|
|
563
|
+
.join('');
|
|
564
|
+
|
|
565
|
+
container.innerHTML = html;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Render agent configuration
|
|
570
|
+
*/
|
|
571
|
+
renderAgentConfig(): void {
|
|
572
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-agent');
|
|
573
|
+
if (!container || !this.data.agent) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const agent = this.data.agent;
|
|
578
|
+
const heartbeat = this.data.heartbeat || {};
|
|
579
|
+
const friendlyModel = formatModelName(agent.model) || 'Not Set';
|
|
580
|
+
|
|
581
|
+
container.innerHTML = `
|
|
582
|
+
<div class="mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
|
|
583
|
+
<div class="flex items-center justify-between">
|
|
584
|
+
<div>
|
|
585
|
+
<p class="text-[10px] text-gray-500 uppercase tracking-wide">Current Model</p>
|
|
586
|
+
<p class="font-bold text-gray-900 dark:text-gray-100 text-lg">${escapeHtml(friendlyModel)}</p>
|
|
587
|
+
<p class="text-[10px] text-gray-400 font-mono">${escapeHtml(agent.model || 'Not configured')}</p>
|
|
588
|
+
</div>
|
|
589
|
+
<span class="text-3xl">🤖</span>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
<div class="grid grid-cols-3 gap-3">
|
|
593
|
+
<div>
|
|
594
|
+
<p class="text-[10px] text-gray-500 uppercase tracking-wide">Max Turns</p>
|
|
595
|
+
<p class="font-semibold text-gray-900 dark:text-gray-100 text-sm mt-0.5">${agent.maxTurns || 'N/A'}</p>
|
|
596
|
+
</div>
|
|
597
|
+
<div>
|
|
598
|
+
<p class="text-[10px] text-gray-500 uppercase tracking-wide">Timeout</p>
|
|
599
|
+
<p class="font-semibold text-gray-900 dark:text-gray-100 text-sm mt-0.5">${this.formatTimeout(agent.timeout)}</p>
|
|
600
|
+
</div>
|
|
601
|
+
<div>
|
|
602
|
+
<p class="text-[10px] text-gray-500 uppercase tracking-wide">Heartbeat</p>
|
|
603
|
+
<p class="font-semibold text-gray-900 dark:text-gray-100 text-sm mt-0.5">
|
|
604
|
+
${heartbeat.enabled ? `${Math.round((heartbeat.interval || 1800000) / 60000)}min` : 'Off'}
|
|
605
|
+
</p>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
${
|
|
609
|
+
heartbeat.enabled
|
|
610
|
+
? `
|
|
611
|
+
<div class="mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
|
612
|
+
<p class="text-[10px] text-gray-500">
|
|
613
|
+
Quiet hours: ${heartbeat.quiet_start ?? heartbeat.quietStart ?? 23}:00 - ${
|
|
614
|
+
heartbeat.quiet_end ?? heartbeat.quietEnd ?? 8
|
|
615
|
+
}:00
|
|
616
|
+
</p>
|
|
617
|
+
</div>
|
|
618
|
+
`
|
|
619
|
+
: ''
|
|
620
|
+
}
|
|
621
|
+
`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Render agent swarm section
|
|
626
|
+
* Sprint 3 F2: Multi-agent dashboard
|
|
627
|
+
*/
|
|
628
|
+
renderAgentSwarm(): void {
|
|
629
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-agent-swarm');
|
|
630
|
+
if (!container) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const multiAgent = this.multiAgentData || { enabled: false, agents: [] };
|
|
635
|
+
|
|
636
|
+
if (!multiAgent.enabled) {
|
|
637
|
+
container.innerHTML = `
|
|
638
|
+
<p class="text-gray-500 dark:text-gray-400 text-sm text-center py-4">
|
|
639
|
+
Multi-agent is not enabled. Enable in <a href="/viewer?tab=settings" class="text-indigo-600 hover:underline" data-action="open-settings">Settings</a>.
|
|
640
|
+
</p>
|
|
641
|
+
`;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const agents = multiAgent.agents || [];
|
|
646
|
+
|
|
647
|
+
if (agents.length === 0) {
|
|
648
|
+
container.innerHTML = `
|
|
649
|
+
<p class="text-gray-500 dark:text-gray-400 text-sm text-center py-4">
|
|
650
|
+
No agents configured yet.
|
|
651
|
+
</p>
|
|
652
|
+
`;
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Tier badge colors
|
|
657
|
+
const tierColors = {
|
|
658
|
+
1: { bg: 'bg-indigo-100', text: 'text-indigo-700', label: 'T1' },
|
|
659
|
+
2: { bg: 'bg-green-100', text: 'text-green-700', label: 'T2' },
|
|
660
|
+
3: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'T3' },
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// Status icons (F2 enhanced)
|
|
664
|
+
const statusIcons = {
|
|
665
|
+
idle: '🟢', // 대기 중
|
|
666
|
+
online: '🟢', // 온라인 (fallback)
|
|
667
|
+
busy: '🟡', // 작업 중
|
|
668
|
+
starting: '🔵', // 시작 중
|
|
669
|
+
dead: '🔴', // 비정상 종료
|
|
670
|
+
offline: '🔴', // 오프라인
|
|
671
|
+
disabled: '⚪', // 비활성
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// Status text labels
|
|
675
|
+
const statusLabels = {
|
|
676
|
+
idle: 'Ready',
|
|
677
|
+
online: 'Ready',
|
|
678
|
+
busy: 'Working...',
|
|
679
|
+
starting: 'Starting...',
|
|
680
|
+
dead: 'Error',
|
|
681
|
+
offline: 'Offline',
|
|
682
|
+
disabled: 'Disabled',
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// Agent cards
|
|
686
|
+
const agentCards = agents
|
|
687
|
+
.map((agent) => {
|
|
688
|
+
const tier = tierColors[agent.tier] || tierColors[1];
|
|
689
|
+
const statusIcon = statusIcons[agent.status] || statusIcons.offline;
|
|
690
|
+
const statusLabel = statusLabels[agent.status] || 'Unknown';
|
|
691
|
+
const friendlyModel = formatModelName(agent.model) || agent.model || 'Default';
|
|
692
|
+
|
|
693
|
+
return `
|
|
694
|
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-2.5 hover:shadow-md transition-shadow">
|
|
695
|
+
<div class="flex items-center justify-between mb-1.5">
|
|
696
|
+
<div class="flex items-center gap-2">
|
|
697
|
+
<span class="${tier.bg} ${tier.text} text-xs font-bold px-1.5 py-0.5 rounded">${tier.label}</span>
|
|
698
|
+
<span class="text-xs">${statusIcon} ${escapeHtml(statusLabel)}</span>
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
<h3 class="font-semibold text-gray-900 dark:text-gray-100 text-sm">${escapeHtml(agent.name)}</h3>
|
|
702
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">${escapeHtml(friendlyModel)}</p>
|
|
703
|
+
${
|
|
704
|
+
agent.lastActivity
|
|
705
|
+
? `<p class="text-[10px] text-gray-400 mt-1">Last: ${this.formatRelativeTime(agent.lastActivity)}</p>`
|
|
706
|
+
: ''
|
|
707
|
+
}
|
|
708
|
+
</div>
|
|
709
|
+
`;
|
|
710
|
+
})
|
|
711
|
+
.join('');
|
|
712
|
+
|
|
713
|
+
// Recent delegations (F2 F4 API integration)
|
|
714
|
+
const delegationsData = this.delegationsData || { delegations: [], count: 0 };
|
|
715
|
+
const delegations = delegationsData.delegations || [];
|
|
716
|
+
|
|
717
|
+
// Status badge colors
|
|
718
|
+
const statusColors = {
|
|
719
|
+
completed: 'bg-green-100 text-green-700',
|
|
720
|
+
claimed: 'bg-yellow-100 text-yellow-700',
|
|
721
|
+
failed: 'bg-red-100 text-red-700',
|
|
722
|
+
pending: 'bg-gray-100 text-gray-700',
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const delegationList =
|
|
726
|
+
delegations.length > 0
|
|
727
|
+
? delegations
|
|
728
|
+
.slice(0, 5)
|
|
729
|
+
.map((del) => {
|
|
730
|
+
const statusColor = statusColors[del.status] || statusColors.pending;
|
|
731
|
+
const timestamp = del.completedAt || del.claimedAt;
|
|
732
|
+
return `
|
|
733
|
+
<div class="text-xs text-gray-700 dark:text-gray-300 py-1 border-b border-gray-100 dark:border-gray-700 last:border-0">
|
|
734
|
+
<span class="${statusColor} text-[10px] font-bold px-1 py-0.5 rounded">${escapeHtml(del.status)}</span>
|
|
735
|
+
<span class="font-medium">${escapeHtml(del.claimedBy || 'unknown')}</span>:
|
|
736
|
+
"${escapeHtml(del.description)}"
|
|
737
|
+
${del.wave ? `<span class="text-gray-400">(wave ${del.wave})</span>` : ''}
|
|
738
|
+
${timestamp ? `<span class="text-gray-400 text-[10px]"> ${this.formatRelativeTime(timestamp)}</span>` : ''}
|
|
739
|
+
</div>
|
|
740
|
+
`;
|
|
741
|
+
})
|
|
742
|
+
.join('')
|
|
743
|
+
: '<p class="text-xs text-gray-400">No recent delegations</p>';
|
|
744
|
+
|
|
745
|
+
// Active chains
|
|
746
|
+
const activeChains = multiAgent.activeChains || 0;
|
|
747
|
+
const chainBadge =
|
|
748
|
+
activeChains > 0
|
|
749
|
+
? `<span class="text-xs bg-green-100 text-green-600 px-2 py-0.5 rounded-full">${activeChains} active</span>`
|
|
750
|
+
: '<span class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">0 active</span>';
|
|
751
|
+
|
|
752
|
+
container.innerHTML = `
|
|
753
|
+
<div class="mb-3">
|
|
754
|
+
<p class="text-xs text-gray-500 mb-2">Agent Team:</p>
|
|
755
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
|
756
|
+
${agentCards}
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
<div class="mb-2 pb-2 border-b border-gray-200 dark:border-gray-700">
|
|
760
|
+
<div class="flex items-center justify-between mb-2">
|
|
761
|
+
<p class="text-xs text-gray-500">Delegation Chain:</p>
|
|
762
|
+
${chainBadge}
|
|
763
|
+
</div>
|
|
764
|
+
<div class="bg-gray-50 dark:bg-gray-800 rounded p-2">
|
|
765
|
+
${delegationList}
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Render top topics
|
|
773
|
+
*/
|
|
774
|
+
renderTopTopics(): void {
|
|
775
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-topics');
|
|
776
|
+
if (!container || !this.data.memory) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const topics = this.data.memory.topTopics || [];
|
|
781
|
+
|
|
782
|
+
if (topics.length === 0) {
|
|
783
|
+
container.innerHTML = `
|
|
784
|
+
<p class="text-gray-500 dark:text-gray-400 text-sm">No topics yet. Start making decisions!</p>
|
|
785
|
+
`;
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const counts = topics
|
|
790
|
+
.map((topic) => (Number.isFinite(topic.count) ? topic.count : 0))
|
|
791
|
+
.filter((count) => count >= 0);
|
|
792
|
+
const maxCount = Math.max(1, ...counts);
|
|
793
|
+
|
|
794
|
+
const html = topics
|
|
795
|
+
.map((topic) => {
|
|
796
|
+
const safeCount = Number.isFinite(topic.count) ? topic.count : 0;
|
|
797
|
+
return `
|
|
798
|
+
<div class="flex items-center gap-3 mb-2">
|
|
799
|
+
<div class="flex-1">
|
|
800
|
+
<div class="flex justify-between items-center mb-1">
|
|
801
|
+
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">${escapeHtml(topic.topic)}</span>
|
|
802
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">${safeCount}</span>
|
|
803
|
+
</div>
|
|
804
|
+
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
805
|
+
<div class="bg-indigo-500 h-2 rounded-full" style="width: ${(safeCount / maxCount) * 100}%"></div>
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
`;
|
|
810
|
+
})
|
|
811
|
+
.join('');
|
|
812
|
+
|
|
813
|
+
container.innerHTML = html;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Format timeout in human readable format
|
|
818
|
+
*/
|
|
819
|
+
formatTimeout(ms?: number): string {
|
|
820
|
+
if (!ms) {
|
|
821
|
+
return 'N/A';
|
|
822
|
+
}
|
|
823
|
+
if (ms < 60000) {
|
|
824
|
+
return `${Math.round(ms / 1000)}s`;
|
|
825
|
+
}
|
|
826
|
+
return `${Math.round(ms / 60000)}min`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Render cron jobs section
|
|
831
|
+
*/
|
|
832
|
+
renderCronJobs(): void {
|
|
833
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-cron');
|
|
834
|
+
if (!container) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const jobs = this.cronData?.jobs || this.cronData || [];
|
|
839
|
+
|
|
840
|
+
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
841
|
+
container.innerHTML = `
|
|
842
|
+
<p class="text-gray-500 text-sm text-center py-4">
|
|
843
|
+
No cron jobs configured. Ask the agent to schedule a task or use the Settings tab.
|
|
844
|
+
</p>
|
|
845
|
+
`;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const rows = jobs
|
|
850
|
+
.map((job) => {
|
|
851
|
+
const isEnabled = job.enabled !== false;
|
|
852
|
+
const statusBadge = isEnabled
|
|
853
|
+
? '<span class="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-600">Active</span>'
|
|
854
|
+
: '<span class="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-500">Paused</span>';
|
|
855
|
+
|
|
856
|
+
const nextRun = job.nextRun
|
|
857
|
+
? new Date(job.nextRun).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
858
|
+
: '-';
|
|
859
|
+
|
|
860
|
+
return `
|
|
861
|
+
<div class="flex items-center justify-between py-2 border-b border-gray-100 last:border-0">
|
|
862
|
+
<div class="flex-1 min-w-0">
|
|
863
|
+
<div class="flex items-center gap-2">
|
|
864
|
+
<span class="font-medium text-sm text-gray-900 truncate">${escapeHtml(job.name || job.id)}</span>
|
|
865
|
+
${statusBadge}
|
|
866
|
+
</div>
|
|
867
|
+
<p class="text-xs text-gray-500 mt-0.5">
|
|
868
|
+
<code class="bg-gray-100 px-1 py-0.5 rounded text-[10px]">${escapeHtml(job.schedule || job.cron || '')}</code>
|
|
869
|
+
<span class="ml-2">Next: ${nextRun}</span>
|
|
870
|
+
</p>
|
|
871
|
+
</div>
|
|
872
|
+
<div class="flex items-center gap-1 ml-2 shrink-0">
|
|
873
|
+
<button class="text-xs px-2 py-1 bg-mama-yellow hover:bg-mama-yellow-hover text-mama-black rounded transition-colors"
|
|
874
|
+
data-action="run-cron"
|
|
875
|
+
data-cron-id="${escapeAttr(job.id)}" title="Run Now">
|
|
876
|
+
Run
|
|
877
|
+
</button>
|
|
878
|
+
</div>
|
|
879
|
+
</div>
|
|
880
|
+
`;
|
|
881
|
+
})
|
|
882
|
+
.join('');
|
|
883
|
+
|
|
884
|
+
container.innerHTML = `
|
|
885
|
+
<div class="space-y-0">
|
|
886
|
+
${rows}
|
|
887
|
+
</div>
|
|
888
|
+
`;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Run a cron job immediately
|
|
893
|
+
*/
|
|
894
|
+
async runCronJob(id: string): Promise<void> {
|
|
895
|
+
try {
|
|
896
|
+
await API.runCronJob(id);
|
|
897
|
+
const statusEl = getElementByIdOrNull<HTMLElement>('dashboard-status');
|
|
898
|
+
if (statusEl) {
|
|
899
|
+
statusEl.textContent = `Cron job "${id}" triggered`;
|
|
900
|
+
}
|
|
901
|
+
await this.loadStatus();
|
|
902
|
+
} catch (e) {
|
|
903
|
+
logger.error('[Dashboard] Failed to run cron job:', e);
|
|
904
|
+
const statusEl = getElementByIdOrNull<HTMLElement>('dashboard-status');
|
|
905
|
+
if (statusEl) {
|
|
906
|
+
const message = getErrorMessage(e);
|
|
907
|
+
statusEl.textContent = `Cron job "${id}" failed: ${message}`;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Render token usage summary section
|
|
914
|
+
*/
|
|
915
|
+
renderTokenSummary(): void {
|
|
916
|
+
const container = getElementByIdOrNull<HTMLElement>('dashboard-tokens');
|
|
917
|
+
if (!container) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (!this.tokenData?.summary) {
|
|
922
|
+
container.innerHTML = `
|
|
923
|
+
<p class="text-gray-500 text-sm text-center py-4">
|
|
924
|
+
Token tracking not yet available. Usage data will appear after conversations.
|
|
925
|
+
</p>
|
|
926
|
+
`;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const s = this.tokenData.summary;
|
|
931
|
+
const agents = this.tokenData.byAgent?.agents || [];
|
|
932
|
+
|
|
933
|
+
const formatTokens = (n: number | undefined): string => {
|
|
934
|
+
if (!n || n === 0) {
|
|
935
|
+
return '0';
|
|
936
|
+
}
|
|
937
|
+
if (n >= 1000000) {
|
|
938
|
+
return (n / 1000000).toFixed(1) + 'M';
|
|
939
|
+
}
|
|
940
|
+
if (n >= 1000) {
|
|
941
|
+
return (n / 1000).toFixed(1) + 'K';
|
|
942
|
+
}
|
|
943
|
+
return n.toString();
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const formatCost = (usd: number | undefined): string => {
|
|
947
|
+
if (!usd || usd === 0) {
|
|
948
|
+
return '$0.00';
|
|
949
|
+
}
|
|
950
|
+
return '$' + usd.toFixed(2);
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
// Summary cards
|
|
954
|
+
const periods = [
|
|
955
|
+
{
|
|
956
|
+
label: 'Today',
|
|
957
|
+
tokens: (s.today?.input_tokens || 0) + (s.today?.output_tokens || 0),
|
|
958
|
+
cost: s.today?.cost_usd,
|
|
959
|
+
icon: '📊',
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
label: 'This Week',
|
|
963
|
+
tokens: (s.week?.input_tokens || 0) + (s.week?.output_tokens || 0),
|
|
964
|
+
cost: s.week?.cost_usd,
|
|
965
|
+
icon: '📅',
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
label: 'This Month',
|
|
969
|
+
tokens: (s.month?.input_tokens || 0) + (s.month?.output_tokens || 0),
|
|
970
|
+
cost: s.month?.cost_usd,
|
|
971
|
+
icon: '📆',
|
|
972
|
+
},
|
|
973
|
+
];
|
|
974
|
+
|
|
975
|
+
const cards = periods
|
|
976
|
+
.map(
|
|
977
|
+
(p) => `
|
|
978
|
+
<div class="bg-white border border-gray-200 rounded-lg p-2.5 text-center">
|
|
979
|
+
<span class="text-lg">${p.icon}</span>
|
|
980
|
+
<p class="text-xl font-bold text-gray-900 mt-1">${formatTokens(p.tokens)}</p>
|
|
981
|
+
<p class="text-[10px] text-gray-500">${p.label}</p>
|
|
982
|
+
<p class="text-[10px] text-mama-yellow-hover font-medium">${formatCost(p.cost)}</p>
|
|
983
|
+
</div>
|
|
984
|
+
`
|
|
985
|
+
)
|
|
986
|
+
.join('');
|
|
987
|
+
|
|
988
|
+
// Agent breakdown (mini bar chart)
|
|
989
|
+
const maxTokens = Math.max(
|
|
990
|
+
...agents.map((a) => (a.input_tokens || 0) + (a.output_tokens || 0)),
|
|
991
|
+
1
|
|
992
|
+
);
|
|
993
|
+
const agentBars = agents
|
|
994
|
+
.slice(0, 5)
|
|
995
|
+
.map((a) => {
|
|
996
|
+
const totalTokens = (a.input_tokens || 0) + (a.output_tokens || 0);
|
|
997
|
+
const pct = Math.round((totalTokens / maxTokens) * 100);
|
|
998
|
+
const agentLabel = a.agent_name || a.agent_id || 'unknown';
|
|
999
|
+
return `
|
|
1000
|
+
<div class="flex items-center gap-2 mb-1.5">
|
|
1001
|
+
<span class="text-xs text-gray-700 w-20 truncate" title="${escapeHtml(agentLabel)}">${escapeHtml(agentLabel)}</span>
|
|
1002
|
+
<div class="flex-1 bg-gray-200 rounded-full h-2">
|
|
1003
|
+
<div class="bg-mama-yellow h-2 rounded-full transition-all" style="width: ${pct}%"></div>
|
|
1004
|
+
</div>
|
|
1005
|
+
<span class="text-[10px] text-gray-500 w-12 text-right">${formatTokens(totalTokens)}</span>
|
|
1006
|
+
</div>
|
|
1007
|
+
`;
|
|
1008
|
+
})
|
|
1009
|
+
.join('');
|
|
1010
|
+
|
|
1011
|
+
container.innerHTML = `
|
|
1012
|
+
<div class="grid grid-cols-3 gap-2 mb-3">
|
|
1013
|
+
${cards}
|
|
1014
|
+
</div>
|
|
1015
|
+
${
|
|
1016
|
+
agents.length > 0
|
|
1017
|
+
? `
|
|
1018
|
+
<div>
|
|
1019
|
+
<p class="text-xs text-gray-500 mb-2">By Agent:</p>
|
|
1020
|
+
${agentBars}
|
|
1021
|
+
</div>
|
|
1022
|
+
`
|
|
1023
|
+
: ''
|
|
1024
|
+
}
|
|
1025
|
+
`;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Set status message
|
|
1030
|
+
*/
|
|
1031
|
+
setStatus(message: string, type = ''): void {
|
|
1032
|
+
const statusEl = getElementByIdOrNull<HTMLElement>('dashboard-status');
|
|
1033
|
+
if (statusEl) {
|
|
1034
|
+
statusEl.textContent = message;
|
|
1035
|
+
statusEl.className = `text-sm text-center py-2 ${type === 'error' ? 'text-red-500' : 'text-gray-500 dark:text-gray-400'}`;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Cleanup interval on destroy
|
|
1041
|
+
*/
|
|
1042
|
+
cleanup(): void {
|
|
1043
|
+
if (this.updateInterval) {
|
|
1044
|
+
clearInterval(this.updateInterval);
|
|
1045
|
+
this.updateInterval = null;
|
|
1046
|
+
}
|
|
1047
|
+
if (this.onCronClick) {
|
|
1048
|
+
document.removeEventListener('click', this.onCronClick);
|
|
1049
|
+
this.onCronClick = null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|