codex-work-receipt 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import { formatDate, formatDuration, formatNumber, formatTime } from "../lib/tim
7
7
  import {
8
8
  buildCompensation,
9
9
  DEFAULT_LOCALE,
10
+ getCustomSummaryNotice,
10
11
  getReceiptCopy,
11
12
  getRollingSummaryNotice,
12
13
  getScopeLabel,
@@ -62,6 +63,160 @@ function formatDateKey(value, locale) {
62
63
  return locale === "en" ? `${match[2]}/${match[3]}/${match[1]}` : `${match[1]}/${match[2]}/${match[3]}`;
63
64
  }
64
65
 
66
+ function formatDecimal(value, locale, maximumFractionDigits = 1) {
67
+ return new Intl.NumberFormat(locale === "en" ? "en-US" : "zh-CN", {
68
+ maximumFractionDigits,
69
+ }).format(Math.max(0, Number(value || 0)));
70
+ }
71
+
72
+ function formatPercent(value, locale) {
73
+ return new Intl.NumberFormat(locale === "en" ? "en-US" : "zh-CN", {
74
+ style: "percent",
75
+ maximumFractionDigits: 1,
76
+ }).format(Math.min(1, Math.max(0, Number(value || 0))));
77
+ }
78
+
79
+ function formatSeconds(milliseconds, locale) {
80
+ return `${formatDecimal(Number(milliseconds || 0) / 1000, locale)}s`;
81
+ }
82
+
83
+ function renderBreakdown(items, { key, labels = {}, unit, emptyLabel, locale }) {
84
+ if (!items.length) return `<p class="structure-empty">${escapeHtml(emptyLabel)}</p>`;
85
+ const maximum = Math.max(1, ...items.map((item) => Number(item.count || 0)));
86
+ return items.slice(0, 6).map((item) => {
87
+ const name = String(item[key] || "");
88
+ const label = labels[name] || name;
89
+ const count = Math.max(0, Number(item.count || 0));
90
+ const width = count ? Math.max(6, Math.round((count / maximum) * 100)) : 0;
91
+ return `
92
+ <div class="structure-row">
93
+ <span class="structure-name" title="${escapeHtml(label)}">${escapeHtml(label)}</span>
94
+ <span class="structure-bar" aria-hidden="true"><i style="width:${width}%"></i></span>
95
+ <strong>${escapeHtml(`${formatDecimal(count, locale, 0)} ${unit}`)}</strong>
96
+ </div>`;
97
+ }).join("");
98
+ }
99
+
100
+ function renderInsights(record, copy, locale) {
101
+ const insights = record.stats.insights || {};
102
+ const perTurn = insights.per_turn || {};
103
+ const firstToken = insights.latency_ms?.first_token || {};
104
+ const turnLatency = insights.latency_ms?.turn || {};
105
+ const activity = Array.from({ length: 24 }, (_, hour) => Number(insights.activity_by_hour?.[hour] || 0));
106
+ const activityPeak = Math.max(0, ...activity);
107
+ const heatmapCells = activity.map((count, hour) => {
108
+ const strength = count && activityPeak ? 16 + Math.round((count / activityPeak) * 84) : 4;
109
+ const aria = copy.insights.heatmapCell
110
+ .replaceAll("{hour}", String(hour).padStart(2, "0"))
111
+ .replaceAll("{count}", formatDecimal(count, locale, 0));
112
+ return `<span class="heatmap-cell" style="--heat:${strength}%" role="img" aria-label="${escapeHtml(aria)}" title="${escapeHtml(aria)}"></span>`;
113
+ }).join("");
114
+ const firstTokenValue = firstToken.sample_count
115
+ ? `${copy.insights.p50} ${formatSeconds(firstToken.p50, locale)}`
116
+ : copy.insights.noSamples;
117
+ const firstTokenDetail = firstToken.sample_count
118
+ ? `${copy.insights.p90} ${formatSeconds(firstToken.p90, locale)}`
119
+ : "—";
120
+ const turnValue = turnLatency.sample_count
121
+ ? `${copy.insights.p50} ${formatSeconds(turnLatency.p50, locale)}`
122
+ : copy.insights.noSamples;
123
+ const turnDetail = turnLatency.sample_count
124
+ ? `${copy.insights.p90} ${formatSeconds(turnLatency.p90, locale)}`
125
+ : "—";
126
+ const modelItems = insights.model_usage?.length
127
+ ? insights.model_usage
128
+ : (record.stats.models || []).map((model) => ({ model, count: 0 }));
129
+ const toolItems = insights.tool_usage || [];
130
+ const modelRows = renderBreakdown(modelItems, {
131
+ key: "model",
132
+ unit: copy.insights.turnsUnit,
133
+ emptyLabel: copy.insights.noSamples,
134
+ locale,
135
+ });
136
+ const toolRows = renderBreakdown(toolItems, {
137
+ key: "category",
138
+ labels: copy.insights.toolCategories,
139
+ unit: copy.insights.callsUnit,
140
+ emptyLabel: copy.insights.noSamples,
141
+ locale,
142
+ });
143
+
144
+ return `
145
+ <section class="insights" aria-label="${escapeHtml(copy.insights.title)}">
146
+ <h2 class="insights-title">${escapeHtml(copy.insights.title)}</h2>
147
+ <div class="insight-metrics">
148
+ <div class="insight-metric">
149
+ <span>${escapeHtml(copy.insights.cacheHit)}</span>
150
+ <strong>${escapeHtml(formatPercent(insights.cache_hit_rate, locale))}</strong>
151
+ <small>${escapeHtml(formatNumber(record.stats.tokens.cached_input_tokens, locale))} / ${escapeHtml(formatNumber(record.stats.tokens.input_tokens, locale))}</small>
152
+ </div>
153
+ <div class="insight-metric">
154
+ <span>${escapeHtml(copy.insights.perTurn)}</span>
155
+ <strong>${escapeHtml(`${formatDecimal(perTurn.total_tokens, locale)} Token`)}</strong>
156
+ <small>${escapeHtml(`${formatDecimal(perTurn.output_tokens, locale)} ${copy.insights.outputTokens} · ${formatDecimal(perTurn.tool_calls, locale)} ${copy.insights.toolCalls}`)}</small>
157
+ </div>
158
+ <div class="insight-metric">
159
+ <span>${escapeHtml(copy.insights.firstTokenLatency)}</span>
160
+ <strong>${escapeHtml(firstTokenValue)}</strong>
161
+ <small>${escapeHtml(firstTokenDetail)}</small>
162
+ </div>
163
+ <div class="insight-metric">
164
+ <span>${escapeHtml(copy.insights.turnLatency)}</span>
165
+ <strong>${escapeHtml(turnValue)}</strong>
166
+ <small>${escapeHtml(turnDetail)}</small>
167
+ </div>
168
+ </div>
169
+ <div class="heatmap-block">
170
+ <h3>${escapeHtml(copy.insights.heatmap)}</h3>
171
+ <div class="heatmap-grid">${heatmapCells}</div>
172
+ <div class="heatmap-axis" aria-hidden="true"><span>00</span><span>06</span><span>12</span><span>18</span><span>23</span></div>
173
+ </div>
174
+ <div class="structure-grid">
175
+ <section class="structure-group">
176
+ <h3>${escapeHtml(copy.insights.models)}</h3>${modelRows}
177
+ </section>
178
+ <section class="structure-group">
179
+ <h3>${escapeHtml(copy.insights.tools)}</h3>${toolRows}
180
+ </section>
181
+ </div>
182
+ </section>`;
183
+ }
184
+
185
+ function renderFeatureCommands(featureCopy, locale) {
186
+ const languageArgument = locale === "en" ? " --lang en" : "";
187
+ const tabs = featureCopy.groups.map((group, groupIndex) => {
188
+ const commands = group.commands.map((item) => {
189
+ const command = `npx codex-work-receipt@latest ${item.args}${languageArgument}`;
190
+ return `
191
+ <li class="feature-command">
192
+ <strong class="feature-command__name">${escapeHtml(item.label)}</strong>
193
+ <div class="feature-command__action">
194
+ <code tabindex="0">${escapeHtml(command)}</code>
195
+ <button class="feature-copy-button" type="button" data-copy-command="${escapeHtml(command)}" aria-label="${escapeHtml(`${featureCopy.copyLabel}: ${item.label}`)}" title="${escapeHtml(featureCopy.copyLabel)}">
196
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>
197
+ <span data-copy-label>${escapeHtml(featureCopy.copyLabel)}</span>
198
+ </button>
199
+ </div>
200
+ </li>`;
201
+ }).join("");
202
+ const tabId = `feature-tab-${group.id}`;
203
+ const panelId = `feature-panel-${group.id}`;
204
+ return {
205
+ tab: `<button id="${tabId}" class="feature-tab" type="button" role="tab" aria-selected="${groupIndex === 0 ? "true" : "false"}" aria-controls="${panelId}" tabindex="${groupIndex === 0 ? "0" : "-1"}" data-feature-tab="${escapeHtml(group.id)}">${escapeHtml(group.title)}<span>${formatNumber(group.commands.length, locale)}</span></button>`,
206
+ panel: `
207
+ <section id="${panelId}" class="feature-panel" role="tabpanel" aria-labelledby="${tabId}" data-feature-panel="${escapeHtml(group.id)}">
208
+ <ul>${commands}
209
+ </ul>
210
+ </section>`,
211
+ };
212
+ });
213
+ return `
214
+ <div class="feature-tabs" data-feature-tabs>
215
+ <div class="feature-tabs__list" role="tablist" aria-label="${escapeHtml(featureCopy.tabAria)}">${tabs.map((item) => item.tab).join("")}</div>
216
+ <div class="feature-tabs__panels">${tabs.map((item) => item.panel).join("")}</div>
217
+ </div>`;
218
+ }
219
+
65
220
  export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUrl = null, transferFile = null }) {
66
221
  const locale = record.locale || DEFAULT_LOCALE;
67
222
  const copy = getReceiptCopy(locale);
@@ -73,11 +228,17 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
73
228
  const displayDate = formatDate(endAt, timezone, locale);
74
229
  const rangeStartDate = record.period.range_start_date || formatDateKey(record.period.start_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
75
230
  const rangeEndDate = record.period.range_end_date || formatDateKey(record.period.end_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
76
- const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope);
231
+ const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope)
232
+ || (record.source.scope === "custom-range" && record.source.range_kind === "calendar-days");
77
233
  const spansMultipleDates = rangeStartDate !== rangeEndDate;
78
- const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours);
234
+ const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours, {
235
+ rangeKind: record.source.range_kind,
236
+ filterKind: record.source.filter_kind,
237
+ });
79
238
  const rollingSummaryNotice = record.source.scope === "last-hours"
