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.
Files changed (122) hide show
  1. package/.claude/settings.local.json +12 -6
  2. package/README.md +14 -0
  3. package/analyze.js +277 -49
  4. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-35-54_uu_app_aicoding_conv55.txt +49 -0
  5. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-36-35_uu_app_aicoding_conv54.txt +241 -0
  6. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-47-45_uu_app_aicoding_conv56.txt +122 -0
  7. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-56-31_uu_app_aicoding_conv40.txt +80 -0
  8. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-58-09__unmatched__conv108.txt +26 -0
  9. package/cursor-logs-export/chats/2026-02-05_2026-02-10_08-59-08_uu_app_aicoding_conv57.txt +306 -0
  10. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-00-49_uu_app_aicoding_conv41.txt +149 -0
  11. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-04-15_uu_app_aicoding_conv58.txt +143 -0
  12. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-06-29_uu_app_aicoding_conv59.txt +119 -0
  13. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-17-49_uu_app_aicoding_conv60.txt +227 -0
  14. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-18-36_uu_app_aicoding_conv70.txt +193 -0
  15. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-26-21_uu_app_aicoding_conv42.txt +111 -0
  16. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-31-34_uu_app_aicoding_conv71.txt +232 -0
  17. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-40-01_uu_app_aicoding_conv72.txt +125 -0
  18. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-49-58_uu_app_aicoding_conv73.txt +64 -0
  19. package/cursor-logs-export/chats/2026-02-05_2026-02-10_09-57-27_uu_entitymanage_conv43.txt +157 -0
  20. package/cursor-logs-export/chats/2026-02-05_2026-02-10_10-02-36_uu_app_aicoding_conv44.txt +294 -0
  21. package/cursor-logs-export/chats/2026-02-05_2026-02-10_10-48-21_uu_app_aicoding_conv79.txt +181 -0
  22. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-13-29_uu_app_aicoding_conv45.txt +160 -0
  23. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-19-00_uu_app_aicoding_conv46.txt +82 -0
  24. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-21-15_uu_app_aicoding_conv74.txt +103 -0
  25. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-25-21_uu_app_aicoding_conv75.txt +119 -0
  26. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-26-01_uu_app_aicoding_conv47.txt +266 -0
  27. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-31-42_uu_entitymanage_conv48.txt +130 -0
  28. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-33-00_uu_app_aicoding_conv1.txt +260 -0
  29. package/cursor-logs-export/chats/2026-02-05_2026-02-10_11-51-10_uu_app_aicoding_conv80.txt +68 -0
  30. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-24-42_cursor_usage_an_conv106.txt +769 -0
  31. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-37-27_uu_app_aicoding_conv2.txt +897 -0
  32. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-48-53__unmatched__conv109.txt +26 -0
  33. package/cursor-logs-export/chats/2026-02-05_2026-02-10_12-51-19_uu_app_aicoding_conv3.txt +72 -0
  34. package/cursor-logs-export/chats/2026-02-05_2026-02-10_13-01-28_uu_app_aicoding_conv4.txt +112 -0
  35. package/cursor-logs-export/chats/2026-02-05_2026-02-10_13-21-29_uu_app_aicoding_conv5.txt +286 -0
  36. package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-14-37_uu_app_aicoding_conv76.txt +765 -0
  37. package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-25-53_uu_app_aicoding_conv7.txt +134 -0
  38. package/cursor-logs-export/chats/2026-02-05_2026-02-10_14-31-19_uu_app_aicoding_conv8.txt +118 -0
  39. package/cursor-logs-export/chats/2026-02-05_2026-02-10_15-15-16_uu_app_aicoding_conv9.txt +4644 -0
  40. package/cursor-logs-export/chats/2026-02-05_2026-02-10_15-20-50_uu_app_aicoding_conv6.txt +945 -0
  41. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-00-41_cursor_usage_an_conv107.txt +85 -0
  42. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-25-01_uu_app_aicoding_conv11.txt +274 -0
  43. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-29-52_uu_app_aicoding_conv10.txt +1603 -0
  44. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-38-00_uu_app_aicoding_conv12.txt +96 -0
  45. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-43-55_uu_app_aicoding_conv13.txt +74 -0
  46. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-47-13_uu_app_aicoding_conv14.txt +172 -0
  47. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-48-38_uu_cloud_univer_conv82.txt +253 -0
  48. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-51-54_uu_app_aicoding_conv16.txt +189 -0
  49. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-51-54_uu_app_aicoding_conv17.txt +57 -0
  50. package/cursor-logs-export/chats/2026-02-05_2026-02-10_16-59-13_uu_app_aicoding_conv15.txt +36 -0
  51. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-03-28_uu_app_aicoding_conv18.txt +212 -0
  52. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-05-14_uu_app_aicoding_conv19.txt +87 -0
  53. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-13-17_uu_app_aicoding_conv20.txt +77 -0
  54. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-25-15_uu_app_aicoding_conv21.txt +131 -0
  55. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-31-30_uu_app_aicoding_conv23.txt +108 -0
  56. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-38-46_uu_app_aicoding_conv81.txt +428 -0
  57. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-43-08_uu_app_aicoding_conv24.txt +15297 -0
  58. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-51-39_uu_app_aicoding_conv22.txt +60 -0
  59. package/cursor-logs-export/chats/2026-02-05_2026-02-10_17-59-43_uu_app_aicoding_conv25.txt +189 -0
  60. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-03-50_uu_app_aicoding_conv26.txt +120 -0
  61. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-30-45_uu_app_aicoding_conv83.txt +523 -0
  62. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-32-40_uu_app_aicoding_conv27.txt +3941 -0
  63. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-39-32_uu_app_aicoding_conv84.txt +133 -0
  64. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-41-01_uu_app_aicoding_conv28.txt +136 -0
  65. package/cursor-logs-export/chats/2026-02-05_2026-02-10_18-56-27_uu_app_aicoding_conv85.txt +211 -0
  66. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-10-56_uu_app_aicoding_conv86.txt +319 -0
  67. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-22-42_uu_app_aicoding_conv87.txt +193 -0
  68. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-27-57_uu_app_aicoding_conv88.txt +272 -0
  69. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-32-27_uu_app_aicoding_conv89.txt +50 -0
  70. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-42-59_uu_app_aicoding_conv90.txt +125 -0
  71. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-47-01_uu_app_aicoding_conv91.txt +102 -0
  72. package/cursor-logs-export/chats/2026-02-05_2026-02-10_19-58-26_uu_app_aicoding_conv92.txt +145 -0
  73. package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-43-25_uu_app_aicoding_conv93.txt +553 -0
  74. package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-56-36_uu_app_aicoding_conv95.txt +195 -0
  75. package/cursor-logs-export/chats/2026-02-05_2026-02-10_20-58-23_uu_app_aicoding_conv96.txt +86 -0
  76. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-01-26_uu_app_aicoding_conv94.txt +116 -0
  77. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-03-46_uu_app_aicoding_conv61.txt +1743 -0
  78. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-06-54_uu_app_aicoding_conv97.txt +102 -0
  79. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-07-32_uu_app_aicoding_conv29.txt +9930 -0
  80. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-09-02_uu_app_aicoding_conv98.txt +111 -0
  81. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-11-07_uu_app_aicoding_conv49.txt +170 -0
  82. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-16-16_uu_app_aicoding_conv62.txt +200 -0
  83. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-17-18_uu_app_aicoding_conv31.txt +351 -0
  84. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-26-32_uu_app_aicoding_conv99.txt +219 -0
  85. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-29-18_uu_app_aicoding_conv100.txt +121 -0
  86. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-33-35_uu_app_aicoding_conv30.txt +204 -0
  87. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-38-37_uu_app_aicoding_conv63.txt +251 -0
  88. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-42-10_uu_entitymanage_conv33.txt +163 -0
  89. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-43-41_uu_app_aicoding_conv64.txt +139 -0
  90. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-43-53_uu_app_aicoding_conv101.txt +221 -0
  91. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-44-55_uu_app_aicoding_conv50.txt +156 -0
  92. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-47-10_uu_app_aicoding_conv65.txt +136 -0
  93. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-48-40_uu_app_aicoding_conv51.txt +130 -0
  94. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-49-31_uu_app_aicoding_conv102.txt +153 -0
  95. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-49-44_uu_app_aicoding_conv66.txt +54 -0
  96. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-51-05_uu_app_aicoding_conv67.txt +55 -0
  97. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-51-26_uu_app_aicoding_conv32.txt +6172 -0
  98. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-56-08_uu_app_aicoding_conv103.txt +102 -0
  99. package/cursor-logs-export/chats/2026-02-05_2026-02-10_21-59-00_uu_app_aicoding_conv52.txt +244 -0
  100. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-10-16_uu_app_aicoding_conv77.txt +61 -0
  101. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-11-24_uu_app_aicoding_conv68.txt +142 -0
  102. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-12-31_uu_app_aicoding_conv104.txt +66 -0
  103. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-16-03_uu_app_aicoding_conv53.txt +439 -0
  104. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-23-41_uu_entitymanage_conv34.txt +2251 -0
  105. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-25-56_uu_app_aicoding_conv69.txt +169 -0
  106. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-26-54_uu_app_aicoding_conv105.txt +70 -0
  107. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-33-45_uu_entitymanage_conv35.txt +144 -0
  108. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-39-23_uu_app_aicoding_conv37.txt +104 -0
  109. package/cursor-logs-export/chats/2026-02-05_2026-02-10_22-45-30_uu_app_aicoding_conv78.txt +187 -0
  110. package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-04-38_uu_app_aicoding_conv36.txt +2292 -0
  111. package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-08-50_uu_entitymanage_conv38.txt +109 -0
  112. package/cursor-logs-export/chats/2026-02-05_2026-02-10_23-14-01_uu_entitymanage_conv39.txt +112 -0
  113. package/cursor-logs-export/report.html +3071 -0
  114. package/html-template.js +610 -18
  115. package/package.json +19 -6
  116. package/.idea/cursor-usage-analyzer.iml +0 -12
  117. package/.idea/modules.xml +0 -8
  118. package/.idea/vcs.xml +0 -11
  119. package/cursor-usage-analyzer-0.1.0.tgz +0 -0
  120. package/cursor-usage-analyzer-0.2.0.tgz +0 -0
  121. package/cursor-usage-analyzer-0.2.1.tgz +0 -0
  122. 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: 1400px; margin: 0 auto; }
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 { width: 100%; border-collapse: collapse; overflow: hidden; }
105
- th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e7eb; }
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: 13px;
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: 28px;
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: 8px; opacity: 0.3; font-size: 14px; }
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: 14px; }
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">&#9888;</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>Conversation Details</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">&#9776; Table</button>
549
+ <button id="viewCards" title="Card view">&#9638; 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="apiTokens" data-type="number">API Tokens</th>
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">&times;</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">&#128196; 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>`;