vibemon 1.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.
- package/README.md +40 -0
- package/assets/characters/apto.png +0 -0
- package/assets/characters/claw.png +0 -0
- package/assets/characters/clawd.png +0 -0
- package/assets/characters/kiro.png +0 -0
- package/assets/generators/generate-icons.js +86 -0
- package/assets/generators/icon-128.png +0 -0
- package/assets/generators/icon-16.png +0 -0
- package/assets/generators/icon-256.png +0 -0
- package/assets/generators/icon-32.png +0 -0
- package/assets/generators/icon-64.png +0 -0
- package/assets/generators/icon-generator.html +221 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/bin/cli.js +16 -0
- package/index.html +26 -0
- package/main.js +358 -0
- package/modules/http-server.cjs +584 -0
- package/modules/http-utils.cjs +110 -0
- package/modules/multi-window-manager.cjs +927 -0
- package/modules/state-manager.cjs +168 -0
- package/modules/tray-manager.cjs +660 -0
- package/modules/validators.cjs +180 -0
- package/modules/ws-client.cjs +313 -0
- package/package.json +112 -0
- package/preload.js +22 -0
- package/renderer.js +84 -0
- package/shared/config.cjs +64 -0
- package/shared/constants.cjs +8 -0
- package/shared/data/constants.json +86 -0
- package/stats.html +521 -0
- package/styles.css +90 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared configuration for Vibe Monitor (CommonJS)
|
|
3
|
+
* App settings from constants.json
|
|
4
|
+
* Rendering data is in vibemon-engine-standalone.js
|
|
5
|
+
*
|
|
6
|
+
* Constants are in constants.cjs - re-exported here for convenience
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
// Re-export all constants for backward compatibility
|
|
13
|
+
const constants = require('./constants.cjs');
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// WebSocket Configuration (from environment variables)
|
|
17
|
+
// =============================================================================
|
|
18
|
+
const WS_URL = process.env.VIBEMON_WS_URL || 'wss://ws.vibemon.io';
|
|
19
|
+
const WS_TOKEN = process.env.VIBEMON_WS_TOKEN || null;
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Paths
|
|
23
|
+
// =============================================================================
|
|
24
|
+
const STATS_CACHE_PATH = path.join(os.homedir(), '.claude', 'stats-cache.json');
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// State & Character Data (from constants.json)
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
// Directly from constants.json
|
|
31
|
+
const {
|
|
32
|
+
VALID_STATES,
|
|
33
|
+
STATE_COLORS,
|
|
34
|
+
CHARACTER_NAMES,
|
|
35
|
+
CHARACTER_COLORS
|
|
36
|
+
} = constants;
|
|
37
|
+
|
|
38
|
+
// Derived CHARACTER_CONFIG for backward compatibility
|
|
39
|
+
const CHARACTER_CONFIG = Object.fromEntries(
|
|
40
|
+
CHARACTER_NAMES.map(name => [name, {
|
|
41
|
+
name,
|
|
42
|
+
color: CHARACTER_COLORS[name]
|
|
43
|
+
}])
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
// Re-export all constants
|
|
48
|
+
...constants,
|
|
49
|
+
|
|
50
|
+
// Paths
|
|
51
|
+
STATS_CACHE_PATH,
|
|
52
|
+
|
|
53
|
+
// State data (from constants.json)
|
|
54
|
+
VALID_STATES,
|
|
55
|
+
STATE_COLORS,
|
|
56
|
+
|
|
57
|
+
// Character data (from constants.json)
|
|
58
|
+
CHARACTER_CONFIG,
|
|
59
|
+
CHARACTER_NAMES,
|
|
60
|
+
|
|
61
|
+
// WebSocket
|
|
62
|
+
WS_URL,
|
|
63
|
+
WS_TOKEN
|
|
64
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"HTTP_PORT": 19280,
|
|
3
|
+
"MAX_PAYLOAD_SIZE": 10240,
|
|
4
|
+
|
|
5
|
+
"WINDOW_WIDTH": 172,
|
|
6
|
+
"WINDOW_HEIGHT": 348,
|
|
7
|
+
"SNAP_THRESHOLD": 30,
|
|
8
|
+
"SNAP_DEBOUNCE": 150,
|
|
9
|
+
"WINDOW_GAP": 10,
|
|
10
|
+
"MAX_WINDOWS": 5,
|
|
11
|
+
"MAX_PROJECT_LIST": 10,
|
|
12
|
+
|
|
13
|
+
"IDLE_TIMEOUT": 60000,
|
|
14
|
+
"SLEEP_TIMEOUT": 300000,
|
|
15
|
+
"WINDOW_CLOSE_TIMEOUT": 600000,
|
|
16
|
+
|
|
17
|
+
"TRAY_ICON_SIZE": 22,
|
|
18
|
+
|
|
19
|
+
"DEFAULT_CHARACTER": "clawd",
|
|
20
|
+
"CHAR_SIZE": 128,
|
|
21
|
+
"SCALE": 2,
|
|
22
|
+
|
|
23
|
+
"COLOR_EYE": "#000000",
|
|
24
|
+
"COLOR_WHITE": "#FFFFFF",
|
|
25
|
+
|
|
26
|
+
"FLOAT_AMPLITUDE_X": 3,
|
|
27
|
+
"FLOAT_AMPLITUDE_Y": 5,
|
|
28
|
+
"CHAR_X_BASE": 22,
|
|
29
|
+
"CHAR_Y_BASE": 20,
|
|
30
|
+
|
|
31
|
+
"FRAME_INTERVAL": 100,
|
|
32
|
+
"FLOAT_CYCLE_FRAMES": 32,
|
|
33
|
+
"LOADING_DOT_COUNT": 4,
|
|
34
|
+
"THINKING_ANIMATION_SLOWDOWN": 3,
|
|
35
|
+
"BLINK_START_FRAME": 30,
|
|
36
|
+
"BLINK_END_FRAME": 31,
|
|
37
|
+
|
|
38
|
+
"PROJECT_NAME_MAX_LENGTH": 20,
|
|
39
|
+
"PROJECT_NAME_TRUNCATE_AT": 17,
|
|
40
|
+
"MODEL_NAME_MAX_LENGTH": 14,
|
|
41
|
+
"MODEL_NAME_TRUNCATE_AT": 11,
|
|
42
|
+
|
|
43
|
+
"MATRIX_STREAM_DENSITY": 0.7,
|
|
44
|
+
"MATRIX_SPEED_MIN": 1,
|
|
45
|
+
"MATRIX_SPEED_MAX": 6,
|
|
46
|
+
"MATRIX_COLUMN_WIDTH": 4,
|
|
47
|
+
"MATRIX_FLICKER_PERIOD": 3,
|
|
48
|
+
"MATRIX_TAIL_LENGTH_FAST": 8,
|
|
49
|
+
"MATRIX_TAIL_LENGTH_SLOW": 6,
|
|
50
|
+
|
|
51
|
+
"LOCK_MODES": {
|
|
52
|
+
"first-project": "First Project",
|
|
53
|
+
"on-thinking": "On Thinking"
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
"ALWAYS_ON_TOP_MODES": {
|
|
57
|
+
"active-only": "Active Only",
|
|
58
|
+
"all": "All Windows",
|
|
59
|
+
"disabled": "Disabled"
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
"ACTIVE_STATES": ["thinking", "planning", "working", "notification", "packing"],
|
|
63
|
+
|
|
64
|
+
"VALID_STATES": ["start", "idle", "thinking", "planning", "working", "packing", "notification", "sleep", "done"],
|
|
65
|
+
|
|
66
|
+
"CHARACTER_NAMES": ["apto", "clawd", "kiro", "claw"],
|
|
67
|
+
|
|
68
|
+
"CHARACTER_COLORS": {
|
|
69
|
+
"apto": "#797C98",
|
|
70
|
+
"clawd": "#D97757",
|
|
71
|
+
"kiro": "#FFFFFF",
|
|
72
|
+
"claw": "#DD4444"
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"STATE_COLORS": {
|
|
76
|
+
"start": "#00CCCC",
|
|
77
|
+
"idle": "#00AA00",
|
|
78
|
+
"thinking": "#9933FF",
|
|
79
|
+
"planning": "#008888",
|
|
80
|
+
"working": "#0066CC",
|
|
81
|
+
"packing": "#AAAAAA",
|
|
82
|
+
"notification": "#FFCC00",
|
|
83
|
+
"sleep": "#111144",
|
|
84
|
+
"done": "#00AA00"
|
|
85
|
+
}
|
|
86
|
+
}
|
package/stats.html
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Claude Code Stats</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
html, body {
|
|
15
|
+
background: transparent;
|
|
16
|
+
width: 640px;
|
|
17
|
+
height: 475px;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
body {
|
|
22
|
+
font-family: 'Söhne', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
23
|
+
color: #1C1917;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.window {
|
|
27
|
+
width: 640px;
|
|
28
|
+
height: 475px;
|
|
29
|
+
background: #FAF9F6;
|
|
30
|
+
border-radius: 12px;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.title-bar {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: space-between;
|
|
39
|
+
padding: 10px 16px;
|
|
40
|
+
background: #D97706;
|
|
41
|
+
-webkit-app-region: drag;
|
|
42
|
+
cursor: grab;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.title-bar h1 {
|
|
46
|
+
font-size: 0.9rem;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
color: #FFFFFF;
|
|
49
|
+
margin: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.close-btn {
|
|
53
|
+
-webkit-app-region: no-drag;
|
|
54
|
+
width: 20px;
|
|
55
|
+
height: 20px;
|
|
56
|
+
border: none;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
background: rgba(255, 255, 255, 0.3);
|
|
59
|
+
color: #FFFFFF;
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
transition: background 0.2s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.close-btn:hover {
|
|
69
|
+
background: rgba(255, 255, 255, 0.5);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.content {
|
|
73
|
+
padding: 12px 16px;
|
|
74
|
+
height: calc(475px - 40px);
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Summary Cards */
|
|
79
|
+
.summary-grid {
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-columns: repeat(6, 1fr);
|
|
82
|
+
gap: 8px;
|
|
83
|
+
margin-bottom: 12px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.summary-card {
|
|
87
|
+
background: #FFFFFF;
|
|
88
|
+
border: 1px solid #E7E5E4;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
padding: 8px 6px;
|
|
91
|
+
text-align: center;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.summary-card .value {
|
|
95
|
+
font-size: 1.1rem;
|
|
96
|
+
font-weight: 700;
|
|
97
|
+
margin-bottom: 2px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.summary-card .label {
|
|
101
|
+
font-size: 0.6rem;
|
|
102
|
+
color: #78716C;
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
letter-spacing: 0.5px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.summary-card.sessions .value { color: #D97706; }
|
|
108
|
+
.summary-card.messages .value { color: #9333EA; }
|
|
109
|
+
.summary-card.tools .value { color: #059669; }
|
|
110
|
+
.summary-card.tokens .value { color: #DC2626; }
|
|
111
|
+
.summary-card.days .value { color: #2563EB; }
|
|
112
|
+
.summary-card.longest .value { color: #EA580C; }
|
|
113
|
+
|
|
114
|
+
/* Contributions Graph */
|
|
115
|
+
.contributions-section {
|
|
116
|
+
background: #FFFFFF;
|
|
117
|
+
border: 1px solid #E7E5E4;
|
|
118
|
+
border-radius: 8px;
|
|
119
|
+
padding: 12px;
|
|
120
|
+
margin-bottom: 12px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.contributions-section h2 {
|
|
124
|
+
font-size: 0.75rem;
|
|
125
|
+
margin-bottom: 8px;
|
|
126
|
+
color: #44403C;
|
|
127
|
+
font-weight: 600;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.contributions-wrapper {
|
|
131
|
+
display: flex;
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
gap: 6px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.contributions-graph {
|
|
137
|
+
display: flex;
|
|
138
|
+
gap: 2px;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.contrib-week {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
gap: 2px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.contrib-day {
|
|
149
|
+
width: 9px;
|
|
150
|
+
height: 9px;
|
|
151
|
+
border-radius: 2px;
|
|
152
|
+
background: #EBEDF0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.contrib-day.level-1 { background: #FED7AA; }
|
|
156
|
+
.contrib-day.level-2 { background: #FDBA74; }
|
|
157
|
+
.contrib-day.level-3 { background: #F97316; }
|
|
158
|
+
.contrib-day.level-4 { background: #C2410C; }
|
|
159
|
+
|
|
160
|
+
.contrib-legend {
|
|
161
|
+
display: flex;
|
|
162
|
+
align-items: center;
|
|
163
|
+
justify-content: flex-end;
|
|
164
|
+
gap: 4px;
|
|
165
|
+
font-size: 0.65rem;
|
|
166
|
+
color: #78716C;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.contrib-legend span {
|
|
170
|
+
margin: 0 2px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.legend-box {
|
|
174
|
+
width: 9px;
|
|
175
|
+
height: 9px;
|
|
176
|
+
border-radius: 2px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Bottom Row */
|
|
180
|
+
.bottom-row {
|
|
181
|
+
display: grid;
|
|
182
|
+
grid-template-columns: 1fr 1fr;
|
|
183
|
+
gap: 12px;
|
|
184
|
+
height: 180px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.chart-section {
|
|
188
|
+
background: #FFFFFF;
|
|
189
|
+
border: 1px solid #E7E5E4;
|
|
190
|
+
border-radius: 8px;
|
|
191
|
+
padding: 12px;
|
|
192
|
+
display: flex;
|
|
193
|
+
flex-direction: column;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.chart-section h2 {
|
|
197
|
+
font-size: 0.75rem;
|
|
198
|
+
margin-bottom: 8px;
|
|
199
|
+
color: #44403C;
|
|
200
|
+
font-weight: 600;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.chart-container {
|
|
204
|
+
flex: 1;
|
|
205
|
+
position: relative;
|
|
206
|
+
min-height: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Model List */
|
|
210
|
+
.model-list {
|
|
211
|
+
font-size: 0.7rem;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.model-item {
|
|
215
|
+
display: flex;
|
|
216
|
+
justify-content: space-between;
|
|
217
|
+
padding: 6px 0;
|
|
218
|
+
border-bottom: 1px solid #F5F5F4;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.model-item:last-child {
|
|
222
|
+
border-bottom: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.model-name {
|
|
226
|
+
color: #D97706;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.model-tokens {
|
|
231
|
+
color: #78716C;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Hour Line Chart */
|
|
235
|
+
.hour-chart {
|
|
236
|
+
width: 100%;
|
|
237
|
+
height: 100%;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.hour-chart .line {
|
|
241
|
+
fill: none;
|
|
242
|
+
stroke: #D97706;
|
|
243
|
+
stroke-width: 1;
|
|
244
|
+
stroke-linecap: round;
|
|
245
|
+
stroke-linejoin: round;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.hour-chart .area {
|
|
249
|
+
fill: url(#areaGradient);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.hour-chart .axis {
|
|
253
|
+
font-size: 9px;
|
|
254
|
+
fill: #A8A29E;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.hour-chart .grid {
|
|
258
|
+
stroke: #E7E5E4;
|
|
259
|
+
stroke-width: 1;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Loading & Error */
|
|
263
|
+
.loading, .error {
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
justify-content: center;
|
|
267
|
+
height: 100%;
|
|
268
|
+
color: #78716C;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.error { color: #DC2626; }
|
|
272
|
+
</style>
|
|
273
|
+
</head>
|
|
274
|
+
<body>
|
|
275
|
+
<div class="window">
|
|
276
|
+
<div class="title-bar">
|
|
277
|
+
<h1>Claude Stats</h1>
|
|
278
|
+
<button class="close-btn" onclick="window.close()">×</button>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="content">
|
|
281
|
+
<div id="content">
|
|
282
|
+
<div class="loading">Loading...</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<script>
|
|
288
|
+
const COLORS = {
|
|
289
|
+
coral: '#D97706',
|
|
290
|
+
purple: '#9333EA',
|
|
291
|
+
green: '#059669',
|
|
292
|
+
red: '#DC2626',
|
|
293
|
+
blue: '#2563EB'
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const formatNumber = (num) => {
|
|
297
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
|
298
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
|
299
|
+
return num.toLocaleString();
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const formatDuration = (ms) => {
|
|
303
|
+
const hours = Math.floor(ms / 3600000);
|
|
304
|
+
const minutes = Math.floor((ms % 3600000) / 60000);
|
|
305
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
306
|
+
return `${minutes}m`;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const getModelShortName = (model) => {
|
|
310
|
+
if (model.includes('opus')) return 'Opus 4.5';
|
|
311
|
+
if (model.includes('sonnet')) return 'Sonnet 4.5';
|
|
312
|
+
if (model.includes('haiku')) return 'Haiku';
|
|
313
|
+
return model.split('-').slice(1, 3).join(' ');
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
async function loadStats() {
|
|
317
|
+
try {
|
|
318
|
+
const response = await fetch('/stats/data');
|
|
319
|
+
if (!response.ok) throw new Error('Failed to load');
|
|
320
|
+
const data = await response.json();
|
|
321
|
+
renderStats(data);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
document.getElementById('content').innerHTML = `<div class="error">Failed to load stats</div>`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderStats(data) {
|
|
328
|
+
const { dailyActivity, modelUsage, totalSessions, totalMessages, longestSession, firstSessionDate, hourCounts } = data;
|
|
329
|
+
|
|
330
|
+
const totalToolCalls = dailyActivity.reduce((sum, d) => sum + d.toolCallCount, 0);
|
|
331
|
+
const totalTokens = Object.values(modelUsage || {}).reduce((sum, m) => sum + (m.inputTokens || 0) + (m.outputTokens || 0), 0);
|
|
332
|
+
const daysSinceStart = firstSessionDate ? Math.ceil((new Date() - new Date(firstSessionDate)) / 86400000) : 0;
|
|
333
|
+
const activeDays = dailyActivity?.length || 0;
|
|
334
|
+
|
|
335
|
+
document.getElementById('content').innerHTML = `
|
|
336
|
+
<div class="summary-grid">
|
|
337
|
+
<div class="summary-card sessions">
|
|
338
|
+
<div class="value">${formatNumber(totalSessions || 0)}</div>
|
|
339
|
+
<div class="label">Sessions</div>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="summary-card messages">
|
|
342
|
+
<div class="value">${formatNumber(totalMessages || 0)}</div>
|
|
343
|
+
<div class="label">Messages</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="summary-card tools">
|
|
346
|
+
<div class="value">${formatNumber(totalToolCalls)}</div>
|
|
347
|
+
<div class="label">Tools</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="summary-card tokens">
|
|
350
|
+
<div class="value">${formatNumber(totalTokens)}</div>
|
|
351
|
+
<div class="label">Tokens</div>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="summary-card days">
|
|
354
|
+
<div class="value">${activeDays}</div>
|
|
355
|
+
<div class="label">Days</div>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="summary-card longest">
|
|
358
|
+
<div class="value">${longestSession ? formatDuration(longestSession.duration) : '-'}</div>
|
|
359
|
+
<div class="label">Longest</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div class="contributions-section">
|
|
364
|
+
<h2>Activity</h2>
|
|
365
|
+
<div class="contributions-wrapper">
|
|
366
|
+
<div class="contributions-graph" id="contribGraph"></div>
|
|
367
|
+
<div class="contrib-legend">
|
|
368
|
+
<span>Less</span>
|
|
369
|
+
<div class="legend-box" style="background:#EBEDF0"></div>
|
|
370
|
+
<div class="legend-box" style="background:#FED7AA"></div>
|
|
371
|
+
<div class="legend-box" style="background:#FDBA74"></div>
|
|
372
|
+
<div class="legend-box" style="background:#F97316"></div>
|
|
373
|
+
<div class="legend-box" style="background:#C2410C"></div>
|
|
374
|
+
<span>More</span>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div class="bottom-row">
|
|
380
|
+
<div class="chart-section">
|
|
381
|
+
<h2>By Hour</h2>
|
|
382
|
+
<div class="chart-container" id="hourChart"></div>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="chart-section">
|
|
385
|
+
<h2>Models</h2>
|
|
386
|
+
<div class="model-list" id="modelList"></div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
renderContributions(dailyActivity || []);
|
|
392
|
+
renderHourBars(hourCounts || {});
|
|
393
|
+
renderModelList(modelUsage || {});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function renderContributions(dailyActivity) {
|
|
397
|
+
const container = document.getElementById('contribGraph');
|
|
398
|
+
|
|
399
|
+
// Create activity map
|
|
400
|
+
const activityMap = {};
|
|
401
|
+
let maxActivity = 0;
|
|
402
|
+
dailyActivity.forEach(d => {
|
|
403
|
+
activityMap[d.date] = d.messageCount;
|
|
404
|
+
if (d.messageCount > maxActivity) maxActivity = d.messageCount;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Generate last 52 weeks (364 days)
|
|
408
|
+
const today = new Date();
|
|
409
|
+
const weeks = [];
|
|
410
|
+
|
|
411
|
+
// Find the start of the week grid (going back ~52 weeks, starting from Sunday)
|
|
412
|
+
const startDate = new Date(today);
|
|
413
|
+
startDate.setDate(startDate.getDate() - 363);
|
|
414
|
+
// Adjust to previous Sunday
|
|
415
|
+
startDate.setDate(startDate.getDate() - startDate.getDay());
|
|
416
|
+
|
|
417
|
+
let currentDate = new Date(startDate);
|
|
418
|
+
|
|
419
|
+
for (let w = 0; w < 52; w++) {
|
|
420
|
+
const week = [];
|
|
421
|
+
for (let d = 0; d < 7; d++) {
|
|
422
|
+
const dateStr = currentDate.toISOString().split('T')[0];
|
|
423
|
+
const activity = activityMap[dateStr] || 0;
|
|
424
|
+
let level = 0;
|
|
425
|
+
if (activity > 0) {
|
|
426
|
+
const ratio = activity / maxActivity;
|
|
427
|
+
if (ratio > 0.75) level = 4;
|
|
428
|
+
else if (ratio > 0.5) level = 3;
|
|
429
|
+
else if (ratio > 0.25) level = 2;
|
|
430
|
+
else level = 1;
|
|
431
|
+
}
|
|
432
|
+
week.push({ date: dateStr, level, activity });
|
|
433
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
434
|
+
}
|
|
435
|
+
weeks.push(week);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
container.innerHTML = weeks.map(week => `
|
|
439
|
+
<div class="contrib-week">
|
|
440
|
+
${week.map(day => `
|
|
441
|
+
<div class="contrib-day level-${day.level}" title="${day.date}: ${day.activity} messages"></div>
|
|
442
|
+
`).join('')}
|
|
443
|
+
</div>
|
|
444
|
+
`).join('');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderHourBars(hourCounts) {
|
|
448
|
+
const container = document.getElementById('hourChart');
|
|
449
|
+
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
450
|
+
const counts = hours.map(h => hourCounts[h] || 0);
|
|
451
|
+
const maxCount = Math.max(...counts, 1);
|
|
452
|
+
|
|
453
|
+
const width = 280;
|
|
454
|
+
const height = 140;
|
|
455
|
+
const padding = { top: 10, right: 10, bottom: 20, left: 25 };
|
|
456
|
+
const chartW = width - padding.left - padding.right;
|
|
457
|
+
const chartH = height - padding.top - padding.bottom;
|
|
458
|
+
|
|
459
|
+
// Generate points
|
|
460
|
+
const points = hours.map((h, i) => {
|
|
461
|
+
const x = padding.left + (i / 23) * chartW;
|
|
462
|
+
const y = padding.top + chartH - (counts[i] / maxCount) * chartH;
|
|
463
|
+
return { x, y, h, count: counts[i] };
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Create smooth curve path using cubic bezier
|
|
467
|
+
const tension = 0.15;
|
|
468
|
+
const maxY = padding.top + chartH; // bottom limit (y increases downward)
|
|
469
|
+
const clampY = (y) => Math.min(y, maxY);
|
|
470
|
+
let linePath = `M ${points[0].x} ${points[0].y}`;
|
|
471
|
+
for (let i = 1; i < points.length; i++) {
|
|
472
|
+
const p0 = points[i - 2] || points[0];
|
|
473
|
+
const p1 = points[i - 1];
|
|
474
|
+
const p2 = points[i];
|
|
475
|
+
const p3 = points[i + 1] || points[points.length - 1];
|
|
476
|
+
const cp1x = p1.x + (p2.x - p0.x) * tension;
|
|
477
|
+
const cp1y = clampY(p1.y + (p2.y - p0.y) * tension);
|
|
478
|
+
const cp2x = p2.x - (p3.x - p1.x) * tension;
|
|
479
|
+
const cp2y = clampY(p2.y - (p3.y - p1.y) * tension);
|
|
480
|
+
linePath += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.x} ${p2.y}`;
|
|
481
|
+
}
|
|
482
|
+
const areaPath = linePath + ` L ${points[23].x} ${padding.top + chartH} L ${padding.left} ${padding.top + chartH} Z`;
|
|
483
|
+
|
|
484
|
+
container.innerHTML = `
|
|
485
|
+
<svg class="hour-chart" viewBox="0 0 ${width} ${height}">
|
|
486
|
+
<defs>
|
|
487
|
+
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
|
|
488
|
+
<stop offset="0%" stop-color="#D97706" stop-opacity="0.3"/>
|
|
489
|
+
<stop offset="100%" stop-color="#D97706" stop-opacity="0.05"/>
|
|
490
|
+
</linearGradient>
|
|
491
|
+
</defs>
|
|
492
|
+
<path class="area" d="${areaPath}"/>
|
|
493
|
+
<path class="line" d="${linePath}"/>
|
|
494
|
+
${[0, 6, 12, 18].map(h => {
|
|
495
|
+
const x = padding.left + (h / 23) * chartW;
|
|
496
|
+
return `<text class="axis" x="${x}" y="${height - 5}" text-anchor="middle">${h}</text>`;
|
|
497
|
+
}).join('')}
|
|
498
|
+
<text class="axis" x="${width - padding.right}" y="${height - 5}" text-anchor="end">23</text>
|
|
499
|
+
</svg>
|
|
500
|
+
`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function renderModelList(modelUsage) {
|
|
504
|
+
const container = document.getElementById('modelList');
|
|
505
|
+
const entries = Object.entries(modelUsage);
|
|
506
|
+
|
|
507
|
+
container.innerHTML = entries.map(([model, usage]) => {
|
|
508
|
+
const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
|
|
509
|
+
return `
|
|
510
|
+
<div class="model-item">
|
|
511
|
+
<span class="model-name">${getModelShortName(model)}</span>
|
|
512
|
+
<span class="model-tokens">${formatNumber(total)} tokens</span>
|
|
513
|
+
</div>
|
|
514
|
+
`;
|
|
515
|
+
}).join('');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
loadStats();
|
|
519
|
+
</script>
|
|
520
|
+
</body>
|
|
521
|
+
</html>
|