80
239
  ? getRollingSummaryNotice(locale, record.source.hours)
240
+ : record.source.scope === "custom-range" && record.source.range_kind === "exact-time"
241
+ ? getCustomSummaryNotice(locale)
81
242
  : null;
82
243
  const dateRange = spansMultipleDates
83
244
  ? `${formatDateKey(rangeStartDate, locale)}—${formatDateKey(rangeEndDate, locale)}`
@@ -114,6 +275,19 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
114
275
  successLabel: copy.downloadSuccess,
115
276
  errorLabel: copy.downloadError,
116
277
  }).replaceAll("<", "\\u003c");
278
+ const featureConfig = JSON.stringify({
279
+ copyLabel: copy.sidebar.features.copyLabel,
280
+ copiedLabel: copy.sidebar.features.copiedLabel,
281
+ copyErrorLabel: copy.sidebar.features.copyErrorLabel,
282
+ copiedStatus: copy.sidebar.features.copiedStatus,
283
+ copyErrorStatus: copy.sidebar.features.copyErrorStatus,
284
+ }).replaceAll("<", "\\u003c");
285
+ const featureCommands = renderFeatureCommands(copy.sidebar.features, locale);
286
+ const featureCommandCount = copy.sidebar.features.groups
287
+ .reduce((total, group) => total + group.commands.length, 0);
288
+ const featureCountLabel = copy.sidebar.features.countLabel
289
+ .replace("{count}", formatNumber(featureCommandCount, locale));
290
+ const insights = renderInsights(record, copy, locale);
117
291
  const rows = [
118
292
  receiptRow(copy.rows.scope, scopeLabel),
119
293
  receiptRow(copy.rows.sessions, formatCount(record.stats.session_count, copy.units.sessions, locale)),
@@ -312,6 +486,251 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
312
486
  font-weight: 600;
313
487
  line-height: 1.4;
314
488
  }
