claude-code-templates 1.26.1 → 1.26.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/package.json +1 -1
- package/src/analytics-web/chats_mobile.html +471 -0
- package/src/chats-mobile.js +325 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-templates",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.3",
|
|
4
4
|
"description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -782,6 +782,15 @@
|
|
|
782
782
|
stroke: rgba(251, 146, 60, 1);
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
.action-btn.analytics-btn:hover {
|
|
786
|
+
background: rgba(139, 92, 246, 0.15);
|
|
787
|
+
color: rgba(139, 92, 246, 1);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.action-btn.analytics-btn:hover svg {
|
|
791
|
+
stroke: rgba(139, 92, 246, 1);
|
|
792
|
+
}
|
|
793
|
+
|
|
785
794
|
/* Conversations list */
|
|
786
795
|
.conversations-list {
|
|
787
796
|
flex: 1;
|
|
@@ -2067,6 +2076,14 @@
|
|
|
2067
2076
|
</svg>
|
|
2068
2077
|
<span>Search</span>
|
|
2069
2078
|
</button>
|
|
2079
|
+
<button class="action-btn analytics-btn" id="showAnalytics" onclick="showAnalyticsModal()">
|
|
2080
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2081
|
+
<line x1="18" y1="20" x2="18" y2="10"></line>
|
|
2082
|
+
<line x1="12" y1="20" x2="12" y2="4"></line>
|
|
2083
|
+
<line x1="6" y1="20" x2="6" y2="14"></line>
|
|
2084
|
+
</svg>
|
|
2085
|
+
<span>Analytics</span>
|
|
2086
|
+
</button>
|
|
2070
2087
|
</div>
|
|
2071
2088
|
</div>
|
|
2072
2089
|
<div class="header-right">
|
|
@@ -2196,6 +2213,176 @@
|
|
|
2196
2213
|
</div>
|
|
2197
2214
|
</div>
|
|
2198
2215
|
|
|
2216
|
+
<!-- Analytics Modal -->
|
|
2217
|
+
<div class="modal-overlay" id="analyticsModal">
|
|
2218
|
+
<div class="modal" style="max-width: 900px; max-height: 90vh; overflow-y: auto;">
|
|
2219
|
+
<div class="modal-header">
|
|
2220
|
+
<h3 class="modal-title">Session Analytics (Beta)</h3>
|
|
2221
|
+
</div>
|
|
2222
|
+
<div style="padding: 20px;">
|
|
2223
|
+
<!-- Loading State -->
|
|
2224
|
+
<div id="analyticsLoading" style="text-align: center; padding: 40px;">
|
|
2225
|
+
<p style="color: var(--text-secondary);">Loading analytics data...</p>
|
|
2226
|
+
</div>
|
|
2227
|
+
|
|
2228
|
+
<!-- Analytics Content (hidden initially) -->
|
|
2229
|
+
<div id="analyticsContent" style="display: none;">
|
|
2230
|
+
<!-- Overview Stats - 3 columns -->
|
|
2231
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px;">
|
|
2232
|
+
<div style="background: rgba(139, 92, 246, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #8b5cf6;">
|
|
2233
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Total Messages</div>
|
|
2234
|
+
<div style="font-size: 24px; font-weight: 700; color: #8b5cf6;" id="analyticsMessageCount">-</div>
|
|
2235
|
+
</div>
|
|
2236
|
+
<div style="background: rgba(59, 130, 246, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #3b82f6;">
|
|
2237
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Total Tokens</div>
|
|
2238
|
+
<div style="font-size: 24px; font-weight: 700; color: #3b82f6;" id="analyticsTotalTokens">-</div>
|
|
2239
|
+
</div>
|
|
2240
|
+
<div style="background: rgba(16, 185, 129, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #10b981;">
|
|
2241
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Tool Calls</div>
|
|
2242
|
+
<div style="font-size: 24px; font-weight: 700; color: #10b981;" id="analyticsToolCalls">-</div>
|
|
2243
|
+
</div>
|
|
2244
|
+
<div style="background: rgba(234, 88, 12, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #ea580c;">
|
|
2245
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Tools Used</div>
|
|
2246
|
+
<div style="font-size: 24px; font-weight: 700; color: #ea580c;" id="analyticsUniqueTools">-</div>
|
|
2247
|
+
</div>
|
|
2248
|
+
<div style="background: rgba(251, 146, 60, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #fb923c;">
|
|
2249
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Models Used</div>
|
|
2250
|
+
<div style="font-size: 24px; font-weight: 700; color: #fb923c;" id="analyticsUniqueModels">-</div>
|
|
2251
|
+
</div>
|
|
2252
|
+
<div style="background: rgba(168, 85, 247, 0.1); border-radius: 8px; padding: 12px; border-left: 3px solid #a855f7;">
|
|
2253
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Cache Efficiency</div>
|
|
2254
|
+
<div style="font-size: 24px; font-weight: 700; color: #a855f7;" id="analyticsCacheEfficiency">-</div>
|
|
2255
|
+
</div>
|
|
2256
|
+
</div>
|
|
2257
|
+
|
|
2258
|
+
<!-- Token Usage Breakdown -->
|
|
2259
|
+
<div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
|
2260
|
+
<h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
|
|
2261
|
+
Token Usage Breakdown
|
|
2262
|
+
</h4>
|
|
2263
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
|
2264
|
+
<div>
|
|
2265
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Input Tokens</div>
|
|
2266
|
+
<div style="font-size: 18px; font-weight: 600; color: #3b82f6;" id="analyticsInputTokens">-</div>
|
|
2267
|
+
</div>
|
|
2268
|
+
<div>
|
|
2269
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Output Tokens</div>
|
|
2270
|
+
<div style="font-size: 18px; font-weight: 600; color: #8b5cf6;" id="analyticsOutputTokens">-</div>
|
|
2271
|
+
</div>
|
|
2272
|
+
<div>
|
|
2273
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Cache Creation</div>
|
|
2274
|
+
<div style="font-size: 18px; font-weight: 600; color: #fb923c;" id="analyticsCacheCreation">-</div>
|
|
2275
|
+
</div>
|
|
2276
|
+
<div>
|
|
2277
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Cache Reads</div>
|
|
2278
|
+
<div style="font-size: 18px; font-weight: 600; color: #10b981;" id="analyticsCacheReads">-</div>
|
|
2279
|
+
</div>
|
|
2280
|
+
</div>
|
|
2281
|
+
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.1);">
|
|
2282
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Cost Estimate (USD)</div>
|
|
2283
|
+
<div style="font-size: 16px; font-weight: 600; color: #10b981;" id="analyticsCostEstimate">$0.00</div>
|
|
2284
|
+
</div>
|
|
2285
|
+
</div>
|
|
2286
|
+
|
|
2287
|
+
<!-- Session Timeline -->
|
|
2288
|
+
<div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
|
2289
|
+
<h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
|
|
2290
|
+
Session Timeline
|
|
2291
|
+
</h4>
|
|
2292
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
|
2293
|
+
<div>
|
|
2294
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Started</div>
|
|
2295
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsStartTime">-</div>
|
|
2296
|
+
</div>
|
|
2297
|
+
<div>
|
|
2298
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Last Activity</div>
|
|
2299
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsLastActivity">-</div>
|
|
2300
|
+
</div>
|
|
2301
|
+
<div>
|
|
2302
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Duration</div>
|
|
2303
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsDuration">-</div>
|
|
2304
|
+
</div>
|
|
2305
|
+
<div>
|
|
2306
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Status</div>
|
|
2307
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsStatus">-</div>
|
|
2308
|
+
</div>
|
|
2309
|
+
</div>
|
|
2310
|
+
</div>
|
|
2311
|
+
|
|
2312
|
+
<!-- Time Breakdown -->
|
|
2313
|
+
<div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
|
2314
|
+
<h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
|
|
2315
|
+
Time Breakdown
|
|
2316
|
+
</h4>
|
|
2317
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">
|
|
2318
|
+
<div>
|
|
2319
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Claude Executing</div>
|
|
2320
|
+
<div style="font-size: 18px; font-weight: 600; color: #3b82f6;" id="analyticsWaitTime">-</div>
|
|
2321
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 2px;" id="analyticsWaitPercent">-</div>
|
|
2322
|
+
</div>
|
|
2323
|
+
<div>
|
|
2324
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">User Conversing</div>
|
|
2325
|
+
<div style="font-size: 18px; font-weight: 600; color: #10b981;" id="analyticsUserTime">-</div>
|
|
2326
|
+
<div style="font-size: 11px; color: var(--text-secondary); margin-top: 2px;" id="analyticsUserPercent">-</div>
|
|
2327
|
+
</div>
|
|
2328
|
+
</div>
|
|
2329
|
+
<div style="height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; display: flex;">
|
|
2330
|
+
<div id="analyticsWaitBar" style="background: linear-gradient(90deg, #3b82f6, #2563eb); transition: width 0.3s ease;"></div>
|
|
2331
|
+
<div id="analyticsUserBar" style="background: linear-gradient(90deg, #10b981, #059669); transition: width 0.3s ease;"></div>
|
|
2332
|
+
</div>
|
|
2333
|
+
</div>
|
|
2334
|
+
|
|
2335
|
+
<!-- Model Information -->
|
|
2336
|
+
<div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
|
2337
|
+
<h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
|
|
2338
|
+
Model Information
|
|
2339
|
+
</h4>
|
|
2340
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
|
2341
|
+
<div>
|
|
2342
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Primary Model</div>
|
|
2343
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary); font-family: monospace;" id="analyticsPrimaryModel">-</div>
|
|
2344
|
+
</div>
|
|
2345
|
+
<div>
|
|
2346
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Service Tier</div>
|
|
2347
|
+
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary);" id="analyticsServiceTier">-</div>
|
|
2348
|
+
</div>
|
|
2349
|
+
</div>
|
|
2350
|
+
<div id="analyticsAllModels" style="display: none; margin-top: 12px;">
|
|
2351
|
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">All Models Used</div>
|
|
2352
|
+
<div id="analyticsModelsList" style="display: flex; flex-wrap: wrap; gap: 6px;">
|
|
2353
|
+
<!-- Models will be populated here -->
|
|
2354
|
+
</div>
|
|
2355
|
+
</div>
|
|
2356
|
+
</div>
|
|
2357
|
+
|
|
2358
|
+
<!-- Tool Usage -->
|
|
2359
|
+
<div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
|
2360
|
+
<h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
|
|
2361
|
+
Tool Usage
|
|
2362
|
+
</h4>
|
|
2363
|
+
<div id="analyticsToolsList" style="max-height: 200px; overflow-y: auto;">
|
|
2364
|
+
<!-- Tool usage will be populated here -->
|
|
2365
|
+
</div>
|
|
2366
|
+
</div>
|
|
2367
|
+
|
|
2368
|
+
<!-- Components Used -->
|
|
2369
|
+
<div id="analyticsComponentsSection" style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 16px; margin-bottom: 16px; display: none;">
|
|
2370
|
+
<h4 style="margin: 0 0 12px 0; color: var(--text-primary); font-size: 14px;">
|
|
2371
|
+
Components Used
|
|
2372
|
+
</h4>
|
|
2373
|
+
<div id="analyticsComponentsContent">
|
|
2374
|
+
<!-- Components will be populated here -->
|
|
2375
|
+
</div>
|
|
2376
|
+
</div>
|
|
2377
|
+
</div>
|
|
2378
|
+
</div>
|
|
2379
|
+
|
|
2380
|
+
<div class="modal-actions">
|
|
2381
|
+
<button class="modal-btn secondary" onclick="closeAnalyticsModal()">Close</button>
|
|
2382
|
+
</div>
|
|
2383
|
+
</div>
|
|
2384
|
+
</div>
|
|
2385
|
+
|
|
2199
2386
|
<!-- Import WebSocket and Data Services -->
|
|
2200
2387
|
<script src="services/WebSocketService.js"></script>
|
|
2201
2388
|
<script src="services/DataService.js"></script>
|
|
@@ -2601,8 +2788,10 @@
|
|
|
2601
2788
|
|
|
2602
2789
|
const resumeBtn = document.getElementById('resumeConversation');
|
|
2603
2790
|
const downloadBtn = document.getElementById('downloadConversation');
|
|
2791
|
+
const analyticsBtn = document.getElementById('showAnalytics');
|
|
2604
2792
|
resumeBtn.setAttribute('data-conversation-id', conversationId);
|
|
2605
2793
|
downloadBtn.setAttribute('data-conversation-id', conversationId);
|
|
2794
|
+
analyticsBtn.setAttribute('data-conversation-id', conversationId);
|
|
2606
2795
|
|
|
2607
2796
|
// Load messages (placeholder for now)
|
|
2608
2797
|
this.loadChatMessages(conversationId);
|
|
@@ -4762,6 +4951,288 @@
|
|
|
4762
4951
|
}
|
|
4763
4952
|
}
|
|
4764
4953
|
|
|
4954
|
+
// Analytics Modal Functions
|
|
4955
|
+
async function showAnalyticsModal() {
|
|
4956
|
+
const analyticsBtn = document.getElementById('showAnalytics');
|
|
4957
|
+
const conversationId = analyticsBtn.getAttribute('data-conversation-id');
|
|
4958
|
+
|
|
4959
|
+
if (!conversationId) {
|
|
4960
|
+
console.error('No conversation ID found');
|
|
4961
|
+
return;
|
|
4962
|
+
}
|
|
4963
|
+
|
|
4964
|
+
console.log('📊 Opening analytics modal for conversation:', conversationId);
|
|
4965
|
+
|
|
4966
|
+
// Show analytics modal
|
|
4967
|
+
const analyticsModal = document.getElementById('analyticsModal');
|
|
4968
|
+
analyticsModal.classList.add('show');
|
|
4969
|
+
|
|
4970
|
+
// Reset to loading state
|
|
4971
|
+
document.getElementById('analyticsLoading').style.display = 'block';
|
|
4972
|
+
document.getElementById('analyticsContent').style.display = 'none';
|
|
4973
|
+
|
|
4974
|
+
// Close modal when clicking outside
|
|
4975
|
+
analyticsModal.addEventListener('click', (e) => {
|
|
4976
|
+
if (e.target === analyticsModal) {
|
|
4977
|
+
closeAnalyticsModal();
|
|
4978
|
+
}
|
|
4979
|
+
});
|
|
4980
|
+
|
|
4981
|
+
// Fetch analytics data
|
|
4982
|
+
try {
|
|
4983
|
+
const response = await fetch(`/api/conversations/${conversationId}/analytics`);
|
|
4984
|
+
|
|
4985
|
+
if (!response.ok) {
|
|
4986
|
+
throw new Error(`Failed to fetch analytics: ${response.statusText}`);
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
const data = await response.json();
|
|
4990
|
+
console.log('✅ Analytics fetched:', data);
|
|
4991
|
+
|
|
4992
|
+
// Populate modal with analytics data
|
|
4993
|
+
populateAnalyticsModal(data.analytics);
|
|
4994
|
+
|
|
4995
|
+
// Hide loading, show content
|
|
4996
|
+
document.getElementById('analyticsLoading').style.display = 'none';
|
|
4997
|
+
document.getElementById('analyticsContent').style.display = 'block';
|
|
4998
|
+
|
|
4999
|
+
} catch (error) {
|
|
5000
|
+
console.error('❌ Failed to fetch analytics:', error);
|
|
5001
|
+
document.getElementById('analyticsLoading').innerHTML =
|
|
5002
|
+
`<p style="color: #ef4444;">Failed to load analytics: ${error.message}</p>`;
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
function closeAnalyticsModal() {
|
|
5007
|
+
const analyticsModal = document.getElementById('analyticsModal');
|
|
5008
|
+
analyticsModal.classList.remove('show');
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
function populateAnalyticsModal(analytics) {
|
|
5012
|
+
console.log('📊 Populating analytics modal with data:', analytics);
|
|
5013
|
+
|
|
5014
|
+
// Overview stats - with safe fallbacks
|
|
5015
|
+
document.getElementById('analyticsMessageCount').textContent = (analytics.messageCount || 0).toLocaleString();
|
|
5016
|
+
document.getElementById('analyticsTotalTokens').textContent = (analytics.totalTokens || 0).toLocaleString();
|
|
5017
|
+
document.getElementById('analyticsToolCalls').textContent = (analytics.toolCalls || 0).toLocaleString();
|
|
5018
|
+
document.getElementById('analyticsCacheEfficiency').textContent = analytics.cacheEfficiency || '0%';
|
|
5019
|
+
|
|
5020
|
+
// New stats: unique tools and models
|
|
5021
|
+
const uniqueTools = analytics.toolUsage?.uniqueTools || 0;
|
|
5022
|
+
const uniqueModels = (analytics.modelInfo?.modelUsage && Array.isArray(analytics.modelInfo.modelUsage))
|
|
5023
|
+
? analytics.modelInfo.modelUsage.length
|
|
5024
|
+
: 0;
|
|
5025
|
+
|
|
5026
|
+
console.log('📊 Unique tools:', uniqueTools, 'Unique models:', uniqueModels);
|
|
5027
|
+
document.getElementById('analyticsUniqueTools').textContent = uniqueTools;
|
|
5028
|
+
document.getElementById('analyticsUniqueModels').textContent = uniqueModels;
|
|
5029
|
+
|
|
5030
|
+
// Token breakdown
|
|
5031
|
+
document.getElementById('analyticsInputTokens').textContent = analytics.tokenUsage.inputTokens.toLocaleString();
|
|
5032
|
+
document.getElementById('analyticsOutputTokens').textContent = analytics.tokenUsage.outputTokens.toLocaleString();
|
|
5033
|
+
document.getElementById('analyticsCacheCreation').textContent = analytics.tokenUsage.cacheCreationTokens.toLocaleString();
|
|
5034
|
+
document.getElementById('analyticsCacheReads').textContent = analytics.tokenUsage.cacheReadTokens.toLocaleString();
|
|
5035
|
+
|
|
5036
|
+
// Cost estimate
|
|
5037
|
+
document.getElementById('analyticsCostEstimate').textContent = `$${analytics.costEstimate.total}`;
|
|
5038
|
+
|
|
5039
|
+
// Model info
|
|
5040
|
+
document.getElementById('analyticsPrimaryModel').textContent = analytics.modelInfo.primaryModel;
|
|
5041
|
+
document.getElementById('analyticsServiceTier').textContent = analytics.modelInfo.serviceTier;
|
|
5042
|
+
|
|
5043
|
+
// Show all models if multiple were used with usage percentages
|
|
5044
|
+
const allModelsSection = document.getElementById('analyticsAllModels');
|
|
5045
|
+
if (analytics.modelInfo.hasMultipleModels && analytics.modelInfo.modelUsage && analytics.modelInfo.modelUsage.length > 1) {
|
|
5046
|
+
const modelsListHTML = analytics.modelInfo.modelUsage
|
|
5047
|
+
.map(usage => `
|
|
5048
|
+
<span style="
|
|
5049
|
+
display: inline-flex;
|
|
5050
|
+
align-items: center;
|
|
5051
|
+
gap: 6px;
|
|
5052
|
+
padding: 4px 10px;
|
|
5053
|
+
background: rgba(139, 92, 246, 0.15);
|
|
5054
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
5055
|
+
border-radius: 4px;
|
|
5056
|
+
font-size: 12px;
|
|
5057
|
+
font-family: monospace;
|
|
5058
|
+
color: var(--text-primary);
|
|
5059
|
+
">
|
|
5060
|
+
<span>${usage.model}</span>
|
|
5061
|
+
<span style="
|
|
5062
|
+
padding: 2px 6px;
|
|
5063
|
+
background: rgba(139, 92, 246, 0.3);
|
|
5064
|
+
border-radius: 3px;
|
|
5065
|
+
font-size: 10px;
|
|
5066
|
+
font-weight: 600;
|
|
5067
|
+
color: #e9d5ff;
|
|
5068
|
+
">${usage.count} msgs (${usage.percentage}%)</span>
|
|
5069
|
+
</span>
|
|
5070
|
+
`)
|
|
5071
|
+
.join('');
|
|
5072
|
+
document.getElementById('analyticsModelsList').innerHTML = modelsListHTML;
|
|
5073
|
+
allModelsSection.style.display = 'block';
|
|
5074
|
+
} else {
|
|
5075
|
+
allModelsSection.style.display = 'none';
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
// Tool usage breakdown
|
|
5079
|
+
const toolsList = document.getElementById('analyticsToolsList');
|
|
5080
|
+
if (Object.keys(analytics.toolUsage.breakdown).length > 0) {
|
|
5081
|
+
const toolsHTML = Object.entries(analytics.toolUsage.breakdown)
|
|
5082
|
+
.sort((a, b) => b[1] - a[1]) // Sort by usage count
|
|
5083
|
+
.map(([tool, count]) => {
|
|
5084
|
+
const percentage = Math.round((count / analytics.toolUsage.totalCalls) * 100);
|
|
5085
|
+
return `
|
|
5086
|
+
<div style="margin-bottom: 10px; padding: 8px; background: rgba(255,255,255,0.03); border-radius: 6px;">
|
|
5087
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
|
5088
|
+
<span style="font-size: 13px; font-weight: 600; color: var(--text-primary);">${tool}</span>
|
|
5089
|
+
<span style="font-size: 12px; color: var(--text-secondary);">${count} calls (${percentage}%)</span>
|
|
5090
|
+
</div>
|
|
5091
|
+
<div style="height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden;">
|
|
5092
|
+
<div style="height: 100%; width: ${percentage}%; background: linear-gradient(90deg, #10b981, #059669); transition: width 0.3s ease;"></div>
|
|
5093
|
+
</div>
|
|
5094
|
+
</div>
|
|
5095
|
+
`;
|
|
5096
|
+
})
|
|
5097
|
+
.join('');
|
|
5098
|
+
toolsList.innerHTML = toolsHTML;
|
|
5099
|
+
} else {
|
|
5100
|
+
toolsList.innerHTML = '<p style="color: var(--text-secondary); font-size: 13px; text-align: center; padding: 20px;">No tool usage detected in this session</p>';
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
// Timeline
|
|
5104
|
+
const formatDate = (isoString) => {
|
|
5105
|
+
if (!isoString) return 'N/A';
|
|
5106
|
+
const date = new Date(isoString);
|
|
5107
|
+
return date.toLocaleString('en-US', {
|
|
5108
|
+
month: 'short',
|
|
5109
|
+
day: 'numeric',
|
|
5110
|
+
hour: '2-digit',
|
|
5111
|
+
minute: '2-digit'
|
|
5112
|
+
});
|
|
5113
|
+
};
|
|
5114
|
+
|
|
5115
|
+
document.getElementById('analyticsStartTime').textContent = formatDate(analytics.timeline.startTime);
|
|
5116
|
+
document.getElementById('analyticsLastActivity').textContent = formatDate(analytics.timeline.endTime);
|
|
5117
|
+
document.getElementById('analyticsDuration').textContent = analytics.timeline.duration;
|
|
5118
|
+
document.getElementById('analyticsStatus').textContent = analytics.timeline.status.charAt(0).toUpperCase() + analytics.timeline.status.slice(1);
|
|
5119
|
+
|
|
5120
|
+
// Time breakdown
|
|
5121
|
+
if (analytics.timeBreakdown && analytics.timeBreakdown.totalWaitTime) {
|
|
5122
|
+
document.getElementById('analyticsWaitTime').textContent = analytics.timeBreakdown.totalWaitTime || '0s';
|
|
5123
|
+
document.getElementById('analyticsUserTime').textContent = analytics.timeBreakdown.totalUserTime || '0s';
|
|
5124
|
+
document.getElementById('analyticsWaitPercent').textContent = `${analytics.timeBreakdown.waitTimePercent || 0}% of iteration time`;
|
|
5125
|
+
document.getElementById('analyticsUserPercent').textContent = `${analytics.timeBreakdown.userTimePercent || 0}% of iteration time`;
|
|
5126
|
+
|
|
5127
|
+
// Update progress bars
|
|
5128
|
+
document.getElementById('analyticsWaitBar').style.width = `${analytics.timeBreakdown.waitTimePercent || 0}%`;
|
|
5129
|
+
document.getElementById('analyticsUserBar').style.width = `${analytics.timeBreakdown.userTimePercent || 0}%`;
|
|
5130
|
+
} else {
|
|
5131
|
+
// No timing data available
|
|
5132
|
+
document.getElementById('analyticsWaitTime').textContent = 'N/A';
|
|
5133
|
+
document.getElementById('analyticsUserTime').textContent = 'N/A';
|
|
5134
|
+
document.getElementById('analyticsWaitPercent').textContent = 'No data';
|
|
5135
|
+
document.getElementById('analyticsUserPercent').textContent = 'No data';
|
|
5136
|
+
document.getElementById('analyticsWaitBar').style.width = '0%';
|
|
5137
|
+
document.getElementById('analyticsUserBar').style.width = '0%';
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
// Components used (agents, commands, skills)
|
|
5141
|
+
if (analytics.componentsUsed) {
|
|
5142
|
+
const hasComponents = analytics.componentsUsed.totalAgents > 0 ||
|
|
5143
|
+
analytics.componentsUsed.totalCommands > 0 ||
|
|
5144
|
+
analytics.componentsUsed.totalSkills > 0;
|
|
5145
|
+
|
|
5146
|
+
if (hasComponents) {
|
|
5147
|
+
let componentsHTML = '';
|
|
5148
|
+
|
|
5149
|
+
// Agents
|
|
5150
|
+
if (analytics.componentsUsed.agents.length > 0) {
|
|
5151
|
+
componentsHTML += '<div style="margin-bottom: 12px;">';
|
|
5152
|
+
componentsHTML += '<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600;">Agents</div>';
|
|
5153
|
+
componentsHTML += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
|
|
5154
|
+
analytics.componentsUsed.agents.forEach(agent => {
|
|
5155
|
+
componentsHTML += `
|
|
5156
|
+
<span style="
|
|
5157
|
+
display: inline-flex;
|
|
5158
|
+
align-items: center;
|
|
5159
|
+
gap: 4px;
|
|
5160
|
+
padding: 4px 10px;
|
|
5161
|
+
background: rgba(139, 92, 246, 0.15);
|
|
5162
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
5163
|
+
border-radius: 4px;
|
|
5164
|
+
font-size: 12px;
|
|
5165
|
+
color: var(--text-primary);
|
|
5166
|
+
">
|
|
5167
|
+
<span>${agent.type}</span>
|
|
5168
|
+
<span style="color: var(--text-secondary); font-size: 11px;">×${agent.count}</span>
|
|
5169
|
+
</span>
|
|
5170
|
+
`;
|
|
5171
|
+
});
|
|
5172
|
+
componentsHTML += '</div></div>';
|
|
5173
|
+
}
|
|
5174
|
+
|
|
5175
|
+
// Slash Commands
|
|
5176
|
+
if (analytics.componentsUsed.slashCommands.length > 0) {
|
|
5177
|
+
componentsHTML += '<div style="margin-bottom: 12px;">';
|
|
5178
|
+
componentsHTML += '<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600;">Slash Commands</div>';
|
|
5179
|
+
componentsHTML += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
|
|
5180
|
+
analytics.componentsUsed.slashCommands.forEach(cmd => {
|
|
5181
|
+
componentsHTML += `
|
|
5182
|
+
<span style="
|
|
5183
|
+
display: inline-flex;
|
|
5184
|
+
align-items: center;
|
|
5185
|
+
gap: 4px;
|
|
5186
|
+
padding: 4px 10px;
|
|
5187
|
+
background: rgba(59, 130, 246, 0.15);
|
|
5188
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
5189
|
+
border-radius: 4px;
|
|
5190
|
+
font-size: 12px;
|
|
5191
|
+
font-family: monospace;
|
|
5192
|
+
color: var(--text-primary);
|
|
5193
|
+
">
|
|
5194
|
+
<span>${cmd.name}</span>
|
|
5195
|
+
<span style="color: var(--text-secondary); font-size: 11px;">×${cmd.count}</span>
|
|
5196
|
+
</span>
|
|
5197
|
+
`;
|
|
5198
|
+
});
|
|
5199
|
+
componentsHTML += '</div></div>';
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
// Skills
|
|
5203
|
+
if (analytics.componentsUsed.skills.length > 0) {
|
|
5204
|
+
componentsHTML += '<div>';
|
|
5205
|
+
componentsHTML += '<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600;">Skills</div>';
|
|
5206
|
+
componentsHTML += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
|
|
5207
|
+
analytics.componentsUsed.skills.forEach(skill => {
|
|
5208
|
+
componentsHTML += `
|
|
5209
|
+
<span style="
|
|
5210
|
+
display: inline-flex;
|
|
5211
|
+
align-items: center;
|
|
5212
|
+
gap: 4px;
|
|
5213
|
+
padding: 4px 10px;
|
|
5214
|
+
background: rgba(16, 185, 129, 0.15);
|
|
5215
|
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
5216
|
+
border-radius: 4px;
|
|
5217
|
+
font-size: 12px;
|
|
5218
|
+
color: var(--text-primary);
|
|
5219
|
+
">
|
|
5220
|
+
<span>${skill.name}</span>
|
|
5221
|
+
<span style="color: var(--text-secondary); font-size: 11px;">×${skill.count}</span>
|
|
5222
|
+
</span>
|
|
5223
|
+
`;
|
|
5224
|
+
});
|
|
5225
|
+
componentsHTML += '</div></div>';
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
document.getElementById('analyticsComponentsContent').innerHTML = componentsHTML;
|
|
5229
|
+
document.getElementById('analyticsComponentsSection').style.display = 'block';
|
|
5230
|
+
} else {
|
|
5231
|
+
document.getElementById('analyticsComponentsSection').style.display = 'none';
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
|
|
4765
5236
|
// Initialize the app
|
|
4766
5237
|
document.addEventListener('DOMContentLoaded', () => {
|
|
4767
5238
|
new ChatsMobileApp();
|
package/src/chats-mobile.js
CHANGED
|
@@ -9,6 +9,7 @@ const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
|
|
|
9
9
|
const StateCalculator = require('./analytics/core/StateCalculator');
|
|
10
10
|
const FileWatcher = require('./analytics/core/FileWatcher');
|
|
11
11
|
const DataCache = require('./analytics/data/DataCache');
|
|
12
|
+
const AgentAnalyzer = require('./analytics/core/AgentAnalyzer');
|
|
12
13
|
const WebSocketServer = require('./analytics/notifications/WebSocketServer');
|
|
13
14
|
const SessionSharing = require('./session-sharing');
|
|
14
15
|
|
|
@@ -512,6 +513,289 @@ class ChatsMobile {
|
|
|
512
513
|
}
|
|
513
514
|
});
|
|
514
515
|
|
|
516
|
+
// API to get detailed analytics for a conversation
|
|
517
|
+
this.app.get('/api/conversations/:id/analytics', async (req, res) => {
|
|
518
|
+
try {
|
|
519
|
+
const conversationId = req.params.id;
|
|
520
|
+
const conversation = this.data.conversations.find(conv => conv.id === conversationId);
|
|
521
|
+
|
|
522
|
+
if (!conversation) {
|
|
523
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log(chalk.cyan(`📊 Fetching analytics for conversation ${conversationId}...`));
|
|
527
|
+
|
|
528
|
+
// Get parsed messages for this conversation
|
|
529
|
+
const messages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
|
|
530
|
+
|
|
531
|
+
// Calculate session duration and timing breakdown
|
|
532
|
+
const startTime = messages.length > 0 ? new Date(messages[0].timestamp) : null;
|
|
533
|
+
const endTime = messages.length > 0 ? new Date(messages[messages.length - 1].timestamp) : null;
|
|
534
|
+
const durationMs = startTime && endTime ? endTime - startTime : 0;
|
|
535
|
+
const durationHours = Math.floor(durationMs / (1000 * 60 * 60));
|
|
536
|
+
const durationMinutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
537
|
+
|
|
538
|
+
// Calculate time conversing vs executing (time between messages)
|
|
539
|
+
let totalWaitTime = 0; // Time waiting for Claude (thinking + executing)
|
|
540
|
+
let totalUserTime = 0; // Time user takes to respond
|
|
541
|
+
|
|
542
|
+
// Find user and assistant messages only (ignore tool results and other message types)
|
|
543
|
+
const conversationMessages = messages.filter(msg => msg.role === 'user' || msg.role === 'assistant');
|
|
544
|
+
|
|
545
|
+
let lastUserTime = null;
|
|
546
|
+
let lastAssistantTime = null;
|
|
547
|
+
|
|
548
|
+
conversationMessages.forEach(msg => {
|
|
549
|
+
if (msg.role === 'user') {
|
|
550
|
+
// If there was a previous assistant message, calculate user thinking time
|
|
551
|
+
if (lastAssistantTime) {
|
|
552
|
+
const thinkingTime = new Date(msg.timestamp) - lastAssistantTime;
|
|
553
|
+
// Only count gaps less than 1 hour to avoid counting long breaks
|
|
554
|
+
if (thinkingTime > 0 && thinkingTime < 60 * 60 * 1000) {
|
|
555
|
+
totalUserTime += thinkingTime;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
lastUserTime = new Date(msg.timestamp);
|
|
559
|
+
} else if (msg.role === 'assistant') {
|
|
560
|
+
// If there was a previous user message, calculate Claude execution time
|
|
561
|
+
if (lastUserTime) {
|
|
562
|
+
const executionTime = new Date(msg.timestamp) - lastUserTime;
|
|
563
|
+
// Only count gaps less than 10 minutes (typical execution time)
|
|
564
|
+
if (executionTime > 0 && executionTime < 10 * 60 * 1000) {
|
|
565
|
+
totalWaitTime += executionTime;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
lastAssistantTime = new Date(msg.timestamp);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const totalIterationTime = totalWaitTime + totalUserTime;
|
|
573
|
+
const waitTimePercent = totalIterationTime > 0 ? Math.round((totalWaitTime / totalIterationTime) * 100) : 0;
|
|
574
|
+
const userTimePercent = totalIterationTime > 0 ? Math.round((totalUserTime / totalIterationTime) * 100) : 0;
|
|
575
|
+
|
|
576
|
+
// Calculate cache efficiency
|
|
577
|
+
const cacheTotal = (conversation.tokenUsage?.cacheCreationTokens || 0) + (conversation.tokenUsage?.cacheReadTokens || 0);
|
|
578
|
+
const cacheEfficiency = cacheTotal > 0
|
|
579
|
+
? Math.round((conversation.tokenUsage?.cacheReadTokens || 0) / cacheTotal * 100)
|
|
580
|
+
: 0;
|
|
581
|
+
|
|
582
|
+
// Estimate cost (approximate Claude API pricing)
|
|
583
|
+
// Sonnet 4.5: $3/1M input, $15/1M output
|
|
584
|
+
// Cache write: $3.75/1M, Cache read: $0.30/1M
|
|
585
|
+
const inputCost = (conversation.tokenUsage?.inputTokens || 0) / 1000000 * 3;
|
|
586
|
+
const outputCost = (conversation.tokenUsage?.outputTokens || 0) / 1000000 * 15;
|
|
587
|
+
const cacheWriteCost = (conversation.tokenUsage?.cacheCreationTokens || 0) / 1000000 * 3.75;
|
|
588
|
+
const cacheReadCost = (conversation.tokenUsage?.cacheReadTokens || 0) / 1000000 * 0.30;
|
|
589
|
+
const totalCost = inputCost + outputCost + cacheWriteCost + cacheReadCost;
|
|
590
|
+
|
|
591
|
+
// Detect agents, hooks, and components used
|
|
592
|
+
const agentAnalyzer = new AgentAnalyzer();
|
|
593
|
+
const componentsUsed = {
|
|
594
|
+
agents: [],
|
|
595
|
+
slashCommands: [],
|
|
596
|
+
skills: []
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
messages.forEach(message => {
|
|
600
|
+
const messageContent = message.content;
|
|
601
|
+
const messageRole = message.role;
|
|
602
|
+
|
|
603
|
+
if (messageRole === 'assistant' && messageContent && Array.isArray(messageContent)) {
|
|
604
|
+
messageContent.forEach(content => {
|
|
605
|
+
// Detect Task tool with subagent_type (agents)
|
|
606
|
+
if (content.type === 'tool_use' && content.name === 'Task' && content.input?.subagent_type) {
|
|
607
|
+
const agentType = content.input.subagent_type;
|
|
608
|
+
if (!componentsUsed.agents.find(a => a.type === agentType)) {
|
|
609
|
+
componentsUsed.agents.push({
|
|
610
|
+
type: agentType,
|
|
611
|
+
count: 1
|
|
612
|
+
});
|
|
613
|
+
} else {
|
|
614
|
+
componentsUsed.agents.find(a => a.type === agentType).count++;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Detect SlashCommand tool (commands)
|
|
619
|
+
if (content.type === 'tool_use' && content.name === 'SlashCommand' && content.input?.command) {
|
|
620
|
+
const command = content.input.command;
|
|
621
|
+
if (!componentsUsed.slashCommands.find(c => c.name === command)) {
|
|
622
|
+
componentsUsed.slashCommands.push({
|
|
623
|
+
name: command,
|
|
624
|
+
count: 1
|
|
625
|
+
});
|
|
626
|
+
} else {
|
|
627
|
+
componentsUsed.slashCommands.find(c => c.name === command).count++;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Detect Skill tool (skills)
|
|
632
|
+
if (content.type === 'tool_use' && content.name === 'Skill' && content.input?.command) {
|
|
633
|
+
const skill = content.input.command;
|
|
634
|
+
if (!componentsUsed.skills.find(s => s.name === skill)) {
|
|
635
|
+
componentsUsed.skills.push({
|
|
636
|
+
name: skill,
|
|
637
|
+
count: 1
|
|
638
|
+
});
|
|
639
|
+
} else {
|
|
640
|
+
componentsUsed.skills.find(s => s.name === skill).count++;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Format time durations
|
|
648
|
+
const formatDuration = (ms) => {
|
|
649
|
+
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
650
|
+
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
|
651
|
+
const seconds = Math.floor((ms % (1000 * 60)) / 1000);
|
|
652
|
+
|
|
653
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
654
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
655
|
+
return `${seconds}s`;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Generate optimization tips based on analytics
|
|
659
|
+
const optimizationTips = [];
|
|
660
|
+
|
|
661
|
+
if (cacheEfficiency < 20 && cacheTotal > 0) {
|
|
662
|
+
optimizationTips.push('• Low cache efficiency detected. Consider restructuring prompts to maximize cache reuse.');
|
|
663
|
+
}
|
|
664
|
+
if (conversation.toolUsage?.totalToolCalls > 50) {
|
|
665
|
+
optimizationTips.push('• High tool usage detected. Review if all tool calls are necessary.');
|
|
666
|
+
}
|
|
667
|
+
if (conversation.tokenUsage?.outputTokens > conversation.tokenUsage?.inputTokens * 2) {
|
|
668
|
+
optimizationTips.push('• Output tokens significantly exceed input. Consider more concise prompts.');
|
|
669
|
+
}
|
|
670
|
+
if (messages.length > 100) {
|
|
671
|
+
optimizationTips.push('• Long conversation detected. Consider starting fresh sessions for new topics to optimize context.');
|
|
672
|
+
}
|
|
673
|
+
if (conversation.modelInfo?.hasMultipleModels) {
|
|
674
|
+
optimizationTips.push('• Multiple models used in this session. Stick to one model for consistency.');
|
|
675
|
+
}
|
|
676
|
+
if (waitTimePercent > 70) {
|
|
677
|
+
optimizationTips.push('• High execution time detected. Consider breaking down complex tasks or optimizing tool usage.');
|
|
678
|
+
}
|
|
679
|
+
if (optimizationTips.length === 0) {
|
|
680
|
+
optimizationTips.push('• Great! Your conversation shows efficient usage patterns.');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Prepare detailed analytics response
|
|
684
|
+
const analytics = {
|
|
685
|
+
// Overview
|
|
686
|
+
messageCount: messages.length,
|
|
687
|
+
totalTokens: conversation.tokenUsage?.total || 0,
|
|
688
|
+
toolCalls: conversation.toolUsage?.totalToolCalls || 0,
|
|
689
|
+
cacheEfficiency: `${cacheEfficiency}%`,
|
|
690
|
+
|
|
691
|
+
// Token breakdown
|
|
692
|
+
tokenUsage: {
|
|
693
|
+
inputTokens: conversation.tokenUsage?.inputTokens || 0,
|
|
694
|
+
outputTokens: conversation.tokenUsage?.outputTokens || 0,
|
|
695
|
+
cacheCreationTokens: conversation.tokenUsage?.cacheCreationTokens || 0,
|
|
696
|
+
cacheReadTokens: conversation.tokenUsage?.cacheReadTokens || 0,
|
|
697
|
+
total: conversation.tokenUsage?.total || 0
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
// Cost estimate
|
|
701
|
+
costEstimate: {
|
|
702
|
+
total: totalCost.toFixed(4),
|
|
703
|
+
breakdown: {
|
|
704
|
+
input: inputCost.toFixed(4),
|
|
705
|
+
output: outputCost.toFixed(4),
|
|
706
|
+
cacheWrite: cacheWriteCost.toFixed(4),
|
|
707
|
+
cacheRead: cacheReadCost.toFixed(4)
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
// Model info with usage percentages
|
|
712
|
+
modelInfo: {
|
|
713
|
+
primaryModel: conversation.modelInfo?.primaryModel || 'Unknown',
|
|
714
|
+
serviceTier: conversation.modelInfo?.currentServiceTier || 'Unknown',
|
|
715
|
+
hasMultipleModels: conversation.modelInfo?.hasMultipleModels || false,
|
|
716
|
+
allModels: conversation.modelInfo?.models || [],
|
|
717
|
+
modelUsage: (() => {
|
|
718
|
+
// Calculate model usage percentages
|
|
719
|
+
const modelCounts = {};
|
|
720
|
+
let totalMessages = 0;
|
|
721
|
+
|
|
722
|
+
messages.forEach(msg => {
|
|
723
|
+
if (msg.model && msg.model !== '<synthetic>') {
|
|
724
|
+
modelCounts[msg.model] = (modelCounts[msg.model] || 0) + 1;
|
|
725
|
+
totalMessages++;
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
return Object.entries(modelCounts).map(([model, count]) => ({
|
|
730
|
+
model,
|
|
731
|
+
count,
|
|
732
|
+
percentage: totalMessages > 0 ? ((count / totalMessages) * 100).toFixed(1) : '0.0'
|
|
733
|
+
})).sort((a, b) => b.count - a.count);
|
|
734
|
+
})()
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
// Tool usage
|
|
738
|
+
toolUsage: {
|
|
739
|
+
totalCalls: conversation.toolUsage?.totalToolCalls || 0,
|
|
740
|
+
uniqueTools: conversation.toolUsage?.uniqueTools || 0,
|
|
741
|
+
breakdown: conversation.toolUsage?.toolStats || {},
|
|
742
|
+
timeline: conversation.toolUsage?.toolTimeline || []
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
// Session timeline
|
|
746
|
+
timeline: {
|
|
747
|
+
startTime: startTime ? startTime.toISOString() : null,
|
|
748
|
+
endTime: endTime ? endTime.toISOString() : null,
|
|
749
|
+
duration: durationHours > 0
|
|
750
|
+
? `${durationHours}h ${durationMinutes}m`
|
|
751
|
+
: `${durationMinutes}m`,
|
|
752
|
+
durationMs: durationMs,
|
|
753
|
+
status: conversation.status || 'unknown'
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
// Time breakdown (conversing vs executing)
|
|
757
|
+
timeBreakdown: {
|
|
758
|
+
totalWaitTime: formatDuration(totalWaitTime),
|
|
759
|
+
totalUserTime: formatDuration(totalUserTime),
|
|
760
|
+
waitTimePercent: waitTimePercent,
|
|
761
|
+
userTimePercent: userTimePercent,
|
|
762
|
+
waitTimeMs: totalWaitTime,
|
|
763
|
+
userTimeMs: totalUserTime,
|
|
764
|
+
totalIterationTime: formatDuration(totalIterationTime)
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
// Components used (agents, commands, skills)
|
|
768
|
+
componentsUsed: {
|
|
769
|
+
agents: componentsUsed.agents.sort((a, b) => b.count - a.count),
|
|
770
|
+
slashCommands: componentsUsed.slashCommands.sort((a, b) => b.count - a.count),
|
|
771
|
+
skills: componentsUsed.skills.sort((a, b) => b.count - a.count),
|
|
772
|
+
totalAgents: componentsUsed.agents.length,
|
|
773
|
+
totalCommands: componentsUsed.slashCommands.length,
|
|
774
|
+
totalSkills: componentsUsed.skills.length
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
// Optimization tips
|
|
778
|
+
optimizationTips: optimizationTips,
|
|
779
|
+
|
|
780
|
+
// Metadata
|
|
781
|
+
conversationId: conversationId,
|
|
782
|
+
project: conversation.project || 'Unknown',
|
|
783
|
+
timestamp: new Date().toISOString()
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
res.json({
|
|
787
|
+
success: true,
|
|
788
|
+
analytics: analytics
|
|
789
|
+
});
|
|
790
|
+
} catch (error) {
|
|
791
|
+
console.error('Error fetching conversation analytics:', error);
|
|
792
|
+
res.status(500).json({
|
|
793
|
+
error: 'Failed to fetch analytics',
|
|
794
|
+
message: error.message
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
515
799
|
// Serve the mobile chats page as default
|
|
516
800
|
this.app.get('/', (req, res) => {
|
|
517
801
|
res.sendFile(path.join(__dirname, 'analytics-web', 'chats_mobile.html'));
|
|
@@ -903,6 +1187,12 @@ class ChatsMobile {
|
|
|
903
1187
|
* Stop the server
|
|
904
1188
|
*/
|
|
905
1189
|
async stop() {
|
|
1190
|
+
// Prevent multiple stop calls
|
|
1191
|
+
if (this.isStopped) {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
this.isStopped = true;
|
|
1195
|
+
|
|
906
1196
|
if (this.cloudflaredProcess) {
|
|
907
1197
|
try {
|
|
908
1198
|
this.cloudflaredProcess.kill('SIGTERM');
|
|
@@ -911,26 +1201,28 @@ class ChatsMobile {
|
|
|
911
1201
|
this.log('warn', chalk.yellow('⚠️ Error stopping Cloudflare Tunnel:', error.message));
|
|
912
1202
|
}
|
|
913
1203
|
}
|
|
914
|
-
|
|
1204
|
+
|
|
915
1205
|
if (this.webSocketServer) {
|
|
916
1206
|
try {
|
|
1207
|
+
console.log(chalk.gray('🔌 Closing WebSocket server...'));
|
|
917
1208
|
await this.webSocketServer.close();
|
|
918
|
-
|
|
1209
|
+
console.log(chalk.green('✅ WebSocket server closed'));
|
|
919
1210
|
} catch (error) {
|
|
920
1211
|
this.log('warn', chalk.yellow('⚠️ Error stopping WebSocket server:', error.message));
|
|
921
1212
|
}
|
|
922
1213
|
}
|
|
923
|
-
|
|
1214
|
+
|
|
924
1215
|
if (this.httpServer) {
|
|
925
1216
|
await new Promise((resolve) => {
|
|
926
1217
|
this.httpServer.close(resolve);
|
|
927
1218
|
});
|
|
928
1219
|
}
|
|
929
|
-
|
|
1220
|
+
|
|
930
1221
|
if (this.fileWatcher) {
|
|
1222
|
+
console.log(chalk.gray('🛑 Stopping file watchers...'));
|
|
931
1223
|
await this.fileWatcher.stop();
|
|
932
1224
|
}
|
|
933
|
-
|
|
1225
|
+
|
|
934
1226
|
console.log(chalk.gray('🛑 Chats Mobile server stopped'));
|
|
935
1227
|
}
|
|
936
1228
|
}
|
|
@@ -961,14 +1253,35 @@ async function startChatsMobile(options = {}) {
|
|
|
961
1253
|
}
|
|
962
1254
|
|
|
963
1255
|
console.log(chalk.gray('Press Ctrl+C to stop'));
|
|
964
|
-
|
|
965
|
-
// Handle graceful shutdown
|
|
966
|
-
|
|
1256
|
+
|
|
1257
|
+
// Handle graceful shutdown - remove existing listeners first to prevent duplicates
|
|
1258
|
+
const shutdownHandler = async () => {
|
|
1259
|
+
if (chatsMobile.isShuttingDown) return; // Prevent multiple shutdown attempts
|
|
1260
|
+
chatsMobile.isShuttingDown = true;
|
|
1261
|
+
|
|
967
1262
|
console.log(chalk.yellow('\n🛑 Shutting down...'));
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1263
|
+
|
|
1264
|
+
// Remove this specific handler to prevent it from being called again
|
|
1265
|
+
process.removeListener('SIGINT', shutdownHandler);
|
|
1266
|
+
process.removeListener('SIGTERM', shutdownHandler);
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
await chatsMobile.stop();
|
|
1270
|
+
process.exit(0);
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
console.error(chalk.red('❌ Error during shutdown:'), error);
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
// Remove any existing SIGINT/SIGTERM listeners to prevent duplicates
|
|
1278
|
+
process.removeAllListeners('SIGINT');
|
|
1279
|
+
process.removeAllListeners('SIGTERM');
|
|
1280
|
+
|
|
1281
|
+
// Add the new handler
|
|
1282
|
+
process.on('SIGINT', shutdownHandler);
|
|
1283
|
+
process.on('SIGTERM', shutdownHandler);
|
|
1284
|
+
|
|
972
1285
|
} catch (error) {
|
|
973
1286
|
console.error(chalk.red('❌ Failed to start Chats Mobile:'), error);
|
|
974
1287
|
process.exit(1);
|