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.
- package/CHANGELOG.md +19 -0
- package/README.en.md +14 -4
- package/README.md +14 -4
- package/docs/cli.en.md +40 -2
- package/docs/cli.md +40 -2
- package/docs/codex-skill.en.md +1 -1
- package/docs/codex-skill.md +1 -1
- package/docs/data-schema.en.md +20 -4
- package/docs/data-schema.md +20 -4
- package/docs/privacy.en.md +8 -3
- package/docs/privacy.md +8 -3
- package/package.json +2 -2
- package/skills/ai-work-receipt/SKILL.md +10 -5
- package/src/auto-runner.mjs +4 -4
- package/src/cli.mjs +64 -5
- package/src/core/args.mjs +59 -3
- package/src/core/fact-buckets.mjs +3 -3
- package/src/core/fact-identity.mjs +8 -4
- package/src/core/generator.mjs +15 -3
- package/src/core/metrics.mjs +143 -15
- package/src/core/presentation.mjs +190 -3
- package/src/core/project-identity.mjs +132 -0
- package/src/core/range.mjs +155 -8
- package/src/core/receipt-record.mjs +36 -1
- package/src/core/selector.mjs +146 -21
- package/src/lib/tool-category.mjs +36 -0
- package/src/parsers/codex.mjs +58 -7
- package/src/renderers/html.mjs +661 -2
package/src/renderers/html.mjs
CHANGED
|
@@ -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) {
|