cursor-usage-analyzer 0.2.1 → 0.3.1
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/.claude/settings.local.json +12 -6
- package/README.md +14 -0
- package/analyze.js +277 -49
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-35-54_uu_app_aicoding_conv55.txt +49 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-36-35_uu_app_aicoding_conv54.txt +241 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-47-45_uu_app_aicoding_conv56.txt +122 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-56-31_uu_app_aicoding_conv40.txt +80 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-58-09__unmatched__conv108.txt +26 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-59-08_uu_app_aicoding_conv57.txt +306 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-00-49_uu_app_aicoding_conv41.txt +149 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-04-15_uu_app_aicoding_conv58.txt +143 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-06-29_uu_app_aicoding_conv59.txt +119 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-17-49_uu_app_aicoding_conv60.txt +227 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-18-36_uu_app_aicoding_conv70.txt +193 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-26-21_uu_app_aicoding_conv42.txt +111 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-31-34_uu_app_aicoding_conv71.txt +232 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-40-01_uu_app_aicoding_conv72.txt +125 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-49-58_uu_app_aicoding_conv73.txt +64 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-57-27_uu_entitymanage_conv43.txt +157 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_10-02-36_uu_app_aicoding_conv44.txt +294 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_10-48-21_uu_app_aicoding_conv79.txt +181 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-13-29_uu_app_aicoding_conv45.txt +160 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-19-00_uu_app_aicoding_conv46.txt +82 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-21-15_uu_app_aicoding_conv74.txt +103 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-25-21_uu_app_aicoding_conv75.txt +119 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-26-01_uu_app_aicoding_conv47.txt +266 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-31-42_uu_entitymanage_conv48.txt +130 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-33-00_uu_app_aicoding_conv1.txt +260 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-51-10_uu_app_aicoding_conv80.txt +68 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-24-42_cursor_usage_an_conv106.txt +769 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-37-27_uu_app_aicoding_conv2.txt +897 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-48-53__unmatched__conv109.txt +26 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-51-19_uu_app_aicoding_conv3.txt +72 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_13-01-28_uu_app_aicoding_conv4.txt +112 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_13-21-29_uu_app_aicoding_conv5.txt +286 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-14-37_uu_app_aicoding_conv76.txt +765 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-25-53_uu_app_aicoding_conv7.txt +134 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-31-19_uu_app_aicoding_conv8.txt +118 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_15-15-16_uu_app_aicoding_conv9.txt +4644 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_15-20-50_uu_app_aicoding_conv6.txt +945 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-00-41_cursor_usage_an_conv107.txt +85 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-25-01_uu_app_aicoding_conv11.txt +274 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-29-52_uu_app_aicoding_conv10.txt +1603 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-38-00_uu_app_aicoding_conv12.txt +96 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-43-55_uu_app_aicoding_conv13.txt +74 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-47-13_uu_app_aicoding_conv14.txt +172 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-48-38_uu_cloud_univer_conv82.txt +253 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-51-54_uu_app_aicoding_conv16.txt +189 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-51-54_uu_app_aicoding_conv17.txt +57 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-59-13_uu_app_aicoding_conv15.txt +36 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-03-28_uu_app_aicoding_conv18.txt +212 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-05-14_uu_app_aicoding_conv19.txt +87 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-13-17_uu_app_aicoding_conv20.txt +77 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-25-15_uu_app_aicoding_conv21.txt +131 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-31-30_uu_app_aicoding_conv23.txt +108 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-38-46_uu_app_aicoding_conv81.txt +428 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-43-08_uu_app_aicoding_conv24.txt +15297 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-51-39_uu_app_aicoding_conv22.txt +60 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-59-43_uu_app_aicoding_conv25.txt +189 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-03-50_uu_app_aicoding_conv26.txt +120 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-30-45_uu_app_aicoding_conv83.txt +523 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-32-40_uu_app_aicoding_conv27.txt +3941 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-39-32_uu_app_aicoding_conv84.txt +133 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-41-01_uu_app_aicoding_conv28.txt +136 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-56-27_uu_app_aicoding_conv85.txt +211 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-10-56_uu_app_aicoding_conv86.txt +319 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-22-42_uu_app_aicoding_conv87.txt +193 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-27-57_uu_app_aicoding_conv88.txt +272 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-32-27_uu_app_aicoding_conv89.txt +50 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-42-59_uu_app_aicoding_conv90.txt +125 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-47-01_uu_app_aicoding_conv91.txt +102 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-58-26_uu_app_aicoding_conv92.txt +145 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-43-25_uu_app_aicoding_conv93.txt +553 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-56-36_uu_app_aicoding_conv95.txt +195 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-58-23_uu_app_aicoding_conv96.txt +86 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-01-26_uu_app_aicoding_conv94.txt +116 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-03-46_uu_app_aicoding_conv61.txt +1743 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-06-54_uu_app_aicoding_conv97.txt +102 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-07-32_uu_app_aicoding_conv29.txt +9930 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-09-02_uu_app_aicoding_conv98.txt +111 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-11-07_uu_app_aicoding_conv49.txt +170 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-16-16_uu_app_aicoding_conv62.txt +200 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-17-18_uu_app_aicoding_conv31.txt +351 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-26-32_uu_app_aicoding_conv99.txt +219 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-29-18_uu_app_aicoding_conv100.txt +121 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-33-35_uu_app_aicoding_conv30.txt +204 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-38-37_uu_app_aicoding_conv63.txt +251 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-42-10_uu_entitymanage_conv33.txt +163 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-43-41_uu_app_aicoding_conv64.txt +139 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-43-53_uu_app_aicoding_conv101.txt +221 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-44-55_uu_app_aicoding_conv50.txt +156 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-47-10_uu_app_aicoding_conv65.txt +136 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-48-40_uu_app_aicoding_conv51.txt +130 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-49-31_uu_app_aicoding_conv102.txt +153 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-49-44_uu_app_aicoding_conv66.txt +54 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-51-05_uu_app_aicoding_conv67.txt +55 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-51-26_uu_app_aicoding_conv32.txt +6172 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-56-08_uu_app_aicoding_conv103.txt +102 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-59-00_uu_app_aicoding_conv52.txt +244 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-10-16_uu_app_aicoding_conv77.txt +61 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-11-24_uu_app_aicoding_conv68.txt +142 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-12-31_uu_app_aicoding_conv104.txt +66 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-16-03_uu_app_aicoding_conv53.txt +439 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-23-41_uu_entitymanage_conv34.txt +2251 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-25-56_uu_app_aicoding_conv69.txt +169 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-26-54_uu_app_aicoding_conv105.txt +70 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-33-45_uu_entitymanage_conv35.txt +144 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-39-23_uu_app_aicoding_conv37.txt +104 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-45-30_uu_app_aicoding_conv78.txt +187 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-04-38_uu_app_aicoding_conv36.txt +2292 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-08-50_uu_entitymanage_conv38.txt +109 -0
- package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-14-01_uu_entitymanage_conv39.txt +112 -0
- package/cursor-logs-export/report.html +3071 -0
- package/html-template.js +610 -18
- package/package.json +19 -6
- package/.idea/cursor-usage-analyzer.iml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -11
- package/cursor-usage-analyzer-0.1.0.tgz +0 -0
- package/cursor-usage-analyzer-0.2.0.tgz +0 -0
- package/cursor-usage-analyzer-0.2.1.tgz +0 -0
- package/team-usage-events-10287858-2025-12-18.csv +0 -600
package/html-template.js
CHANGED
|
@@ -6,6 +6,7 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
6
6
|
const avgTokens = avg(stats.totalTokens, stats.totalConversations);
|
|
7
7
|
const avgMessages = avg(stats.totalMessages, stats.totalConversations);
|
|
8
8
|
const avgLines = avg(stats.totalLinesAdded + stats.totalLinesRemoved, stats.totalConversations);
|
|
9
|
+
const projectCount = Object.keys(stats.workspaceUsage).length;
|
|
9
10
|
|
|
10
11
|
// Prepare chart data
|
|
11
12
|
const isMultiDay = stats.isMultiDay;
|
|
@@ -17,7 +18,7 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
17
18
|
|
|
18
19
|
const activityTitle = isMultiDay ? 'Activity Over Period' : 'Activity During Day';
|
|
19
20
|
const activityLabels = isMultiDay
|
|
20
|
-
? Object.keys(stats.dailyDistribution).sort()
|
|
21
|
+
? Object.keys(stats.dailyDistribution).sort((a, b) => new Date(a) - new Date(b))
|
|
21
22
|
: Array.from({length: 24}, (_, i) => i + ':00');
|
|
22
23
|
const activityData = isMultiDay
|
|
23
24
|
? activityLabels.map(k => stats.dailyDistribution[k])
|
|
@@ -38,7 +39,7 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
38
39
|
background: #f9fafb;
|
|
39
40
|
color: #111827;
|
|
40
41
|
}
|
|
41
|
-
.container { max-width:
|
|
42
|
+
.container { max-width: 1600px; margin: 0 auto; }
|
|
42
43
|
h1 { color: #2563eb; margin-bottom: 8px; }
|
|
43
44
|
h2 {
|
|
44
45
|
color: #111827;
|
|
@@ -65,10 +66,32 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
65
66
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
.stat { padding: 20px; }
|
|
69
|
-
.stat-value { font-size: 32px; font-weight: 700; color: #2563eb; margin-bottom: 4px; }
|
|
69
|
+
.stat { padding: 20px; overflow: hidden; }
|
|
70
|
+
.stat-value { font-size: 32px; font-weight: 700; color: #2563eb; margin-bottom: 4px; white-space: nowrap; }
|
|
70
71
|
.stat-label { color: #6b7280; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
71
72
|
.stat-sublabel { color: #9ca3af; font-size: 12px; margin-top: 4px; }
|
|
73
|
+
.stat-cost { border-left: 4px solid #dc2626; }
|
|
74
|
+
.stat-cost .stat-value { color: #dc2626; }
|
|
75
|
+
.cost-val { color: #dc2626; font-weight: 700; }
|
|
76
|
+
.cost-val-green { color: #16a34a; font-weight: 700; }
|
|
77
|
+
.cost-val-red { color: #dc2626; font-weight: 700; }
|
|
78
|
+
|
|
79
|
+
.warning-banner {
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: 12px;
|
|
82
|
+
padding: 14px 18px;
|
|
83
|
+
background: #fffbeb;
|
|
84
|
+
border: 1px solid #fde68a;
|
|
85
|
+
border-left: 4px solid #f59e0b;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
margin-bottom: 32px;
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
color: #92400e;
|
|
91
|
+
}
|
|
92
|
+
.warning-banner-icon { font-size: 20px; flex-shrink: 0; line-height: 1.2; }
|
|
93
|
+
.warning-banner-title { font-weight: 700; margin-bottom: 2px; color: #78350f; }
|
|
94
|
+
.warning-banner-text { color: #92400e; }
|
|
72
95
|
|
|
73
96
|
.chart-container { padding: 24px; }
|
|
74
97
|
.chart-title { font-size: 16px; font-weight: 600; color: #111827; margin-bottom: 16px; }
|
|
@@ -81,6 +104,19 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
81
104
|
margin-bottom: 20px;
|
|
82
105
|
}
|
|
83
106
|
.filter-group { display: flex; flex-direction: column; gap: 4px; }
|
|
107
|
+
.filter-clear {
|
|
108
|
+
align-self: end;
|
|
109
|
+
padding: 8px 16px;
|
|
110
|
+
border: 1px solid #e5e7eb;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
background: white;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
color: #6b7280;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
transition: all 0.15s;
|
|
117
|
+
white-space: nowrap;
|
|
118
|
+
}
|
|
119
|
+
.filter-clear:hover { background: #f3f4f6; color: #111827; border-color: #d1d5db; }
|
|
84
120
|
.filter-group label {
|
|
85
121
|
font-size: 11px;
|
|
86
122
|
color: #6b7280;
|
|
@@ -101,32 +137,296 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
101
137
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
102
138
|
}
|
|
103
139
|
|
|
104
|
-
table {
|
|
105
|
-
|
|
140
|
+
.table-outer {
|
|
141
|
+
position: relative;
|
|
142
|
+
}
|
|
143
|
+
.table-wrapper {
|
|
144
|
+
overflow-x: auto;
|
|
145
|
+
-webkit-overflow-scrolling: touch;
|
|
146
|
+
background: white;
|
|
147
|
+
border-radius: 8px;
|
|
148
|
+
border: 1px solid #e5e7eb;
|
|
149
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
150
|
+
}
|
|
151
|
+
.scroll-hint {
|
|
152
|
+
position: absolute;
|
|
153
|
+
right: 0;
|
|
154
|
+
top: 0;
|
|
155
|
+
bottom: 0;
|
|
156
|
+
width: 56px;
|
|
157
|
+
pointer-events: none;
|
|
158
|
+
background: linear-gradient(to right, transparent, rgba(37, 99, 235, 0.18));
|
|
159
|
+
border-radius: 0 8px 8px 0;
|
|
160
|
+
transition: opacity 0.3s;
|
|
161
|
+
z-index: 2;
|
|
162
|
+
}
|
|
163
|
+
.scroll-hint.hidden { opacity: 0; }
|
|
164
|
+
table { width: 100%; border-collapse: collapse; min-width: 900px; }
|
|
165
|
+
table { background: none; border: none; box-shadow: none; border-radius: 0; }
|
|
166
|
+
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #e5e7eb; white-space: nowrap; }
|
|
167
|
+
td.wrap { white-space: normal; min-width: 140px; max-width: 260px; overflow: hidden; text-overflow: ellipsis; }
|
|
106
168
|
th {
|
|
107
169
|
background: #f9fafb;
|
|
108
170
|
font-weight: 600;
|
|
109
|
-
font-size:
|
|
171
|
+
font-size: 12px;
|
|
110
172
|
color: #6b7280;
|
|
111
173
|
text-transform: uppercase;
|
|
112
174
|
letter-spacing: 0.5px;
|
|
113
175
|
cursor: pointer;
|
|
114
176
|
user-select: none;
|
|
115
177
|
position: relative;
|
|
116
|
-
padding-right:
|
|
178
|
+
padding-right: 22px;
|
|
179
|
+
position: sticky;
|
|
180
|
+
top: 0;
|
|
181
|
+
z-index: 1;
|
|
117
182
|
}
|
|
118
183
|
th:hover { background: #f3f4f6; }
|
|
119
|
-
th.sortable::after { content: '⇅'; position: absolute; right:
|
|
184
|
+
th.sortable::after { content: '⇅'; position: absolute; right: 4px; opacity: 0.3; font-size: 12px; }
|
|
120
185
|
th.sortable.asc::after { content: '↑'; opacity: 1; }
|
|
121
186
|
th.sortable.desc::after { content: '↓'; opacity: 1; }
|
|
122
|
-
td { font-size:
|
|
187
|
+
td { font-size: 13px; }
|
|
123
188
|
tr:last-child td { border-bottom: none; }
|
|
124
189
|
tr:hover { background: #f9fafb; }
|
|
125
190
|
small { font-size: 12px; color: #6b7280; }
|
|
191
|
+
|
|
192
|
+
/* View toggle */
|
|
193
|
+
.view-toggle {
|
|
194
|
+
display: inline-flex;
|
|
195
|
+
border: 1px solid #e5e7eb;
|
|
196
|
+
border-radius: 6px;
|
|
197
|
+
overflow: hidden;
|
|
198
|
+
margin-left: 16px;
|
|
199
|
+
vertical-align: middle;
|
|
200
|
+
}
|
|
201
|
+
.view-toggle button {
|
|
202
|
+
padding: 6px 14px;
|
|
203
|
+
border: none;
|
|
204
|
+
background: white;
|
|
205
|
+
font-size: 13px;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
color: #6b7280;
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 5px;
|
|
211
|
+
transition: all 0.15s;
|
|
212
|
+
}
|
|
213
|
+
.view-toggle button:not(:last-child) { border-right: 1px solid #e5e7eb; }
|
|
214
|
+
.view-toggle button.active { background: #2563eb; color: white; }
|
|
215
|
+
.view-toggle button:hover:not(.active) { background: #f3f4f6; }
|
|
216
|
+
|
|
217
|
+
/* Card view */
|
|
218
|
+
#cardsContainer { display: none; flex-direction: column; gap: 8px; }
|
|
219
|
+
#cardsContainer.active { display: flex; }
|
|
220
|
+
.table-outer.active { display: block; }
|
|
221
|
+
.table-outer:not(.active) { display: none; }
|
|
222
|
+
|
|
223
|
+
.conv-card {
|
|
224
|
+
background: white;
|
|
225
|
+
border: 1px solid #e5e7eb;
|
|
226
|
+
border-radius: 8px;
|
|
227
|
+
padding: 14px 18px;
|
|
228
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
229
|
+
transition: border-color 0.15s;
|
|
230
|
+
}
|
|
231
|
+
.conv-card:hover { border-color: #bfdbfe; }
|
|
232
|
+
.conv-card-header {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: baseline;
|
|
235
|
+
gap: 10px;
|
|
236
|
+
margin-bottom: 8px;
|
|
237
|
+
flex-wrap: wrap;
|
|
238
|
+
}
|
|
239
|
+
.conv-card-name {
|
|
240
|
+
font-weight: 600;
|
|
241
|
+
font-size: 14px;
|
|
242
|
+
color: #111827;
|
|
243
|
+
flex-shrink: 1;
|
|
244
|
+
min-width: 0;
|
|
245
|
+
}
|
|
246
|
+
.conv-card-meta {
|
|
247
|
+
font-size: 12px;
|
|
248
|
+
color: #9ca3af;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
}
|
|
251
|
+
.conv-card-badges {
|
|
252
|
+
display: flex;
|
|
253
|
+
gap: 6px;
|
|
254
|
+
flex-wrap: wrap;
|
|
255
|
+
margin-left: auto;
|
|
256
|
+
}
|
|
257
|
+
.conv-badge {
|
|
258
|
+
display: inline-flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
gap: 3px;
|
|
261
|
+
font-size: 11px;
|
|
262
|
+
padding: 2px 8px;
|
|
263
|
+
border-radius: 4px;
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
white-space: nowrap;
|
|
266
|
+
}
|
|
267
|
+
.conv-badge-blue { background: #eff6ff; color: #2563eb; }
|
|
268
|
+
.conv-badge-green { background: #f0fdf4; color: #16a34a; }
|
|
269
|
+
.conv-badge-purple { background: #faf5ff; color: #7c3aed; }
|
|
270
|
+
.conv-badge-amber { background: #fffbeb; color: #d97706; }
|
|
271
|
+
.conv-badge-gray { background: #f3f4f6; color: #6b7280; }
|
|
272
|
+
.conv-badge-red { background: #fef2f2; color: #dc2626; }
|
|
273
|
+
.conv-card-stats {
|
|
274
|
+
display: flex;
|
|
275
|
+
gap: 16px;
|
|
276
|
+
flex-wrap: wrap;
|
|
277
|
+
align-items: center;
|
|
278
|
+
}
|
|
279
|
+
.conv-card-stat {
|
|
280
|
+
font-size: 12px;
|
|
281
|
+
color: #6b7280;
|
|
282
|
+
}
|
|
283
|
+
.conv-card-stat b {
|
|
284
|
+
color: #374151;
|
|
285
|
+
font-weight: 600;
|
|
286
|
+
}
|
|
287
|
+
.conv-card-cost {
|
|
288
|
+
font-size: 13px;
|
|
289
|
+
font-weight: 700;
|
|
290
|
+
margin-left: auto;
|
|
291
|
+
padding: 2px 10px;
|
|
292
|
+
border-radius: 4px;
|
|
293
|
+
}
|
|
294
|
+
.conv-card-cost-low { color: #16a34a; background: #f0fdf4; }
|
|
295
|
+
.conv-card-cost-mid { color: #d97706; background: #fffbeb; }
|
|
296
|
+
.conv-card-cost-high { color: #dc2626; background: #fef2f2; }
|
|
126
297
|
|
|
298
|
+
/* Modal */
|
|
299
|
+
.modal-overlay {
|
|
300
|
+
display: none;
|
|
301
|
+
position: fixed;
|
|
302
|
+
inset: 0;
|
|
303
|
+
background: rgba(0,0,0,0.45);
|
|
304
|
+
z-index: 100;
|
|
305
|
+
align-items: center;
|
|
306
|
+
justify-content: center;
|
|
307
|
+
padding: 24px;
|
|
308
|
+
}
|
|
309
|
+
.modal-overlay.open { display: flex; }
|
|
310
|
+
.modal {
|
|
311
|
+
background: white;
|
|
312
|
+
border-radius: 12px;
|
|
313
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
|
|
314
|
+
width: 100%;
|
|
315
|
+
max-width: 720px;
|
|
316
|
+
max-height: 90vh;
|
|
317
|
+
overflow-y: auto;
|
|
318
|
+
animation: modalIn 0.15s ease-out;
|
|
319
|
+
}
|
|
320
|
+
@keyframes modalIn { from { opacity: 0; transform: scale(0.97) translateY(8px); } }
|
|
321
|
+
.modal-head {
|
|
322
|
+
display: flex;
|
|
323
|
+
align-items: flex-start;
|
|
324
|
+
justify-content: space-between;
|
|
325
|
+
padding: 20px 24px 12px;
|
|
326
|
+
border-bottom: 1px solid #e5e7eb;
|
|
327
|
+
gap: 12px;
|
|
328
|
+
}
|
|
329
|
+
.modal-head h3 { font-size: 17px; font-weight: 700; color: #111827; word-break: break-word; }
|
|
330
|
+
.modal-close {
|
|
331
|
+
border: none;
|
|
332
|
+
background: #f3f4f6;
|
|
333
|
+
color: #6b7280;
|
|
334
|
+
width: 32px;
|
|
335
|
+
height: 32px;
|
|
336
|
+
border-radius: 6px;
|
|
337
|
+
font-size: 18px;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
flex-shrink: 0;
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
justify-content: center;
|
|
343
|
+
transition: background 0.15s;
|
|
344
|
+
}
|
|
345
|
+
.modal-close:hover { background: #e5e7eb; color: #111827; }
|
|
346
|
+
.modal-body { padding: 16px 24px 24px; }
|
|
347
|
+
.modal-meta {
|
|
348
|
+
display: flex;
|
|
349
|
+
gap: 6px;
|
|
350
|
+
flex-wrap: wrap;
|
|
351
|
+
margin-bottom: 16px;
|
|
352
|
+
}
|
|
353
|
+
.modal-grid {
|
|
354
|
+
display: grid;
|
|
355
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
356
|
+
gap: 10px;
|
|
357
|
+
margin-bottom: 18px;
|
|
358
|
+
}
|
|
359
|
+
.modal-stat {
|
|
360
|
+
background: #f9fafb;
|
|
361
|
+
border-radius: 6px;
|
|
362
|
+
padding: 10px 14px;
|
|
363
|
+
}
|
|
364
|
+
.modal-stat-val { font-size: 18px; font-weight: 700; color: #2563eb; }
|
|
365
|
+
.modal-stat-val.cost-val { color: #dc2626; }
|
|
366
|
+
.modal-stat-label { font-size: 11px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.4px; margin-top: 2px; }
|
|
367
|
+
.modal-file-link {
|
|
368
|
+
display: inline-flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: 6px;
|
|
371
|
+
padding: 10px 18px;
|
|
372
|
+
background: #2563eb;
|
|
373
|
+
color: white;
|
|
374
|
+
border-radius: 6px;
|
|
375
|
+
text-decoration: none;
|
|
376
|
+
font-size: 13px;
|
|
377
|
+
font-weight: 600;
|
|
378
|
+
transition: background 0.15s;
|
|
379
|
+
}
|
|
380
|
+
.modal-file-link:hover { background: #1d4ed8; }
|
|
381
|
+
.modal-preview {
|
|
382
|
+
margin-top: 14px;
|
|
383
|
+
padding: 12px 14px;
|
|
384
|
+
background: #f9fafb;
|
|
385
|
+
border-radius: 6px;
|
|
386
|
+
font-size: 13px;
|
|
387
|
+
color: #374151;
|
|
388
|
+
line-height: 1.5;
|
|
389
|
+
border-left: 3px solid #2563eb;
|
|
390
|
+
}
|
|
391
|
+
.modal-preview-label {
|
|
392
|
+
font-size: 11px;
|
|
393
|
+
color: #6b7280;
|
|
394
|
+
text-transform: uppercase;
|
|
395
|
+
letter-spacing: 0.4px;
|
|
396
|
+
margin-bottom: 6px;
|
|
397
|
+
font-weight: 600;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Clickable rows/cards */
|
|
401
|
+
#conversationsBody tr { cursor: pointer; }
|
|
402
|
+
.conv-card { cursor: pointer; }
|
|
403
|
+
|
|
404
|
+
/* Unmatched rows */
|
|
405
|
+
tr.unmatched td { background: #fef2f2; }
|
|
406
|
+
tr.unmatched:hover td { background: #fee2e2; }
|
|
407
|
+
.conv-card.unmatched {
|
|
408
|
+
background: #fef2f2;
|
|
409
|
+
border-color: #fecaca;
|
|
410
|
+
border-left: 3px solid #dc2626;
|
|
411
|
+
}
|
|
412
|
+
.conv-card.unmatched:hover { border-color: #fca5a5; }
|
|
413
|
+
.unmatched-tag {
|
|
414
|
+
display: inline-block;
|
|
415
|
+
font-size: 10px;
|
|
416
|
+
font-weight: 700;
|
|
417
|
+
color: #dc2626;
|
|
418
|
+
background: #fee2e2;
|
|
419
|
+
padding: 1px 6px;
|
|
420
|
+
border-radius: 3px;
|
|
421
|
+
text-transform: uppercase;
|
|
422
|
+
letter-spacing: 0.3px;
|
|
423
|
+
}
|
|
424
|
+
|
|
127
425
|
@media (max-width: 768px) {
|
|
128
426
|
.charts-grid { grid-template-columns: 1fr; }
|
|
129
427
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
428
|
+
.modal { max-width: 100%; margin: 0; border-radius: 8px; }
|
|
429
|
+
.modal-grid { grid-template-columns: repeat(2, 1fr); }
|
|
130
430
|
}
|
|
131
431
|
</style>
|
|
132
432
|
</head>
|
|
@@ -146,16 +446,29 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
146
446
|
<div class="stat-label">Messages</div>
|
|
147
447
|
<div class="stat-sublabel">Ø ${avgMessages} per conversation</div>
|
|
148
448
|
</div>
|
|
449
|
+
<div class="stat">
|
|
450
|
+
<div class="stat-value">${projectCount}</div>
|
|
451
|
+
<div class="stat-label">Projects</div>
|
|
452
|
+
</div>
|
|
149
453
|
<div class="stat">
|
|
150
454
|
<div class="stat-value">${stats.totalTokens.toLocaleString()}</div>
|
|
151
455
|
<div class="stat-label">Context Tokens</div>
|
|
152
456
|
<div class="stat-sublabel">Ø ${avgTokens.toLocaleString()} per conversation</div>
|
|
153
457
|
</div>
|
|
154
458
|
${stats.totalApiCalls > 0 ? `
|
|
459
|
+
<div class="stat stat-cost">
|
|
460
|
+
<div class="stat-value">$${(stats.csvTotals?.total || stats.totalApiTokens.cost).toFixed(2)}</div>
|
|
461
|
+
<div class="stat-label">Total Cost (CSV)</div>
|
|
462
|
+
<div class="stat-sublabel">On-Demand: $${(stats.csvTotals?.onDemand || 0).toFixed(2)} | Included: $${(stats.csvTotals?.included || 0).toFixed(2)}</div>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="stat">
|
|
465
|
+
<div class="stat-value" style="color: #16a34a;">$${stats.totalApiTokens.cost.toFixed(2)}</div>
|
|
466
|
+
<div class="stat-label">Matched Cost</div>
|
|
467
|
+
<div class="stat-sublabel">${stats.totalApiCalls} of ${stats.csvTotals?.callCount || stats.totalApiCalls} calls matched</div>
|
|
468
|
+
</div>
|
|
155
469
|
<div class="stat">
|
|
156
470
|
<div class="stat-value">${stats.totalApiTokens.totalTokens.toLocaleString()}</div>
|
|
157
471
|
<div class="stat-label">API Tokens (Total)</div>
|
|
158
|
-
<div class="stat-sublabel">$${stats.totalApiTokens.cost.toFixed(2)} total cost</div>
|
|
159
472
|
</div>
|
|
160
473
|
<div class="stat">
|
|
161
474
|
<div class="stat-value">${stats.totalApiTokens.inputWithCache.toLocaleString()}</div>
|
|
@@ -170,8 +483,9 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
170
483
|
<div class="stat-label">Output Tokens</div>
|
|
171
484
|
</div>
|
|
172
485
|
<div class="stat">
|
|
173
|
-
<div class="stat-value">${stats.totalApiCalls}</div>
|
|
486
|
+
<div class="stat-value">${stats.csvTotals?.callCount || stats.totalApiCalls}</div>
|
|
174
487
|
<div class="stat-label">API Calls</div>
|
|
488
|
+
<div class="stat-sublabel">${stats.unmatchedApiCalls || 0} unmatched</div>
|
|
175
489
|
</div>
|
|
176
490
|
` : ''}
|
|
177
491
|
<div class="stat">
|
|
@@ -189,6 +503,20 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
189
503
|
</div>
|
|
190
504
|
</div>
|
|
191
505
|
|
|
506
|
+
${(stats.unmatchedApiCalls || 0) > 0 ? `
|
|
507
|
+
<div class="warning-banner">
|
|
508
|
+
<div class="warning-banner-icon">⚠</div>
|
|
509
|
+
<div>
|
|
510
|
+
<div class="warning-banner-title">${stats.unmatchedApiCalls} API calls ($${(stats.unmatchedCost || 0).toFixed(2)}) could not be matched to any conversation</div>
|
|
511
|
+
<div class="warning-banner-text">
|
|
512
|
+
These calls appear in the CSV export but have no matching conversation in the local Cursor database.
|
|
513
|
+
Possible causes: deleted conversations, tab completions / autocomplete, background indexing, or operations from other devices.
|
|
514
|
+
They are shown as <strong>Unmatched</strong> entries (grouped by day) in the conversation list below so the total cost is fully accounted for.
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
` : ''}
|
|
519
|
+
|
|
192
520
|
<h2>Usage Charts</h2>
|
|
193
521
|
|
|
194
522
|
<div class="charts-grid">
|
|
@@ -214,7 +542,13 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
214
542
|
</div>
|
|
215
543
|
</div>
|
|
216
544
|
|
|
217
|
-
<h2
|
|
545
|
+
<h2 style="display: flex; align-items: center;">
|
|
546
|
+
Conversation Details
|
|
547
|
+
<div class="view-toggle">
|
|
548
|
+
<button id="viewTable" class="active" title="Table view">☰ Table</button>
|
|
549
|
+
<button id="viewCards" title="Card view">▦ Cards</button>
|
|
550
|
+
</div>
|
|
551
|
+
</h2>
|
|
218
552
|
|
|
219
553
|
<div class="filters">
|
|
220
554
|
<div class="filter-group">
|
|
@@ -247,8 +581,12 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
247
581
|
<label>Date To</label>
|
|
248
582
|
<input type="date" id="filterDateTo">
|
|
249
583
|
</div>
|
|
584
|
+
<button class="filter-clear" id="filterClear">Clear filters</button>
|
|
250
585
|
</div>
|
|
251
586
|
|
|
587
|
+
<div class="table-outer">
|
|
588
|
+
<div class="scroll-hint" id="scrollHint"></div>
|
|
589
|
+
<div class="table-wrapper" id="tableWrapper">
|
|
252
590
|
<table id="conversationsTable">
|
|
253
591
|
<thead>
|
|
254
592
|
<tr>
|
|
@@ -259,7 +597,10 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
259
597
|
<th class="sortable" data-column="messages" data-type="number">Messages</th>
|
|
260
598
|
<th class="sortable" data-column="tokens" data-type="number">Context Tokens</th>
|
|
261
599
|
${stats.totalApiCalls > 0 ? `
|
|
262
|
-
<th class="sortable" data-column="
|
|
600
|
+
<th class="sortable" data-column="apiInputTokens" data-type="number">Input Tokens</th>
|
|
601
|
+
<th class="sortable" data-column="apiCacheRead" data-type="number">Cache Read</th>
|
|
602
|
+
<th class="sortable" data-column="apiOutputTokens" data-type="number">Output Tokens</th>
|
|
603
|
+
<th class="sortable" data-column="apiTokens" data-type="number">Total Tokens</th>
|
|
263
604
|
<th class="sortable" data-column="apiCost" data-type="number">Cost</th>
|
|
264
605
|
<th class="sortable" data-column="apiCalls" data-type="number">API Calls</th>
|
|
265
606
|
` : ''}
|
|
@@ -269,16 +610,19 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
269
610
|
</thead>
|
|
270
611
|
<tbody id="conversationsBody">
|
|
271
612
|
${stats.conversations.map((c, idx) => `
|
|
272
|
-
<tr data-index="${idx}">
|
|
613
|
+
<tr data-index="${idx}"${c.isUnmatched ? ' class="unmatched"' : ''}>
|
|
273
614
|
<td data-value="${c.timestamp}"><small>${c.datetime}</small></td>
|
|
274
|
-
<td data-value="${c.name}">${c.name}</td>
|
|
615
|
+
<td class="wrap" data-value="${c.name}" title="${c.name}">${c.isUnmatched ? '<span class="unmatched-tag">unmatched</span> ' : ''}${c.name}</td>
|
|
275
616
|
<td data-value="${c.workspace}">${c.workspace}</td>
|
|
276
617
|
<td data-value="${c.model}"><small>${c.model}</small></td>
|
|
277
618
|
<td data-value="${c.messages}">${c.messages}</td>
|
|
278
619
|
<td data-value="${c.tokens}"><small>${c.tokens.toLocaleString()} / ${c.contextLimit.toLocaleString()}</small></td>
|
|
279
620
|
${stats.totalApiCalls > 0 ? `
|
|
621
|
+
<td data-value="${c.apiTokens?.inputWithCache || 0}"><small>${(c.apiTokens?.inputWithCache || 0).toLocaleString()}</small></td>
|
|
622
|
+
<td data-value="${c.apiTokens?.cacheRead || 0}"><small>${(c.apiTokens?.cacheRead || 0).toLocaleString()}</small></td>
|
|
623
|
+
<td data-value="${c.apiTokens?.outputTokens || 0}"><small>${(c.apiTokens?.outputTokens || 0).toLocaleString()}</small></td>
|
|
280
624
|
<td data-value="${c.apiTokens?.totalTokens || 0}"><small>${(c.apiTokens?.totalTokens || 0).toLocaleString()}</small></td>
|
|
281
|
-
<td data-value="${c.apiTokens?.cost || 0}"><small>$${(c.apiTokens?.cost || 0).toFixed(2)}</small></td>
|
|
625
|
+
<td data-value="${c.apiTokens?.cost || 0}"><small class="cost-val">$${(c.apiTokens?.cost || 0).toFixed(2)}</small></td>
|
|
282
626
|
<td data-value="${c.apiCallCount || 0}">${c.apiCallCount || 0}</td>
|
|
283
627
|
` : ''}
|
|
284
628
|
<td data-value="${c.linesChanged}">${c.linesChanged}</td>
|
|
@@ -287,6 +631,20 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
287
631
|
`).join('')}
|
|
288
632
|
</tbody>
|
|
289
633
|
</table>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<div id="cardsContainer"></div>
|
|
638
|
+
</div>
|
|
639
|
+
|
|
640
|
+
<div class="modal-overlay" id="modalOverlay">
|
|
641
|
+
<div class="modal" id="modal">
|
|
642
|
+
<div class="modal-head">
|
|
643
|
+
<h3 id="modalTitle"></h3>
|
|
644
|
+
<button class="modal-close" id="modalClose">×</button>
|
|
645
|
+
</div>
|
|
646
|
+
<div class="modal-body" id="modalBody"></div>
|
|
647
|
+
</div>
|
|
290
648
|
</div>
|
|
291
649
|
|
|
292
650
|
<script>
|
|
@@ -301,7 +659,8 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
301
659
|
modelCounts: Object.values(stats.modelUsage),
|
|
302
660
|
workspaceLabels: Object.keys(stats.workspaceUsage),
|
|
303
661
|
workspaceCounts: Object.values(stats.workspaceUsage),
|
|
304
|
-
isMultiDay
|
|
662
|
+
isMultiDay,
|
|
663
|
+
hasApi: stats.totalApiCalls > 0
|
|
305
664
|
})};
|
|
306
665
|
|
|
307
666
|
// Chart defaults
|
|
@@ -477,6 +836,239 @@ export function generateHTMLReport(stats, dateStr, reportPath) {
|
|
|
477
836
|
const el = document.getElementById(id);
|
|
478
837
|
el.addEventListener(el.tagName === 'SELECT' ? 'change' : 'input', applyFilters);
|
|
479
838
|
});
|
|
839
|
+
|
|
840
|
+
// --- Card view ---
|
|
841
|
+
const fmt = n => (n || 0).toLocaleString();
|
|
842
|
+
const cardsContainer = document.getElementById('cardsContainer');
|
|
843
|
+
|
|
844
|
+
function renderCards() {
|
|
845
|
+
cardsContainer.innerHTML = data.conversations.map((c, idx) => {
|
|
846
|
+
const api = c.apiTokens || {};
|
|
847
|
+
const apiSection = data.hasApi ? \`
|
|
848
|
+
<span class="conv-card-stat">In: <b>\${fmt(api.inputWithCache)}</b></span>
|
|
849
|
+
<span class="conv-card-stat">Cache: <b>\${fmt(api.cacheRead)}</b></span>
|
|
850
|
+
<span class="conv-card-stat">Out: <b>\${fmt(api.outputTokens)}</b></span>
|
|
851
|
+
<span class="conv-card-stat">Total: <b>\${fmt(api.totalTokens)}</b></span>
|
|
852
|
+
<span class="conv-card-stat">Calls: <b>\${c.apiCallCount || 0}</b></span>
|
|
853
|
+
\` : '';
|
|
854
|
+
const costClass = api.cost >= 3 ? 'conv-card-cost-high' : api.cost >= 1 ? 'conv-card-cost-mid' : 'conv-card-cost-low';
|
|
855
|
+
const costBadge = data.hasApi && api.cost > 0
|
|
856
|
+
? \`<span class="conv-card-cost \${costClass}">$\${api.cost.toFixed(2)}</span>\`
|
|
857
|
+
: '';
|
|
858
|
+
return \`<div class="conv-card\${c.isUnmatched ? ' unmatched' : ''}" data-card-index="\${idx}">
|
|
859
|
+
<div class="conv-card-header">
|
|
860
|
+
<span class="conv-card-name">\${c.isUnmatched ? '<span class="unmatched-tag">unmatched</span> ' : ''}\${c.name}</span>
|
|
861
|
+
<span class="conv-card-meta">\${c.datetime}</span>
|
|
862
|
+
<div class="conv-card-badges">
|
|
863
|
+
<span class="conv-badge conv-badge-blue">\${c.workspace}</span>
|
|
864
|
+
<span class="conv-badge conv-badge-purple">\${c.model}</span>
|
|
865
|
+
<span class="conv-badge conv-badge-gray">\${c.messages} msgs</span>
|
|
866
|
+
\${c.linesChanged !== '+0/-0' ? \`<span class="conv-badge conv-badge-green">\${c.linesChanged}</span>\` : ''}
|
|
867
|
+
\${c.files > 0 ? \`<span class="conv-badge conv-badge-amber">\${c.files} files</span>\` : ''}
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
<div class="conv-card-stats">
|
|
871
|
+
<span class="conv-card-stat">Context: <b>\${fmt(c.tokens)}</b> / \${fmt(c.contextLimit)}</span>
|
|
872
|
+
\${apiSection}
|
|
873
|
+
\${costBadge}
|
|
874
|
+
</div>
|
|
875
|
+
</div>\`;
|
|
876
|
+
}).join('');
|
|
877
|
+
}
|
|
878
|
+
renderCards();
|
|
879
|
+
|
|
880
|
+
// Apply filters to cards too
|
|
881
|
+
const origApplyFilters = applyFilters;
|
|
882
|
+
const applyFiltersAll = () => {
|
|
883
|
+
const filters = {
|
|
884
|
+
name: document.getElementById('filterName').value.toLowerCase(),
|
|
885
|
+
workspace: document.getElementById('filterWorkspace').value,
|
|
886
|
+
model: document.getElementById('filterModel').value,
|
|
887
|
+
dateFrom: document.getElementById('filterDateFrom').value,
|
|
888
|
+
dateTo: document.getElementById('filterDateTo').value
|
|
889
|
+
};
|
|
890
|
+
const fromTs = filters.dateFrom ? new Date(filters.dateFrom).getTime() : 0;
|
|
891
|
+
const toTs = filters.dateTo ? new Date(filters.dateTo + 'T23:59:59').getTime() : Infinity;
|
|
892
|
+
|
|
893
|
+
// Filter table rows
|
|
894
|
+
document.querySelectorAll('#conversationsBody tr').forEach(row => {
|
|
895
|
+
const idx = parseInt(row.dataset.index);
|
|
896
|
+
const conv = data.conversations[idx];
|
|
897
|
+
const show = (!filters.name || conv.name.toLowerCase().includes(filters.name)) &&
|
|
898
|
+
(!filters.workspace || conv.workspace === filters.workspace) &&
|
|
899
|
+
(!filters.model || conv.model === filters.model) &&
|
|
900
|
+
(conv.timestamp >= fromTs && conv.timestamp <= toTs);
|
|
901
|
+
row.style.display = show ? '' : 'none';
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// Filter cards
|
|
905
|
+
document.querySelectorAll('.conv-card').forEach(card => {
|
|
906
|
+
const idx = parseInt(card.dataset.cardIndex);
|
|
907
|
+
const conv = data.conversations[idx];
|
|
908
|
+
const show = (!filters.name || conv.name.toLowerCase().includes(filters.name)) &&
|
|
909
|
+
(!filters.workspace || conv.workspace === filters.workspace) &&
|
|
910
|
+
(!filters.model || conv.model === filters.model) &&
|
|
911
|
+
(conv.timestamp >= fromTs && conv.timestamp <= toTs);
|
|
912
|
+
card.style.display = show ? '' : 'none';
|
|
913
|
+
});
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
// Re-bind filters to unified handler
|
|
917
|
+
['filterName', 'filterWorkspace', 'filterModel', 'filterDateFrom', 'filterDateTo'].forEach(id => {
|
|
918
|
+
const el = document.getElementById(id);
|
|
919
|
+
const evt = el.tagName === 'SELECT' ? 'change' : 'input';
|
|
920
|
+
el.removeEventListener(evt, applyFilters);
|
|
921
|
+
el.addEventListener(evt, applyFiltersAll);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Clear all filters
|
|
925
|
+
document.getElementById('filterClear').addEventListener('click', () => {
|
|
926
|
+
document.getElementById('filterName').value = '';
|
|
927
|
+
document.getElementById('filterWorkspace').value = '';
|
|
928
|
+
document.getElementById('filterModel').value = '';
|
|
929
|
+
document.getElementById('filterDateFrom').value = '';
|
|
930
|
+
document.getElementById('filterDateTo').value = '';
|
|
931
|
+
applyFiltersAll();
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// Sorting for cards: re-order card DOM elements after table sort
|
|
935
|
+
const origSortHandler = (th) => {
|
|
936
|
+
const col = th.dataset.column;
|
|
937
|
+
const type = th.dataset.type;
|
|
938
|
+
// After table sort, read the row order and reorder cards to match
|
|
939
|
+
setTimeout(() => {
|
|
940
|
+
const tbody = document.getElementById('conversationsBody');
|
|
941
|
+
const rowOrder = Array.from(tbody.querySelectorAll('tr')).map(r => r.dataset.index);
|
|
942
|
+
const cardParent = document.getElementById('cardsContainer');
|
|
943
|
+
rowOrder.forEach(idx => {
|
|
944
|
+
const card = cardParent.querySelector('[data-card-index="' + idx + '"]');
|
|
945
|
+
if (card) cardParent.appendChild(card);
|
|
946
|
+
});
|
|
947
|
+
}, 0);
|
|
948
|
+
};
|
|
949
|
+
document.querySelectorAll('th.sortable').forEach(th => {
|
|
950
|
+
th.addEventListener('click', () => origSortHandler(th));
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// Scroll hint on table
|
|
954
|
+
const tableWrapper = document.getElementById('tableWrapper');
|
|
955
|
+
const scrollHint = document.getElementById('scrollHint');
|
|
956
|
+
const updateScrollHint = () => {
|
|
957
|
+
const maxScroll = tableWrapper.scrollWidth - tableWrapper.clientWidth;
|
|
958
|
+
if (maxScroll <= 2) {
|
|
959
|
+
scrollHint.classList.add('hidden');
|
|
960
|
+
} else {
|
|
961
|
+
scrollHint.classList.toggle('hidden', tableWrapper.scrollLeft >= maxScroll - 4);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
tableWrapper.addEventListener('scroll', updateScrollHint, { passive: true });
|
|
965
|
+
window.addEventListener('resize', updateScrollHint);
|
|
966
|
+
// Run after layout is fully computed
|
|
967
|
+
updateScrollHint();
|
|
968
|
+
requestAnimationFrame(updateScrollHint);
|
|
969
|
+
window.addEventListener('load', updateScrollHint);
|
|
970
|
+
|
|
971
|
+
// View toggle
|
|
972
|
+
const btnTable = document.getElementById('viewTable');
|
|
973
|
+
const btnCards = document.getElementById('viewCards');
|
|
974
|
+
tableWrapper.classList.add('active');
|
|
975
|
+
|
|
976
|
+
const tableOuter = document.querySelector('.table-outer');
|
|
977
|
+
tableOuter.classList.add('active');
|
|
978
|
+
|
|
979
|
+
btnTable.addEventListener('click', () => {
|
|
980
|
+
btnTable.classList.add('active');
|
|
981
|
+
btnCards.classList.remove('active');
|
|
982
|
+
tableOuter.classList.add('active');
|
|
983
|
+
cardsContainer.classList.remove('active');
|
|
984
|
+
updateScrollHint();
|
|
985
|
+
});
|
|
986
|
+
btnCards.addEventListener('click', () => {
|
|
987
|
+
btnCards.classList.add('active');
|
|
988
|
+
btnTable.classList.remove('active');
|
|
989
|
+
cardsContainer.classList.add('active');
|
|
990
|
+
tableOuter.classList.remove('active');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// --- Modal ---
|
|
994
|
+
const overlay = document.getElementById('modalOverlay');
|
|
995
|
+
const modalTitle = document.getElementById('modalTitle');
|
|
996
|
+
const modalBody = document.getElementById('modalBody');
|
|
997
|
+
|
|
998
|
+
function openModal(idx) {
|
|
999
|
+
const c = data.conversations[idx];
|
|
1000
|
+
if (!c) return;
|
|
1001
|
+
const api = c.apiTokens || {};
|
|
1002
|
+
|
|
1003
|
+
modalTitle.textContent = c.name;
|
|
1004
|
+
|
|
1005
|
+
let apiStats = '';
|
|
1006
|
+
if (data.hasApi) {
|
|
1007
|
+
apiStats = \`
|
|
1008
|
+
<div class="modal-stat"><div class="modal-stat-val">\${fmt(api.inputWithCache)}</div><div class="modal-stat-label">Input Tokens</div></div>
|
|
1009
|
+
<div class="modal-stat"><div class="modal-stat-val">\${fmt(api.cacheRead)}</div><div class="modal-stat-label">Cache Read</div></div>
|
|
1010
|
+
<div class="modal-stat"><div class="modal-stat-val">\${fmt(api.outputTokens)}</div><div class="modal-stat-label">Output Tokens</div></div>
|
|
1011
|
+
<div class="modal-stat"><div class="modal-stat-val">\${fmt(api.totalTokens)}</div><div class="modal-stat-label">Total API Tokens</div></div>
|
|
1012
|
+
<div class="modal-stat"><div class="modal-stat-val cost-val">$\${(api.cost || 0).toFixed(2)}</div><div class="modal-stat-label">Cost</div></div>
|
|
1013
|
+
<div class="modal-stat"><div class="modal-stat-val">\${c.apiCallCount || 0}</div><div class="modal-stat-label">API Calls</div></div>
|
|
1014
|
+
\`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const fileLink = c.exportedFile
|
|
1018
|
+
? \`<a class="modal-file-link" href="chats/\${c.exportedFile}" target="_blank">📄 Open conversation log</a>\`
|
|
1019
|
+
: '';
|
|
1020
|
+
|
|
1021
|
+
modalBody.innerHTML = \`
|
|
1022
|
+
<div class="modal-meta">
|
|
1023
|
+
<span class="conv-badge conv-badge-gray">\${c.datetime}</span>
|
|
1024
|
+
<span class="conv-badge conv-badge-blue">\${c.workspace}</span>
|
|
1025
|
+
<span class="conv-badge conv-badge-purple">\${c.model}</span>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div class="modal-grid">
|
|
1028
|
+
<div class="modal-stat"><div class="modal-stat-val">\${c.messages}</div><div class="modal-stat-label">Messages</div></div>
|
|
1029
|
+
<div class="modal-stat"><div class="modal-stat-val">\${fmt(c.tokens)}</div><div class="modal-stat-label">Context Tokens</div></div>
|
|
1030
|
+
<div class="modal-stat"><div class="modal-stat-val">\${c.linesChanged}</div><div class="modal-stat-label">Lines Changed</div></div>
|
|
1031
|
+
<div class="modal-stat"><div class="modal-stat-val">\${c.files}</div><div class="modal-stat-label">Files</div></div>
|
|
1032
|
+
\${apiStats}
|
|
1033
|
+
</div>
|
|
1034
|
+
\${fileLink}
|
|
1035
|
+
<div class="modal-preview">
|
|
1036
|
+
<div class="modal-preview-label">First message</div>
|
|
1037
|
+
\${c.preview}
|
|
1038
|
+
</div>
|
|
1039
|
+
\`;
|
|
1040
|
+
|
|
1041
|
+
overlay.classList.add('open');
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function closeModal() { overlay.classList.remove('open'); }
|
|
1045
|
+
|
|
1046
|
+
document.getElementById('modalClose').addEventListener('click', closeModal);
|
|
1047
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });
|
|
1048
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
|
|
1049
|
+
|
|
1050
|
+
// Click on table rows
|
|
1051
|
+
document.getElementById('conversationsBody').addEventListener('click', (e) => {
|
|
1052
|
+
const row = e.target.closest('tr');
|
|
1053
|
+
if (row && row.dataset.index != null) openModal(parseInt(row.dataset.index));
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// Click on cards
|
|
1057
|
+
cardsContainer.addEventListener('click', (e) => {
|
|
1058
|
+
const card = e.target.closest('.conv-card');
|
|
1059
|
+
if (card && card.dataset.cardIndex != null) openModal(parseInt(card.dataset.cardIndex));
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Auto-shrink stat values that overflow their container
|
|
1063
|
+
document.querySelectorAll('.stat-value').forEach(el => {
|
|
1064
|
+
const parent = el.closest('.stat');
|
|
1065
|
+
if (!parent) return;
|
|
1066
|
+
let fontSize = 32;
|
|
1067
|
+
while (el.scrollWidth > parent.clientWidth - 40 && fontSize > 16) {
|
|
1068
|
+
fontSize -= 2;
|
|
1069
|
+
el.style.fontSize = fontSize + 'px';
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
480
1072
|
</script>
|
|
481
1073
|
</body>
|
|
482
1074
|
</html>`;
|