@mooncompany/uplink-chat 0.5.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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// DASHBOARD MODULE
|
|
3
|
+
// Usage analytics, message stats, cost tracking
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
|
|
8
|
+
// State
|
|
9
|
+
let dashboardVisible = false;
|
|
10
|
+
let dashboardEl = null;
|
|
11
|
+
let stats = {
|
|
12
|
+
messages: [],
|
|
13
|
+
tokens: [],
|
|
14
|
+
sessions: 0
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Cost per 1K tokens (approximate for Claude)
|
|
18
|
+
const COST_PER_1K_INPUT = 0.003;
|
|
19
|
+
const COST_PER_1K_OUTPUT = 0.015;
|
|
20
|
+
|
|
21
|
+
function init() {
|
|
22
|
+
// Load saved stats
|
|
23
|
+
loadStats();
|
|
24
|
+
|
|
25
|
+
// Create dashboard panel
|
|
26
|
+
createDashboard();
|
|
27
|
+
|
|
28
|
+
// Add command
|
|
29
|
+
if (window.UplinkCommands) {
|
|
30
|
+
// Command will be added via commands.js
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Track messages
|
|
34
|
+
interceptMessages();
|
|
35
|
+
|
|
36
|
+
logger.debug('Dashboard: Initialized');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createDashboard() {
|
|
40
|
+
dashboardEl = document.createElement('div');
|
|
41
|
+
dashboardEl.className = 'dashboard-overlay';
|
|
42
|
+
dashboardEl.innerHTML = `
|
|
43
|
+
<div class="dashboard-panel">
|
|
44
|
+
<div class="dashboard-header">
|
|
45
|
+
<h2>📊 Usage Dashboard</h2>
|
|
46
|
+
<button class="dashboard-close">×</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="dashboard-content">
|
|
49
|
+
<div class="dashboard-section">
|
|
50
|
+
<h3>Today's Stats</h3>
|
|
51
|
+
<div class="stat-grid">
|
|
52
|
+
<div class="stat-card">
|
|
53
|
+
<span class="stat-value" id="statMessages">0</span>
|
|
54
|
+
<span class="stat-label">Messages</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="stat-card">
|
|
57
|
+
<span class="stat-value" id="statTokens">0</span>
|
|
58
|
+
<span class="stat-label">Tokens</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="stat-card">
|
|
61
|
+
<span class="stat-value" id="statCost">$0.00</span>
|
|
62
|
+
<span class="stat-label">Est. Cost</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="stat-card">
|
|
65
|
+
<span class="stat-value" id="statAvgResponse">0s</span>
|
|
66
|
+
<span class="stat-label">Avg Response</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="dashboard-section">
|
|
72
|
+
<h3>Activity (Last 7 Days)</h3>
|
|
73
|
+
<div class="chart-container">
|
|
74
|
+
<canvas id="activityChart" width="400" height="150"></canvas>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="dashboard-section">
|
|
79
|
+
<h3>Peak Hours</h3>
|
|
80
|
+
<div class="chart-container">
|
|
81
|
+
<canvas id="hoursChart" width="400" height="100"></canvas>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="dashboard-section">
|
|
86
|
+
<h3>All Time</h3>
|
|
87
|
+
<div class="stat-grid">
|
|
88
|
+
<div class="stat-card">
|
|
89
|
+
<span class="stat-value" id="statTotalMessages">0</span>
|
|
90
|
+
<span class="stat-label">Total Messages</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="stat-card">
|
|
93
|
+
<span class="stat-value" id="statTotalTokens">0</span>
|
|
94
|
+
<span class="stat-label">Total Tokens</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="stat-card">
|
|
97
|
+
<span class="stat-value" id="statTotalCost">$0.00</span>
|
|
98
|
+
<span class="stat-label">Total Cost</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="stat-card">
|
|
101
|
+
<span class="stat-value" id="statSessions">0</span>
|
|
102
|
+
<span class="stat-label">Sessions</span>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="dashboard-actions">
|
|
108
|
+
<button id="exportStats" class="dashboard-btn">Export Data</button>
|
|
109
|
+
<button id="resetStats" class="dashboard-btn danger">Reset Stats</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
document.body.appendChild(dashboardEl);
|
|
116
|
+
|
|
117
|
+
// Event listeners
|
|
118
|
+
dashboardEl.querySelector('.dashboard-close').addEventListener('click', hide);
|
|
119
|
+
dashboardEl.addEventListener('click', (e) => {
|
|
120
|
+
if (e.target === dashboardEl) hide();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
document.getElementById('exportStats')?.addEventListener('click', exportStats);
|
|
124
|
+
document.getElementById('resetStats')?.addEventListener('click', resetStats);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function show() {
|
|
128
|
+
dashboardVisible = true;
|
|
129
|
+
dashboardEl.classList.add('visible');
|
|
130
|
+
updateDisplay();
|
|
131
|
+
drawCharts();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hide() {
|
|
135
|
+
dashboardVisible = false;
|
|
136
|
+
dashboardEl.classList.remove('visible');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toggle() {
|
|
140
|
+
if (dashboardVisible) hide();
|
|
141
|
+
else show();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function loadStats() {
|
|
145
|
+
try {
|
|
146
|
+
const saved = localStorage.getItem('uplink-stats');
|
|
147
|
+
if (saved) {
|
|
148
|
+
const parsed = JSON.parse(saved);
|
|
149
|
+
stats = { ...stats, ...parsed };
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
logger.warn('Dashboard: Failed to load stats', e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function saveStats() {
|
|
157
|
+
try {
|
|
158
|
+
localStorage.setItem('uplink-stats', JSON.stringify(stats));
|
|
159
|
+
} catch (e) {
|
|
160
|
+
logger.warn('Dashboard: Failed to save stats', e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function interceptMessages() {
|
|
165
|
+
// Use shared fetch utilities if available
|
|
166
|
+
if (window.UplinkFetch) {
|
|
167
|
+
window.UplinkFetch.registerHook('afterResponse',
|
|
168
|
+
async (url, options, response, duration) => {
|
|
169
|
+
// Track chat API calls
|
|
170
|
+
if (url === '/api/chat') {
|
|
171
|
+
trackMessage('user', duration);
|
|
172
|
+
|
|
173
|
+
// Try to get token usage from response
|
|
174
|
+
try {
|
|
175
|
+
const data = await response.json();
|
|
176
|
+
if (data.usage) {
|
|
177
|
+
trackTokens(data.usage.prompt_tokens || 0, data.usage.completion_tokens || 0);
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
logger.warn('Dashboard: Failed to parse chat response for token tracking', e);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Also listen for streaming token updates from developer panel
|
|
188
|
+
const originalUpdateTokens = window.UplinkDeveloper?.updateTokens;
|
|
189
|
+
if (originalUpdateTokens) {
|
|
190
|
+
window.UplinkDeveloper.updateTokens = function(usage) {
|
|
191
|
+
originalUpdateTokens.call(window.UplinkDeveloper, usage);
|
|
192
|
+
trackTokens(
|
|
193
|
+
usage.prompt_tokens || usage.promptTokens || 0,
|
|
194
|
+
usage.completion_tokens || usage.completionTokens || 0
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function trackMessage(type, durationMs = 0) {
|
|
201
|
+
const now = new Date();
|
|
202
|
+
const today = now.toISOString().split('T')[0];
|
|
203
|
+
const hour = now.getHours();
|
|
204
|
+
|
|
205
|
+
stats.messages.push({
|
|
206
|
+
date: today,
|
|
207
|
+
hour,
|
|
208
|
+
type,
|
|
209
|
+
duration: durationMs,
|
|
210
|
+
timestamp: now.getTime()
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Keep only last 30 days of data
|
|
214
|
+
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
215
|
+
stats.messages = stats.messages.filter(m => m.timestamp > thirtyDaysAgo);
|
|
216
|
+
|
|
217
|
+
saveStats();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function trackTokens(input, output) {
|
|
221
|
+
const now = new Date();
|
|
222
|
+
const today = now.toISOString().split('T')[0];
|
|
223
|
+
|
|
224
|
+
stats.tokens.push({
|
|
225
|
+
date: today,
|
|
226
|
+
input,
|
|
227
|
+
output,
|
|
228
|
+
timestamp: now.getTime()
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Keep only last 30 days
|
|
232
|
+
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
233
|
+
stats.tokens = stats.tokens.filter(t => t.timestamp > thirtyDaysAgo);
|
|
234
|
+
|
|
235
|
+
saveStats();
|
|
236
|
+
|
|
237
|
+
if (dashboardVisible) {
|
|
238
|
+
updateDisplay();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function updateDisplay() {
|
|
243
|
+
const today = new Date().toISOString().split('T')[0];
|
|
244
|
+
|
|
245
|
+
// Today's messages
|
|
246
|
+
const todayMessages = stats.messages.filter(m => m.date === today);
|
|
247
|
+
document.getElementById('statMessages').textContent = todayMessages.length;
|
|
248
|
+
|
|
249
|
+
// Today's tokens
|
|
250
|
+
const todayTokens = stats.tokens.filter(t => t.date === today);
|
|
251
|
+
const todayInputTokens = todayTokens.reduce((sum, t) => sum + t.input, 0);
|
|
252
|
+
const todayOutputTokens = todayTokens.reduce((sum, t) => sum + t.output, 0);
|
|
253
|
+
const todayTotalTokens = todayInputTokens + todayOutputTokens;
|
|
254
|
+
document.getElementById('statTokens').textContent = formatNumber(todayTotalTokens);
|
|
255
|
+
|
|
256
|
+
// Today's cost
|
|
257
|
+
const todayCost = (todayInputTokens / 1000 * COST_PER_1K_INPUT) +
|
|
258
|
+
(todayOutputTokens / 1000 * COST_PER_1K_OUTPUT);
|
|
259
|
+
document.getElementById('statCost').textContent = '$' + todayCost.toFixed(2);
|
|
260
|
+
|
|
261
|
+
// Average response time
|
|
262
|
+
const responseTimes = todayMessages.filter(m => m.duration > 0).map(m => m.duration);
|
|
263
|
+
const avgResponse = responseTimes.length > 0
|
|
264
|
+
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length / 1000
|
|
265
|
+
: 0;
|
|
266
|
+
document.getElementById('statAvgResponse').textContent = avgResponse.toFixed(1) + 's';
|
|
267
|
+
|
|
268
|
+
// All time stats
|
|
269
|
+
document.getElementById('statTotalMessages').textContent = formatNumber(stats.messages.length);
|
|
270
|
+
|
|
271
|
+
const totalInputTokens = stats.tokens.reduce((sum, t) => sum + t.input, 0);
|
|
272
|
+
const totalOutputTokens = stats.tokens.reduce((sum, t) => sum + t.output, 0);
|
|
273
|
+
document.getElementById('statTotalTokens').textContent = formatNumber(totalInputTokens + totalOutputTokens);
|
|
274
|
+
|
|
275
|
+
const totalCost = (totalInputTokens / 1000 * COST_PER_1K_INPUT) +
|
|
276
|
+
(totalOutputTokens / 1000 * COST_PER_1K_OUTPUT);
|
|
277
|
+
document.getElementById('statTotalCost').textContent = '$' + totalCost.toFixed(2);
|
|
278
|
+
|
|
279
|
+
// Sessions (unique days with activity)
|
|
280
|
+
const uniqueDays = new Set(stats.messages.map(m => m.date));
|
|
281
|
+
document.getElementById('statSessions').textContent = uniqueDays.size;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function drawCharts() {
|
|
285
|
+
drawActivityChart();
|
|
286
|
+
drawHoursChart();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function drawActivityChart() {
|
|
290
|
+
const canvas = document.getElementById('activityChart');
|
|
291
|
+
if (!canvas) return;
|
|
292
|
+
|
|
293
|
+
const ctx = canvas.getContext('2d');
|
|
294
|
+
const width = canvas.width;
|
|
295
|
+
const height = canvas.height;
|
|
296
|
+
|
|
297
|
+
ctx.clearRect(0, 0, width, height);
|
|
298
|
+
|
|
299
|
+
// Get last 7 days
|
|
300
|
+
const days = [];
|
|
301
|
+
for (let i = 6; i >= 0; i--) {
|
|
302
|
+
const date = new Date();
|
|
303
|
+
date.setDate(date.getDate() - i);
|
|
304
|
+
days.push(date.toISOString().split('T')[0]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Count messages per day
|
|
308
|
+
const counts = days.map(day =>
|
|
309
|
+
stats.messages.filter(m => m.date === day).length
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const maxCount = Math.max(...counts, 1);
|
|
313
|
+
const barWidth = (width - 60) / 7;
|
|
314
|
+
const barGap = 8;
|
|
315
|
+
|
|
316
|
+
// Draw bars
|
|
317
|
+
ctx.fillStyle = 'rgba(0, 240, 255, 0.6)';
|
|
318
|
+
counts.forEach((count, i) => {
|
|
319
|
+
const barHeight = (count / maxCount) * (height - 40);
|
|
320
|
+
const x = 30 + i * barWidth + barGap / 2;
|
|
321
|
+
const y = height - 20 - barHeight;
|
|
322
|
+
|
|
323
|
+
ctx.beginPath();
|
|
324
|
+
ctx.roundRect(x, y, barWidth - barGap, barHeight, 4);
|
|
325
|
+
ctx.fill();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Draw labels
|
|
329
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
|
330
|
+
ctx.font = '10px system-ui';
|
|
331
|
+
ctx.textAlign = 'center';
|
|
332
|
+
days.forEach((day, i) => {
|
|
333
|
+
const label = new Date(day).toLocaleDateString('en', { weekday: 'short' });
|
|
334
|
+
const x = 30 + i * barWidth + barWidth / 2;
|
|
335
|
+
ctx.fillText(label, x, height - 5);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Draw count on top of bars
|
|
339
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
340
|
+
counts.forEach((count, i) => {
|
|
341
|
+
if (count > 0) {
|
|
342
|
+
const barHeight = (count / maxCount) * (height - 40);
|
|
343
|
+
const x = 30 + i * barWidth + barWidth / 2;
|
|
344
|
+
const y = height - 25 - barHeight;
|
|
345
|
+
ctx.fillText(count.toString(), x, y);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function drawHoursChart() {
|
|
351
|
+
const canvas = document.getElementById('hoursChart');
|
|
352
|
+
if (!canvas) return;
|
|
353
|
+
|
|
354
|
+
const ctx = canvas.getContext('2d');
|
|
355
|
+
const width = canvas.width;
|
|
356
|
+
const height = canvas.height;
|
|
357
|
+
|
|
358
|
+
ctx.clearRect(0, 0, width, height);
|
|
359
|
+
|
|
360
|
+
// Count messages per hour
|
|
361
|
+
const hourCounts = new Array(24).fill(0);
|
|
362
|
+
stats.messages.forEach(m => {
|
|
363
|
+
if (m.hour !== undefined) {
|
|
364
|
+
hourCounts[m.hour]++;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const maxCount = Math.max(...hourCounts, 1);
|
|
369
|
+
const barWidth = (width - 20) / 24;
|
|
370
|
+
|
|
371
|
+
// Draw bars
|
|
372
|
+
hourCounts.forEach((count, hour) => {
|
|
373
|
+
const barHeight = (count / maxCount) * (height - 25);
|
|
374
|
+
const x = 10 + hour * barWidth;
|
|
375
|
+
const y = height - 15 - barHeight;
|
|
376
|
+
|
|
377
|
+
// Color based on time of day
|
|
378
|
+
if (hour >= 6 && hour < 12) {
|
|
379
|
+
ctx.fillStyle = 'rgba(255, 200, 100, 0.6)'; // Morning
|
|
380
|
+
} else if (hour >= 12 && hour < 18) {
|
|
381
|
+
ctx.fillStyle = 'rgba(0, 240, 255, 0.6)'; // Afternoon
|
|
382
|
+
} else if (hour >= 18 && hour < 22) {
|
|
383
|
+
ctx.fillStyle = 'rgba(180, 100, 255, 0.6)'; // Evening
|
|
384
|
+
} else {
|
|
385
|
+
ctx.fillStyle = 'rgba(100, 100, 150, 0.6)'; // Night
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
ctx.fillRect(x, y, barWidth - 1, barHeight);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Draw hour labels (every 6 hours)
|
|
392
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
|
393
|
+
ctx.font = '9px system-ui';
|
|
394
|
+
ctx.textAlign = 'center';
|
|
395
|
+
[0, 6, 12, 18].forEach(hour => {
|
|
396
|
+
const x = 10 + hour * barWidth + barWidth / 2;
|
|
397
|
+
const label = hour === 0 ? '12am' : hour === 12 ? '12pm' :
|
|
398
|
+
hour < 12 ? hour + 'am' : (hour - 12) + 'pm';
|
|
399
|
+
ctx.fillText(label, x, height - 3);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function exportStats() {
|
|
404
|
+
const data = {
|
|
405
|
+
exported: new Date().toISOString(),
|
|
406
|
+
messages: stats.messages,
|
|
407
|
+
tokens: stats.tokens,
|
|
408
|
+
summary: {
|
|
409
|
+
totalMessages: stats.messages.length,
|
|
410
|
+
totalInputTokens: stats.tokens.reduce((s, t) => s + t.input, 0),
|
|
411
|
+
totalOutputTokens: stats.tokens.reduce((s, t) => s + t.output, 0)
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
416
|
+
const url = URL.createObjectURL(blob);
|
|
417
|
+
const a = document.createElement('a');
|
|
418
|
+
a.href = url;
|
|
419
|
+
a.download = `uplink-stats-${new Date().toISOString().split('T')[0]}.json`;
|
|
420
|
+
a.click();
|
|
421
|
+
URL.revokeObjectURL(url);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function resetStats() {
|
|
425
|
+
if (!confirm('Reset all usage statistics? This cannot be undone.')) return;
|
|
426
|
+
|
|
427
|
+
stats = { messages: [], tokens: [], sessions: 0 };
|
|
428
|
+
saveStats();
|
|
429
|
+
updateDisplay();
|
|
430
|
+
drawCharts();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function formatNumber(n) {
|
|
434
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
435
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
436
|
+
return n.toString();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Expose API
|
|
440
|
+
export const UplinkDashboard = {
|
|
441
|
+
show,
|
|
442
|
+
hide,
|
|
443
|
+
toggle,
|
|
444
|
+
trackMessage,
|
|
445
|
+
trackTokens
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Backward compat: assign to window
|
|
449
|
+
window.UplinkDashboard = UplinkDashboard;
|
|
450
|
+
|
|
451
|
+
// Register with core for coordinated initialization
|
|
452
|
+
UplinkCore.registerModule('dashboard', init);
|