489
+ .sidebar-features {
490
+ --feature-ink: #282620;
491
+ --feature-muted: #756f65;
492
+ --feature-line: #c9c2b6;
493
+ --feature-soft-line: #ded7cc;
494
+ --feature-paper: #f6f2e9;
495
+ --feature-paper-soft: #fbfaf7;
496
+ --feature-command: #ece8df;
497
+ padding: 0;
498
+ overflow: hidden;
499
+ border-color: var(--feature-line);
500
+ background: var(--feature-paper);
501
+ color: var(--feature-ink);
502
+ text-align: left;
503
+ transition: background .15s ease, border-color .15s ease, box-shadow .15s ease;
504
+ }
505
+ .sidebar-features[open] {
506
+ background: var(--feature-paper-soft);
507
+ box-shadow: 0 10px 28px rgba(53, 48, 39, .1);
508
+ }
509
+ .sidebar-features__summary {
510
+ display: flex;
511
+ align-items: center;
512
+ justify-content: space-between;
513
+ gap: 8px;
514
+ padding: 12px;
515
+ cursor: pointer;
516
+ list-style: none;
517
+ user-select: none;
518
+ transition: background .15s ease;
519
+ }
520
+ .sidebar-features__summary::-webkit-details-marker { display: none; }
521
+ .sidebar-features__summary:hover { background: #eee8dc; }
522
+ .sidebar-features__summary .sidebar-card__title {
523
+ margin: 0;
524
+ color: var(--feature-ink);
525
+ }
526
+ .sidebar-features__summary .sidebar-card__title svg { color: var(--feature-muted); }
527
+ .sidebar-features__summary:focus-visible {
528
+ outline: 2px solid var(--feature-ink);
529
+ outline-offset: -3px;
530
+ }
531
+ .sidebar-features__meta {
532
+ display: inline-flex;
533
+ align-items: center;
534
+ gap: 5px;
535
+ flex-shrink: 0;
536
+ }
537
+ .sidebar-features__count {
538
+ color: var(--feature-muted);
539
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
540
+ font-size: 9px;
541
+ white-space: nowrap;
542
+ }
543
+ .sidebar-features__chevron {
544
+ width: 14px;
545
+ height: 14px;
546
+ flex-shrink: 0;
547
+ color: var(--feature-muted);
548
+ transition: transform .18s ease;
549
+ }
550
+ .sidebar-features[open] .sidebar-features__chevron { transform: rotate(180deg); }
551
+ .sidebar-features__body {
552
+ padding: 0 12px 12px;
553
+ border-top: 1px solid var(--feature-soft-line);
554
+ background: var(--feature-paper-soft);
555
+ animation: feature-reveal .15s ease-out;
556
+ }
557
+ .sidebar-features .sidebar-features__description {
558
+ margin: 10px 0 12px;
559
+ color: var(--feature-muted);
560
+ }
561
+ .feature-tabs__list {
562
+ display: flex;
563
+ gap: 5px;
564
+ margin: 0 -2px 12px;
565
+ padding: 2px;
566
+ overflow-x: auto;
567
+ scrollbar-width: thin;
568
+ scrollbar-color: #b8b0a4 transparent;
569
+ }
570
+ .feature-tab {
571
+ display: inline-flex;
572
+ align-items: center;
573
+ justify-content: center;
574
+ gap: 5px;
575
+ min-height: 34px;
576
+ padding: 7px 10px;
577
+ flex: 0 0 auto;
578
+ border: 1px solid var(--feature-line);
579
+ border-radius: 5px;
580
+ background: var(--feature-command);
581
+ color: var(--feature-muted);
582
+ cursor: pointer;
583
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
584
+ font-size: 10px;
585
+ font-weight: 700;
586
+ line-height: 1.2;
587
+ white-space: nowrap;
588
+ }
589
+ .feature-tab span {
590
+ min-width: 17px;
591
+ padding: 1px 4px;
592
+ border-radius: 999px;
593
+ background: rgba(40, 38, 32, .08);
594
+ font-size: 8px;
595
+ text-align: center;
596
+ }
597
+ .feature-tab[aria-selected="true"] {
598
+ border-color: var(--feature-ink);
599
+ background: var(--feature-ink);
600
+ color: #fff;
601
+ }
602
+ .feature-tab[aria-selected="true"] span { background: rgba(255, 255, 255, .18); }
603
+ .feature-tab:focus-visible {
604
+ outline: 2px solid #81786c;
605
+ outline-offset: 2px;
606
+ }
607
+ .feature-tabs[data-ready="true"] .feature-panel[hidden] { display: none; }
608
+ .feature-panel + .feature-panel {
609
+ margin-top: 14px;
610
+ padding-top: 13px;
611
+ border-top: 1px solid var(--feature-soft-line);
612
+ }
613
+ .feature-tabs[data-ready="true"] .feature-panel + .feature-panel {
614
+ margin-top: 0;
615
+ padding-top: 0;
616
+ border-top: 0;
617
+ }
618
+ .feature-panel ul {
619
+ display: grid;
620
+ grid-template-columns: repeat(2, minmax(0, 1fr));
621
+ column-gap: 12px;
622
+ margin: 0;
623
+ padding: 0;
624
+ list-style: none;
625
+ }
626
+ .feature-command {
627
+ min-width: 0;
628
+ margin-top: 10px;
629
+ padding-top: 10px;
630
+ border-top: 1px dashed var(--feature-soft-line);
631
+ }
632
+ .feature-command:nth-child(-n + 2) {
633
+ margin-top: 0;
634
+ padding-top: 0;
635
+ border-top: 0;
636
+ }
637
+ .feature-command__name {
638
+ display: block;
639
+ margin-bottom: 5px;
640
+ color: var(--feature-ink);
641
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
642
+ font-size: 10px;
643
+ font-weight: 600;
644
+ line-height: 1.4;
645
+ }
646
+ .feature-command__action {
647
+ display: grid;
648
+ grid-template-columns: minmax(0, 1fr) auto;
649
+ gap: 5px;
650
+ align-items: stretch;
651
+ }
652
+ .feature-command code {
653
+ display: block;
654
+ min-width: 0;
655
+ padding: 6px;
656
+ overflow-x: auto;
657
+ border: 1px solid #d5cfc4;
658
+ border-radius: 4px;
659
+ background: var(--feature-command);
660
+ color: #34312c;
661
+ font-size: 9px;
662
+ line-height: 1.35;
663
+ white-space: nowrap;
664
+ scrollbar-width: thin;
665
+ scrollbar-color: #b8b0a4 transparent;
666
+ }
667
+ .feature-command code:focus-visible {
668
+ outline: 2px solid #81786c;
669
+ outline-offset: 1px;
670
+ }
671
+ .feature-copy-button {
672
+ display: inline-flex;
673
+ align-items: center;
674
+ justify-content: center;
675
+ gap: 3px;
676
+ min-width: 48px;
677
+ padding: 5px 6px;
678
+ border: 1px solid #bdb5a9;
679
+ border-radius: 4px;
680
+ background: #fff;
681
+ color: #4c4942;
682
+ cursor: pointer;
683
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
684
+ font-size: 9px;
685
+ font-weight: 600;
686
+ }
687
+ .feature-copy-button:hover { background: #e5dfd4; }
688
+ .feature-copy-button:focus-visible {
689
+ outline: 2px solid var(--feature-ink);
690
+ outline-offset: 2px;
691
+ }
692
+ .feature-copy-button svg {
693
+ width: 11px;
694
+ height: 11px;
695
+ flex-shrink: 0;
696
+ }
697
+ .feature-copy-button[data-state="success"] {
698
+ border-color: #78a782;
699
+ background: #eef7ef;
700
+ color: #276036;
701
+ }
702
+ .feature-copy-button[data-state="error"] {
703
+ border-color: #c88c84;
704
+ background: #fff1ef;
705
+ color: #8b3028;
706
+ }
707
+ .feature-copy-status {
708
+ position: absolute;
709
+ width: 1px;
710
+ height: 1px;
711
+ padding: 0;
712
+ margin: -1px;
713
+ overflow: hidden;
714
+ clip: rect(0, 0, 0, 0);
715
+ white-space: nowrap;
716
+ border: 0;
717
+ }
718
+ @keyframes feature-reveal {
719
+ from { opacity: 0; transform: translateY(-3px); }
720
+ to { opacity: 1; transform: translateY(0); }
721
+ }
722
+ @media (min-width: 1021px) {
723
+ .sidebar-features[open] {
724
+ width: 560px;
725
+ margin-left: -380px;
726
+ }
727
+ }
728
+ @media (prefers-reduced-motion: reduce) {
729
+ .sidebar-features,
730
+ .sidebar-features__summary,
731
+ .sidebar-features__chevron { transition: none; }
732
+ .sidebar-features__body { animation: none; }
733
+ }
315
734
  .toolbar {
316
735
  display: flex;
317
736
  width: 100%;
@@ -452,6 +871,110 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
452
871
  padding: 8px 6px;
453
872
  background: var(--accent);
454
873
  }
874
+ .insights-title {
875
+ margin: 0 0 12px;
876
+ font-size: 14px;
877
+ letter-spacing: 0;
878
+ text-align: center;
879
+ }
880
+ .insight-metrics {
881
+ display: grid;
882
+ grid-template-columns: repeat(2, minmax(0, 1fr));
883
+ border-top: 1px solid var(--line);
884
+ border-left: 1px solid var(--line);
885
+ }
886
+ .insight-metric {
887
+ min-width: 0;
888
+ padding: 10px;
889
+ border-right: 1px solid var(--line);
890
+ border-bottom: 1px solid var(--line);
891
+ }
892
+ .insight-metric span,
893
+ .insight-metric strong,
894
+ .insight-metric small { display: block; }
895
+ .insight-metric span {
896
+ color: var(--muted);
897
+ font-size: 10px;
898
+ line-height: 1.35;
899
+ }
900
+ .insight-metric strong {
901
+ margin-top: 5px;
902
+ font-size: 16px;
903
+ line-height: 1.2;
904
+ }
905
+ .insight-metric small {
906
+ margin-top: 4px;
907
+ overflow: hidden;
908
+ color: var(--muted);
909
+ font-size: 9px;
910
+ line-height: 1.35;
911
+ text-overflow: ellipsis;
912
+ white-space: nowrap;
913
+ }
914
+ .heatmap-block,
915
+ .structure-grid {
916
+ margin-top: 16px;
917
+ padding-top: 14px;
918
+ border-top: 1px dashed var(--line);
919
+ }
920
+ .heatmap-block h3,
921
+ .structure-group h3 {
922
+ margin: 0 0 9px;
923
+ color: var(--muted);
924
+ font-size: 10px;
925
+ letter-spacing: 0;
926
+ text-transform: uppercase;
927
+ }
928
+ .heatmap-grid {
929
+ display: grid;
930
+ grid-template-columns: repeat(24, minmax(0, 1fr));
931
+ gap: 2px;
932
+ }
933
+ .heatmap-cell {
934
+ display: block;
935
+ height: 22px;
936
+ background: color-mix(in srgb, var(--ink) var(--heat), transparent);
937
+ }
938
+ .heatmap-axis {
939
+ display: flex;
940
+ justify-content: space-between;
941
+ margin-top: 5px;
942
+ color: var(--muted);
943
+ font-size: 8px;
944
+ }
945
+ .structure-grid {
946
+ display: grid;
947
+ grid-template-columns: repeat(2, minmax(0, 1fr));
948
+ gap: 18px;
949
+ }
950
+ .structure-group { min-width: 0; }
951
+ .structure-row {
952
+ display: grid;
953
+ grid-template-columns: minmax(0, 1fr) minmax(32px, .65fr) auto;
954
+ gap: 6px;
955
+ align-items: center;
956
+ min-width: 0;
957
+ padding: 4px 0;
958
+ font-size: 9px;
959
+ }
960
+ .structure-name {
961
+ min-width: 0;
962
+ overflow: hidden;
963
+ text-overflow: ellipsis;
964
+ white-space: nowrap;
965
+ }
966
+ .structure-bar {
967
+ display: block;
968
+ height: 4px;
969
+ background: color-mix(in srgb, var(--ink) 10%, transparent);
970
+ }
971
+ .structure-bar i {
972
+ display: block;
973
+ height: 100%;
974
+ background: var(--ink);
975
+ }
976
+ .structure-row strong { font-size: 8px; white-space: nowrap; }
977
+ .structure-empty { margin: 0; color: var(--muted); font-size: 9px; }
455
978
  .verdict { text-align: center; }
456
979
  .verdict h2 { margin: 0; font-size: 22px; letter-spacing: .04em; }
457
980
  .review {
@@ -663,6 +1186,7 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
663
1186
  flex: 1 1 160px;
664
1187
  min-width: 0;
665
1188
  }
1189
+ .sidebar-features { flex-basis: 100%; }
666
1190
  }
667
1191
  @media (max-width: 420px) {
668
1192
  .layout { padding-inline: 10px; }
@@ -674,8 +1198,15 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
674
1198
  .meta { grid-template-columns: 1fr; }
675
1199
  .meta div:nth-child(even) { text-align: left; }
676
1200
  .transfer-layout { grid-template-columns: 1fr; }
1201
+ .structure-grid { grid-template-columns: 1fr; }
677
1202
  .sidebar { flex-direction: column; }
678
1203
  .sidebar-card { flex-basis: auto; }
1204
+ .feature-panel ul { grid-template-columns: 1fr; }
1205
+ .feature-command:nth-child(2) {
1206
+ margin-top: 10px;
1207
+ padding-top: 10px;
1208
+ border-top: 1px dashed var(--feature-soft-line);
1209
+ }
679
1210
  }
680
1211
  </style>
681
1212
  </head>
@@ -713,6 +1244,10 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
713
1244
  <section>${rows}</section>
714
1245
  <div class="divider"></div>
715
1246
 
1247
+ ${insights}
1248
+
1249
+ <div class="divider"></div>
1250
+
716
1251
  <section class="verdict">
717
1252
  <h2>${escapeHtml(record.presentation.work_title)}</h2>
718
1253
  <p class="review">${escapeHtml(record.presentation.review)}</p>
@@ -768,12 +1303,29 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
768
1303
  <span class="sidebar-sponsor__name">ModelFlare<br>modelflare.dev</span>
769
1304
  </a>
770
1305
  </div>
1306
+ <details class="sidebar-card sidebar-features" data-feature-details>
1307
+ <summary class="sidebar-features__summary">
1308
+ <span class="sidebar-card__title">
1309
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" x2="20" y1="19" y2="19"></line></svg>
1310
+ ${escapeHtml(copy.sidebar.features.title)}
1311
+ </span>
1312
+ <span class="sidebar-features__meta">
1313
+ <span class="sidebar-features__count">${escapeHtml(featureCountLabel)}</span>
1314
+ <svg class="sidebar-features__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>
1315
+ </span>
1316
+ </summary>
1317
+ <div class="sidebar-features__body">
1318
+ <p class="sidebar-features__description">${escapeHtml(copy.sidebar.features.description)}</p>${featureCommands}
1319
+ <span class="feature-copy-status" data-feature-copy-status role="status" aria-live="polite"></span>
1320
+ </div>
1321
+ </details>
771
1322
  </aside>
772
1323
  </div>
773
1324
  <script>${inlineScript(DOM_TO_IMAGE_SOURCE)}</script>
774
1325
  <script>
775
1326
  const exportConfig = ${exportConfig};
776
1327
  const transferConfig = ${transferConfig};
1328
+ const featureConfig = ${featureConfig};
777
1329
  const themes = new Set(["classic", "diner", "payroll"]);
778
1330
  const buttons = [...document.querySelectorAll("[data-theme-value]")];
779
1331
  function applyTheme(theme) {
@@ -787,6 +1339,113 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
787
1339
  try { savedTheme = localStorage.getItem("codex-work-receipt-theme"); } catch {}
788
1340
  applyTheme(savedTheme || document.documentElement.dataset.theme);
789
1341
 
1342
+ const featureDetails = document.querySelector("[data-feature-details]");
1343
+ if (featureDetails) {
1344
+ try {
1345
+ const savedFeatureState = localStorage.getItem("codex-work-receipt-features-open");
1346
+ if (savedFeatureState === "true") featureDetails.open = true;
1347
+ if (savedFeatureState === "false") featureDetails.open = false;
1348
+ } catch {}
1349
+ featureDetails.addEventListener("toggle", () => {
1350
+ try { localStorage.setItem("codex-work-receipt-features-open", String(featureDetails.open)); } catch {}
1351
+ });
1352
+ }
1353
+
1354
+ const featureTabs = document.querySelector("[data-feature-tabs]");
1355
+ if (featureTabs) {
1356
+ const tabs = [...featureTabs.querySelectorAll("[data-feature-tab]")];
1357
+ const panels = [...featureTabs.querySelectorAll("[data-feature-panel]")];
1358
+ const availableTabs = new Set(tabs.map((tab) => tab.dataset.featureTab));
1359
+ function activateFeatureTab(value, { focus = false } = {}) {
1360
+ const selected = availableTabs.has(value) ? value : tabs[0]?.dataset.featureTab;
1361
+ tabs.forEach((tab) => {
1362
+ const active = tab.dataset.featureTab === selected;
1363
+ tab.setAttribute("aria-selected", String(active));
1364
+ tab.tabIndex = active ? 0 : -1;
1365
+ if (active && focus) {
1366
+ tab.focus();
1367
+ tab.scrollIntoView({ block: "nearest", inline: "nearest" });
1368
+ }
1369
+ });
1370
+ panels.forEach((panel) => { panel.hidden = panel.dataset.featurePanel !== selected; });
1371
+ try { localStorage.setItem("codex-work-receipt-feature-tab", selected); } catch {}
1372
+ }
1373
+ let savedFeatureTab = null;
1374
+ try { savedFeatureTab = localStorage.getItem("codex-work-receipt-feature-tab"); } catch {}
1375
+ featureTabs.dataset.ready = "true";
1376
+ activateFeatureTab(savedFeatureTab || tabs[0]?.dataset.featureTab);
1377
+ tabs.forEach((tab, index) => {
1378
+ tab.addEventListener("click", () => activateFeatureTab(tab.dataset.featureTab));
1379
+ tab.addEventListener("keydown", (event) => {
1380
+ let nextIndex = null;
1381
+ if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
1382
+ if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
1383
+ if (event.key === "Home") nextIndex = 0;
1384
+ if (event.key === "End") nextIndex = tabs.length - 1;
1385
+ if (nextIndex === null) return;
1386
+ event.preventDefault();
1387
+ activateFeatureTab(tabs[nextIndex].dataset.featureTab, { focus: true });
1388
+ });
1389
+ });
1390
+ }
1391
+
1392
+ function fallbackCopyText(value) {
1393
+ const textarea = document.createElement("textarea");
1394
+ textarea.value = value;
1395
+ textarea.setAttribute("readonly", "");
1396
+ Object.assign(textarea.style, {
1397
+ position: "fixed",
1398
+ left: "-10000px",
1399
+ top: "0",
1400
+ opacity: "0",
1401
+ pointerEvents: "none",
1402
+ });
1403
+ document.body.appendChild(textarea);
1404
+ textarea.focus();
1405
+ textarea.select();
1406
+ textarea.setSelectionRange(0, textarea.value.length);
1407
+ const copied = document.execCommand("copy");
1408
+ textarea.remove();
1409
+ if (!copied) throw new Error("Clipboard copy was rejected");
1410
+ }
1411
+
1412
+ async function copyFeatureCommand(value) {
1413
+ if (navigator.clipboard?.writeText) {
1414
+ try {
1415
+ await navigator.clipboard.writeText(value);
1416
+ return;
1417
+ } catch {}
1418
+ }
1419
+ fallbackCopyText(value);
1420
+ }
1421
+
1422
+ const featureCopyStatus = document.querySelector("[data-feature-copy-status]");
1423
+ document.querySelectorAll("[data-copy-command]").forEach((button) => {
1424
+ let resetTimer = null;
1425
+ button.addEventListener("click", async () => {
1426
+ const label = button.querySelector("[data-copy-label]");
1427
+ clearTimeout(resetTimer);
1428
+ button.disabled = true;
1429
+ try {
1430
+ await copyFeatureCommand(button.dataset.copyCommand || "");
1431
+ button.dataset.state = "success";
1432
+ if (label) label.textContent = featureConfig.copiedLabel;
1433
+ if (featureCopyStatus) featureCopyStatus.textContent = featureConfig.copiedStatus;
1434
+ } catch (error) {
1435
+ console.error(error);
1436
+ button.dataset.state = "error";
1437
+ if (label) label.textContent = featureConfig.copyErrorLabel;
1438
+ if (featureCopyStatus) featureCopyStatus.textContent = featureConfig.copyErrorStatus;
1439
+ } finally {
1440
+ button.disabled = false;
1441
+ resetTimer = setTimeout(() => {
1442
+ delete button.dataset.state;
1443
+ if (label) label.textContent = featureConfig.copyLabel;
1444
+ }, 1800);
1445
+ }
1446
+ });
1447
+ });
1448
+
790
1449
  const downloadFileButton = document.getElementById("download-import-file");
791
1450
  const downloadStatus = document.getElementById("download-status");
792
1451
  if (downloadFileButton && downloadStatus) {