@kidli1412/dsh-token-heatmap 0.1.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/LICENSE +21 -0
- package/README.md +64 -0
- package/cordis.patch.yml +12 -0
- package/docs//347/203/255/345/212/233/345/233/276.jpg +0 -0
- package/lib/client.js +908 -0
- package/lib/config.js +54 -0
- package/lib/index.js +474 -0
- package/lib/usage.js +251 -0
- package/package.json +60 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-token-heatmap — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written `__ModuleLoader__` bundle (no build step): registers into the
|
|
5
|
+
* conversation `input.dock` list slot and renders a GitHub-style heatmap of a
|
|
6
|
+
* SELECTABLE CALENDAR YEAR (Jan 1 – Dec 31; columns = weeks, rows = Mon..Sun,
|
|
7
|
+
* all seven weekday labels on the left) plus today / this-month / all-time
|
|
8
|
+
* totals and a ‹year› year selector next to the stats. It also registers a
|
|
9
|
+
* settings card into the official `settings.plugin.item` seat
|
|
10
|
+
* (设置 → 插件 → 插件配置) with a display switch and a green/blue palette
|
|
11
|
+
* choice, persisted through the server half's loopback config endpoint.
|
|
12
|
+
*
|
|
13
|
+
* The card is shown ONLY on the new-session (hero) screen — gated on
|
|
14
|
+
* `session.composerPhase === "blank"` — and is positioned visually BELOW the
|
|
15
|
+
* composer input card via a flex `order` on the dock wrapper (the dock's DOM
|
|
16
|
+
* site sits above the bar, but the composer stack is a flex column and the
|
|
17
|
+
* framework's true below-card seat `composer.dock` is disabled on the hero
|
|
18
|
+
* screen).
|
|
19
|
+
*
|
|
20
|
+
* Cell colors use ABSOLUTE token thresholds (not window-relative ranks), so
|
|
21
|
+
* a huge day is always dark regardless of its neighbours:
|
|
22
|
+
* level 0: 0 level 1: <1M level 2: 1M–10M
|
|
23
|
+
* level 3: 10M–100M level 4: ≥100M
|
|
24
|
+
*
|
|
25
|
+
* Data comes from the server half's loopback-only endpoint via same-origin
|
|
26
|
+
* fetch: GET /api/token-heatmap/usage.
|
|
27
|
+
*/
|
|
28
|
+
window.__ModuleLoader__.load({
|
|
29
|
+
id: "@kidli1412/dsh-token-heatmap",
|
|
30
|
+
factory: (require) => {
|
|
31
|
+
var module = { exports: {} };
|
|
32
|
+
var exports = module.exports;
|
|
33
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
34
|
+
|
|
35
|
+
let react = require("react");
|
|
36
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
37
|
+
|
|
38
|
+
//#region css
|
|
39
|
+
const css = [
|
|
40
|
+
".thm_dock{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width,780px);margin:0 auto;padding:0 16px 2px}",
|
|
41
|
+
".thm_card{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);border-radius:12px;padding:10px 12px 12px;display:flex;flex-direction:column;gap:8px}",
|
|
42
|
+
".thm_head{flex-wrap:wrap;align-items:baseline;gap:4px 12px;display:flex}",
|
|
43
|
+
".thm_title{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;line-height:18px}",
|
|
44
|
+
".thm_stat{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}",
|
|
45
|
+
".thm_statLabel{margin-right:4px}",
|
|
46
|
+
".thm_statValue{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;line-height:18px;font-variant-numeric:tabular-nums}",
|
|
47
|
+
".thm_yearNav{align-items:center;gap:2px;display:inline-flex}",
|
|
48
|
+
".thm_yearBtn{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:4px;padding:0 5px;font-size:13px;line-height:18px}",
|
|
49
|
+
".thm_yearBtn:hover:not(:disabled){color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
50
|
+
".thm_yearBtn:disabled{opacity:.35;cursor:default}",
|
|
51
|
+
".thm_yearLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;min-width:36px;text-align:center;font-variant-numeric:tabular-nums}",
|
|
52
|
+
".thm_refresh{cursor:pointer;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:6px;margin-left:auto;padding:2px 6px;font:inherit;font-size:11px;line-height:16px}",
|
|
53
|
+
".thm_refresh:hover{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
|
|
54
|
+
".thm_refresh:disabled{opacity:.5;cursor:default}",
|
|
55
|
+
".thm_gridWrap{overflow-x:auto}",
|
|
56
|
+
".thm_grid{width:max-content;display:flex;flex-direction:column;gap:2px}",
|
|
57
|
+
".thm_months{margin-left:16px;display:flex}",
|
|
58
|
+
".thm_monthLabel{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px;line-height:14px;white-space:nowrap;overflow:hidden}",
|
|
59
|
+
".thm_row{display:flex;gap:2px}",
|
|
60
|
+
".thm_rowLabel{color:var(--dsw-alias-label-caption);width:16px;flex:none;font-size:9px;line-height:12px;text-align:center}",
|
|
61
|
+
".thm_cell{box-sizing:border-box;width:10px;height:10px;border-radius:2px;flex:none}",
|
|
62
|
+
".thm_legend{align-items:center;gap:3px;margin-left:auto;font-size:10px;line-height:14px;display:inline-flex;color:var(--dsw-alias-label-tertiary)}",
|
|
63
|
+
".thm_legendCell{box-sizing:border-box;width:9px;height:9px;border-radius:2px}",
|
|
64
|
+
".thm_error{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger);border-radius:8px;padding:7px 8px;font-size:11px;line-height:16px;display:flex;justify-content:space-between;align-items:center;gap:8px}",
|
|
65
|
+
".thm_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0}",
|
|
66
|
+
".thm_loading{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;padding:6px 2px}",
|
|
67
|
+
".thms_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}",
|
|
68
|
+
".thms_card:hover{border-color:var(--dsw-alias-label-dimmed)}",
|
|
69
|
+
".thms_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
|
|
70
|
+
".thms_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
|
|
71
|
+
".thms_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
|
|
72
|
+
".thms_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
|
|
73
|
+
".thms_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
|
|
74
|
+
".thms_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
|
|
75
|
+
".thms_chevronOpen{transform:rotate(180deg)}",
|
|
76
|
+
".thms_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
|
|
77
|
+
".thms_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:12px}",
|
|
78
|
+
".thms_loading{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0}",
|
|
79
|
+
".thms_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5;margin:0}",
|
|
80
|
+
".thms_retryBtn{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;text-decoration:underline}",
|
|
81
|
+
".thms_field{display:flex;flex-direction:column;gap:4px}",
|
|
82
|
+
".thms_fieldLabel{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
|
|
83
|
+
".thms_fieldHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:0}",
|
|
84
|
+
".thms_switchRow{display:flex;align-items:center;gap:10px}",
|
|
85
|
+
".thms_switch{position:relative;width:36px;height:20px;flex:none;border-radius:999px;border:0;padding:0;cursor:pointer;background:var(--dsw-alias-interactive-bg-hover-solid);transition:background .15s}",
|
|
86
|
+
".thms_switch:disabled{opacity:.5;cursor:default}",
|
|
87
|
+
".thms_switchOn{background:var(--dsw-alias-state-business-primary)}",
|
|
88
|
+
".thms_thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s}",
|
|
89
|
+
".thms_switchOn .thms_thumb{transform:translateX(16px)}",
|
|
90
|
+
".thms_swatches{display:flex;flex-wrap:wrap;gap:8px}",
|
|
91
|
+
".thms_swatch{appearance:none;font:inherit;cursor:pointer;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:0 0;border-radius:8px;padding:6px 10px;display:flex}",
|
|
92
|
+
".thms_swatch:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
|
|
93
|
+
".thms_swatch:disabled{opacity:.5;cursor:default}",
|
|
94
|
+
".thms_swatch[data-active=true]{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary) inset}",
|
|
95
|
+
".thms_swatchCells{display:flex;gap:2px}",
|
|
96
|
+
".thms_swatchCell{width:10px;height:10px;border-radius:2px}",
|
|
97
|
+
".thms_swatchLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}",
|
|
98
|
+
".thms_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}",
|
|
99
|
+
".thms_footerError{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}",
|
|
100
|
+
".thms_btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}",
|
|
101
|
+
".thms_btn:disabled{opacity:.4;cursor:default}",
|
|
102
|
+
".thms_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}",
|
|
103
|
+
".thms_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}",
|
|
104
|
+
".thms_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}"
|
|
105
|
+
].join("");
|
|
106
|
+
const tagId = "@kidli1412/dsh-token-heatmap/TokenHeatmap.module.css";
|
|
107
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
108
|
+
const tag = document.createElement("style");
|
|
109
|
+
tag.dataset.plugin = "@kidli1412/dsh-token-heatmap";
|
|
110
|
+
tag.dataset.pluginCss = tagId;
|
|
111
|
+
tag.textContent = css;
|
|
112
|
+
document.head.appendChild(tag);
|
|
113
|
+
}
|
|
114
|
+
const S = {
|
|
115
|
+
dock: "thm_dock",
|
|
116
|
+
card: "thm_card",
|
|
117
|
+
head: "thm_head",
|
|
118
|
+
title: "thm_title",
|
|
119
|
+
stat: "thm_stat",
|
|
120
|
+
statLabel: "thm_statLabel",
|
|
121
|
+
statValue: "thm_statValue",
|
|
122
|
+
yearNav: "thm_yearNav",
|
|
123
|
+
yearBtn: "thm_yearBtn",
|
|
124
|
+
yearLabel: "thm_yearLabel",
|
|
125
|
+
refresh: "thm_refresh",
|
|
126
|
+
gridWrap: "thm_gridWrap",
|
|
127
|
+
grid: "thm_grid",
|
|
128
|
+
months: "thm_months",
|
|
129
|
+
monthLabel: "thm_monthLabel",
|
|
130
|
+
row: "thm_row",
|
|
131
|
+
rowLabel: "thm_rowLabel",
|
|
132
|
+
cell: "thm_cell",
|
|
133
|
+
legend: "thm_legend",
|
|
134
|
+
legendCell: "thm_legendCell",
|
|
135
|
+
error: "thm_error",
|
|
136
|
+
retry: "thm_retry",
|
|
137
|
+
loading: "thm_loading",
|
|
138
|
+
settingsCard: "thms_card",
|
|
139
|
+
settingsCardOpen: "thms_cardOpen",
|
|
140
|
+
settingsHeader: "thms_header",
|
|
141
|
+
settingsHeadText: "thms_headText",
|
|
142
|
+
settingsName: "thms_name",
|
|
143
|
+
settingsDesc: "thms_desc",
|
|
144
|
+
settingsChevron: "thms_chevron",
|
|
145
|
+
settingsChevronOpen: "thms_chevronOpen",
|
|
146
|
+
settingsPending: "thms_pending",
|
|
147
|
+
settingsBody: "thms_body",
|
|
148
|
+
settingsLoading: "thms_loading",
|
|
149
|
+
settingsError: "thms_error",
|
|
150
|
+
settingsRetry: "thms_retryBtn",
|
|
151
|
+
settingsField: "thms_field",
|
|
152
|
+
settingsFieldLabel: "thms_fieldLabel",
|
|
153
|
+
settingsFieldHint: "thms_fieldHint",
|
|
154
|
+
settingsSwitchRow: "thms_switchRow",
|
|
155
|
+
settingsSwitch: "thms_switch",
|
|
156
|
+
settingsSwitchOn: "thms_switchOn",
|
|
157
|
+
settingsThumb: "thms_thumb",
|
|
158
|
+
settingsSwatches: "thms_swatches",
|
|
159
|
+
settingsSwatch: "thms_swatch",
|
|
160
|
+
settingsSwatchCells: "thms_swatchCells",
|
|
161
|
+
settingsSwatchCell: "thms_swatchCell",
|
|
162
|
+
settingsSwatchLabel: "thms_swatchLabel",
|
|
163
|
+
settingsFooter: "thms_footer",
|
|
164
|
+
settingsFooterError: "thms_footerError",
|
|
165
|
+
settingsBtn: "thms_btn",
|
|
166
|
+
settingsDiscard: "thms_discard",
|
|
167
|
+
settingsSave: "thms_save"
|
|
168
|
+
};
|
|
169
|
+
//#endregion
|
|
170
|
+
|
|
171
|
+
//#region locale
|
|
172
|
+
/** Locale namespace and dictionaries. */
|
|
173
|
+
const NS = "tokenHeatmap";
|
|
174
|
+
const zh = {
|
|
175
|
+
title: "Token 用量",
|
|
176
|
+
refresh: "刷新",
|
|
177
|
+
loading: "加载中…",
|
|
178
|
+
error: "Token 统计暂不可用",
|
|
179
|
+
errorHint: "服务端未加载或请求失败",
|
|
180
|
+
retry: "重试",
|
|
181
|
+
today: "今日",
|
|
182
|
+
month: "本月",
|
|
183
|
+
total: "累计",
|
|
184
|
+
prevYear: "上一年",
|
|
185
|
+
nextYear: "下一年",
|
|
186
|
+
less: "少",
|
|
187
|
+
more: "多",
|
|
188
|
+
cellTokens: "{date} · {tokens} tokens",
|
|
189
|
+
weekdayLabel: ["一", "二", "三", "四", "五", "六", "日"],
|
|
190
|
+
monthName: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
|
|
191
|
+
levelLegend: ["0", "<1M", "1M–10M", "10M–100M", "≥100M"],
|
|
192
|
+
settingsTitle: "Token 用量热力图",
|
|
193
|
+
settingsDescription: "新会话页面的 Token 用量热力图显示与配色。",
|
|
194
|
+
settingsEnabled: "显示热力图",
|
|
195
|
+
settingsEnabledHint: "关闭后,新会话输入框下方的 Token 用量热力图不再显示。",
|
|
196
|
+
settingsScheme: "配色方案",
|
|
197
|
+
settingsSchemeHint: "热力图格子的颜色主题:绿色为经典 GitHub 风格,其余为新增配色。",
|
|
198
|
+
settingsGreen: "绿色",
|
|
199
|
+
settingsBlue: "蓝色",
|
|
200
|
+
settingsOrange: "橙色",
|
|
201
|
+
settingsRed: "红色",
|
|
202
|
+
settingsPurple: "紫色",
|
|
203
|
+
settingsTeal: "青色",
|
|
204
|
+
settingsSave: "保存",
|
|
205
|
+
settingsSaving: "保存中…",
|
|
206
|
+
settingsDiscard: "放弃修改",
|
|
207
|
+
settingsUnsaved: "未保存",
|
|
208
|
+
settingsSaveFailed: "保存失败,修改已保留,请重试。",
|
|
209
|
+
settingsLoadFailed: "配置加载失败。",
|
|
210
|
+
settingsRetry: "重试",
|
|
211
|
+
settingsExpand: "展开设置",
|
|
212
|
+
settingsCollapse: "收起设置"
|
|
213
|
+
};
|
|
214
|
+
const en = {
|
|
215
|
+
title: "Token usage",
|
|
216
|
+
refresh: "Refresh",
|
|
217
|
+
loading: "Loading…",
|
|
218
|
+
error: "Token stats unavailable",
|
|
219
|
+
errorHint: "server half not loaded or request failed",
|
|
220
|
+
retry: "Retry",
|
|
221
|
+
today: "Today",
|
|
222
|
+
month: "This month",
|
|
223
|
+
total: "All time",
|
|
224
|
+
prevYear: "Previous year",
|
|
225
|
+
nextYear: "Next year",
|
|
226
|
+
less: "Less",
|
|
227
|
+
more: "More",
|
|
228
|
+
cellTokens: "{date} · {tokens} tokens",
|
|
229
|
+
weekdayLabel: ["M", "T", "W", "T", "F", "S", "S"],
|
|
230
|
+
monthName: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
|
231
|
+
levelLegend: ["0", "<1M", "1M–10M", "10M–100M", "≥100M"],
|
|
232
|
+
settingsTitle: "Token usage heatmap",
|
|
233
|
+
settingsDescription: "Display and colors of the token heatmap on the new-session screen.",
|
|
234
|
+
settingsEnabled: "Show heatmap",
|
|
235
|
+
settingsEnabledHint: "When off, the token heatmap below the composer is hidden.",
|
|
236
|
+
settingsScheme: "Color scheme",
|
|
237
|
+
settingsSchemeHint: "Cell palette: green is the classic GitHub style; the rest are new themes.",
|
|
238
|
+
settingsGreen: "Green",
|
|
239
|
+
settingsBlue: "Blue",
|
|
240
|
+
settingsOrange: "Orange",
|
|
241
|
+
settingsRed: "Red",
|
|
242
|
+
settingsPurple: "Purple",
|
|
243
|
+
settingsTeal: "Teal",
|
|
244
|
+
settingsSave: "Save",
|
|
245
|
+
settingsSaving: "Saving…",
|
|
246
|
+
settingsDiscard: "Discard",
|
|
247
|
+
settingsUnsaved: "Unsaved",
|
|
248
|
+
settingsSaveFailed: "Save failed; your changes were kept.",
|
|
249
|
+
settingsLoadFailed: "Failed to load configuration.",
|
|
250
|
+
settingsRetry: "Retry",
|
|
251
|
+
settingsExpand: "Show settings",
|
|
252
|
+
settingsCollapse: "Hide settings"
|
|
253
|
+
};
|
|
254
|
+
//#endregion
|
|
255
|
+
|
|
256
|
+
//#region helpers
|
|
257
|
+
/** Locale-safe template interpolation: `t("key", {a})` replaces `{a}`. */
|
|
258
|
+
function interpolate(template, params) {
|
|
259
|
+
if (params === void 0) return template;
|
|
260
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(params, key) ? String(params[key]) : match));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Group thousands. */
|
|
264
|
+
function fmt(n) {
|
|
265
|
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Compact form: 1234 → "1.2k"; 61M → "61m"; 4.2B → "4.2b". */
|
|
269
|
+
function fmtCompact(n) {
|
|
270
|
+
if (n < 1000) return String(n);
|
|
271
|
+
if (n < 1000000) return `${(n / 1000).toFixed(n < 10000 ? 1 : 0)}k`;
|
|
272
|
+
if (n < 1000000000) return `${(n / 1000000).toFixed(n < 10000000 ? 1 : 0)}m`;
|
|
273
|
+
return `${(n / 1000000000).toFixed(n < 10000000000 ? 1 : 0)}b`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Local-calendar `YYYY-MM-DD` key. */
|
|
277
|
+
function dayKey(timeMs) {
|
|
278
|
+
const date = new Date(timeMs);
|
|
279
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
280
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
281
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Local-calendar `YYYY-MM` key. */
|
|
285
|
+
function monthKey(timeMs) {
|
|
286
|
+
const date = new Date(timeMs);
|
|
287
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** `YYYY-MM-DD` → `YYYY-MM` without any timezone parsing. */
|
|
291
|
+
function monthOfDateKey(key) {
|
|
292
|
+
return key.slice(0, 7);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Pick the display dictionary for locale-shaped helpers (month names,
|
|
297
|
+
* weekday labels, date formatting). The locale plugin resolves the UI
|
|
298
|
+
* language from `navigator.languages`, so mirror that probe here.
|
|
299
|
+
*/
|
|
300
|
+
function uiDict() {
|
|
301
|
+
const candidates = typeof navigator !== "undefined"
|
|
302
|
+
? [...(navigator.languages ?? []), navigator.language].filter(Boolean)
|
|
303
|
+
: [];
|
|
304
|
+
return candidates.some((tag) => tag.toLowerCase().startsWith("zh")) ? zh : en;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Display date "8月13日" / "Aug 13". */
|
|
308
|
+
function dateLabel(key) {
|
|
309
|
+
const [year, month, day] = key.split("-").map(Number);
|
|
310
|
+
const names = uiDict().monthName;
|
|
311
|
+
return `${names[month - 1]} ${day}日`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Per-request staleness guard: only the most recent start may `isCurrent()`. */
|
|
315
|
+
function createLoader() {
|
|
316
|
+
let current = 0;
|
|
317
|
+
return {
|
|
318
|
+
start: () => ++current,
|
|
319
|
+
isCurrent: (id) => id === current
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function fetchJson(path) {
|
|
324
|
+
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
325
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
326
|
+
const payload = await response.json();
|
|
327
|
+
if (payload === null || typeof payload !== "object") throw new Error("unexpected response");
|
|
328
|
+
return payload;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function postJson(path, body) {
|
|
332
|
+
const response = await fetch(path, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: { "content-type": "application/json" },
|
|
335
|
+
body: JSON.stringify(body)
|
|
336
|
+
});
|
|
337
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
338
|
+
const payload = await response.json();
|
|
339
|
+
if (payload === null || typeof payload !== "object") throw new Error("unexpected response");
|
|
340
|
+
return payload;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Cell palettes by scheme, level 0 (empty) → 4 (peak). Green is the
|
|
345
|
+
* classic GitHub scale; the others are ColorBrewer single-hue ramps
|
|
346
|
+
* (blue/orange/red/purple) plus a custom teal. The active scheme comes
|
|
347
|
+
* from the settings card (配置 → 配色方案), persisted by the server
|
|
348
|
+
* half's config endpoint.
|
|
349
|
+
*/
|
|
350
|
+
const COLOR_SCHEMES = {
|
|
351
|
+
green: ["rgba(128,128,128,0.15)", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
|
352
|
+
blue: ["rgba(128,128,128,0.15)", "#c6dbef", "#6baed6", "#3182bd", "#08519c"],
|
|
353
|
+
orange: ["rgba(128,128,128,0.15)", "#fdd0a2", "#fdae6b", "#fd8d3c", "#d94801"],
|
|
354
|
+
red: ["rgba(128,128,128,0.15)", "#fcbba1", "#fc9272", "#fb6a4a", "#cb181d"],
|
|
355
|
+
purple: ["rgba(128,128,128,0.15)", "#bcbddc", "#9e9ac8", "#807dba", "#6a51a3"],
|
|
356
|
+
teal: ["rgba(128,128,128,0.15)", "#99f6e4", "#2dd4bf", "#14b8a6", "#0f766e"]
|
|
357
|
+
};
|
|
358
|
+
/** Backward-compatible alias: the default (green) palette. */
|
|
359
|
+
const CELL_COLORS = COLOR_SCHEMES.green;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* ABSOLUTE token thresholds per day (not relative ranks), so a large
|
|
363
|
+
* day is always dark no matter what its neighbours look like:
|
|
364
|
+
* level 0: 0 tokens
|
|
365
|
+
* level 1: 1 .. 1M
|
|
366
|
+
* level 2: 1M .. 10M
|
|
367
|
+
* level 3: 10M .. 100M
|
|
368
|
+
* level 4: ≥ 100M
|
|
369
|
+
* e.g. 61M/day is level 3 (#30a14e, dark-ish green).
|
|
370
|
+
*/
|
|
371
|
+
function levelOf(tokens) {
|
|
372
|
+
if (tokens <= 0) return 0;
|
|
373
|
+
if (tokens < 1e6) return 1;
|
|
374
|
+
if (tokens < 1e7) return 2;
|
|
375
|
+
if (tokens < 1e8) return 3;
|
|
376
|
+
return 4;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Build the GitHub-style grid for a CALENDAR YEAR (Jan 1 – Dec 31,
|
|
381
|
+
* local time). Columns are weeks (Mon-first), aligned so the first
|
|
382
|
+
* column starts on the Monday of the week containing Jan 1; cells
|
|
383
|
+
* outside the year are null, and days after today render as empty
|
|
384
|
+
* level-0 cells so the full year layout stays visible (for past years
|
|
385
|
+
* no day is in the future, so every in-year cell shows real or empty
|
|
386
|
+
* data). Month labels span the columns they cover (Jan..Dec).
|
|
387
|
+
* @param dayMap - date key → tokens.
|
|
388
|
+
* @param now - reference timestamp.
|
|
389
|
+
* @param year - calendar year to render; defaults to the year of `now`.
|
|
390
|
+
* @returns `{ columns, monthStarts }` where each column is
|
|
391
|
+
* `{ month, cells: Array<{key, tokens, level} | null> }`.
|
|
392
|
+
*/
|
|
393
|
+
function buildGrid(dayMap, now, year) {
|
|
394
|
+
const nowDate = new Date(now);
|
|
395
|
+
const gridYear = year ?? nowDate.getFullYear();
|
|
396
|
+
const yearStart = new Date(gridYear, 0, 1);
|
|
397
|
+
const yearEnd = new Date(gridYear, 11, 31);
|
|
398
|
+
const startKey = dayKey(yearStart.getTime());
|
|
399
|
+
const endKey = dayKey(yearEnd.getTime());
|
|
400
|
+
const todayKey = dayKey(nowDate.getTime());
|
|
401
|
+
const firstColumn = new Date(yearStart.getFullYear(), yearStart.getMonth(), yearStart.getDate() - ((yearStart.getDay() + 6) % 7));
|
|
402
|
+
const columns = [];
|
|
403
|
+
const monthStarts = [];
|
|
404
|
+
const cursor = new Date(firstColumn);
|
|
405
|
+
while (cursor.getTime() <= yearEnd.getTime()) {
|
|
406
|
+
const columnStartKey = dayKey(cursor.getTime());
|
|
407
|
+
const cells = [];
|
|
408
|
+
for (let d = 0; d < 7; d += 1) {
|
|
409
|
+
const key = dayKey(cursor.getTime());
|
|
410
|
+
if (key < startKey || key > endKey) {
|
|
411
|
+
cells.push(null);
|
|
412
|
+
} else {
|
|
413
|
+
const future = key > todayKey;
|
|
414
|
+
const tokens = future ? 0 : (dayMap.get(key) ?? 0);
|
|
415
|
+
cells.push({ key, tokens, level: levelOf(tokens) });
|
|
416
|
+
}
|
|
417
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
418
|
+
}
|
|
419
|
+
// Label the column by the month of its first in-year day; the
|
|
420
|
+
// very first column may start in December of the previous year.
|
|
421
|
+
const columnMonth = monthOfDateKey(cells.find((cell) => cell !== null)?.key ?? columnStartKey);
|
|
422
|
+
columns.push({ month: columnMonth, cells });
|
|
423
|
+
if (monthStarts.length === 0 || monthStarts[monthStarts.length - 1].month !== columnMonth) {
|
|
424
|
+
monthStarts.push({ month: columnMonth, index: columns.length - 1 });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { columns, monthStarts };
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
|
|
431
|
+
//#region TokenHeatmap
|
|
432
|
+
/**
|
|
433
|
+
* The heatmap card. Shown only on the new-session hero screen
|
|
434
|
+
* (`composerPhase === "blank"`); a flex `order` on the wrapper places
|
|
435
|
+
* it below the composer input card.
|
|
436
|
+
* @param props - `session`/`input` from the InputZone owner share, `t`
|
|
437
|
+
* bound by the slot runtime locale seat.
|
|
438
|
+
*/
|
|
439
|
+
function TokenHeatmap({ session, input, t }) {
|
|
440
|
+
const dict = t !== void 0 ? t : (key) => zh[key] ?? key;
|
|
441
|
+
const translate = (key, params) => interpolate(dict(key), params);
|
|
442
|
+
const [data, setData] = react.useState(null);
|
|
443
|
+
const [config, setConfig] = react.useState(null);
|
|
444
|
+
const [error, setError] = react.useState(null);
|
|
445
|
+
const [loading, setLoading] = react.useState(false);
|
|
446
|
+
const [year, setYear] = react.useState(() => new Date().getFullYear());
|
|
447
|
+
const loaderRef = react.useRef(null);
|
|
448
|
+
if (loaderRef.current === null) loaderRef.current = createLoader();
|
|
449
|
+
const load = react.useCallback(async () => {
|
|
450
|
+
const id = loaderRef.current.start();
|
|
451
|
+
setLoading(true);
|
|
452
|
+
try {
|
|
453
|
+
const [payload, cfg] = await Promise.all([
|
|
454
|
+
fetchJson("/api/token-heatmap/usage"),
|
|
455
|
+
fetchJson("/api/token-heatmap/config")
|
|
456
|
+
]);
|
|
457
|
+
if (loaderRef.current.isCurrent(id)) {
|
|
458
|
+
setData(payload);
|
|
459
|
+
setConfig(cfg);
|
|
460
|
+
setError(null);
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
if (loaderRef.current.isCurrent(id)) setError(err instanceof Error ? err.message : String(err));
|
|
464
|
+
} finally {
|
|
465
|
+
if (loaderRef.current.isCurrent(id)) setLoading(false);
|
|
466
|
+
}
|
|
467
|
+
}, []);
|
|
468
|
+
react.useEffect(() => {
|
|
469
|
+
load();
|
|
470
|
+
const timer = window.setInterval(load, 5 * 60 * 1000);
|
|
471
|
+
const onVisible = () => {
|
|
472
|
+
if (document.visibilityState === "visible") load();
|
|
473
|
+
};
|
|
474
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
475
|
+
return () => {
|
|
476
|
+
window.clearInterval(timer);
|
|
477
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
478
|
+
};
|
|
479
|
+
}, [load]);
|
|
480
|
+
|
|
481
|
+
// New-session hero only: a blank session whose composer is still in
|
|
482
|
+
// the guidance phase. Conversed sessions hide the card.
|
|
483
|
+
if (session === void 0 || session === null || input === void 0 || input === null) return null;
|
|
484
|
+
if (session.composerPhase !== "blank") return null;
|
|
485
|
+
// Master switch from the settings card (设置 → 插件 → 插件配置).
|
|
486
|
+
if (config !== null && config.enabled === false) return null;
|
|
487
|
+
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
// Year-selector bounds: the current year at most (no future usage),
|
|
490
|
+
// and at least the earliest year present in the data so the selector
|
|
491
|
+
// cannot wander into guaranteed-empty territory.
|
|
492
|
+
const nowYear = new Date(now).getFullYear();
|
|
493
|
+
let minYear = nowYear;
|
|
494
|
+
for (const day of data?.days ?? []) {
|
|
495
|
+
const y = Number(day.date.slice(0, 4));
|
|
496
|
+
if (y < minYear) minYear = y;
|
|
497
|
+
}
|
|
498
|
+
const maxYear = nowYear;
|
|
499
|
+
const todayTokens = data?.days?.find((day) => day.date === dayKey(now))?.tokens ?? 0;
|
|
500
|
+
const monthPrefix = monthKey(now);
|
|
501
|
+
let monthTokens = 0;
|
|
502
|
+
for (const day of data?.days ?? []) {
|
|
503
|
+
if (day.date.startsWith(monthPrefix)) monthTokens += day.tokens;
|
|
504
|
+
}
|
|
505
|
+
const totalTokens = data?.total ?? 0;
|
|
506
|
+
|
|
507
|
+
const dayMap = new Map();
|
|
508
|
+
for (const day of data?.days ?? []) dayMap.set(day.date, day.tokens);
|
|
509
|
+
const grid = buildGrid(dayMap, now, year);
|
|
510
|
+
const levelLegend = uiDict().levelLegend;
|
|
511
|
+
// Palette from the settings card; unknown schemes fall back to green.
|
|
512
|
+
const palette = COLOR_SCHEMES[config?.colorScheme] ?? COLOR_SCHEMES.green;
|
|
513
|
+
|
|
514
|
+
const stats = [
|
|
515
|
+
{ label: dict("today"), value: todayTokens },
|
|
516
|
+
{ label: dict("month"), value: monthTokens },
|
|
517
|
+
{ label: dict("total"), value: totalTokens }
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
let body;
|
|
521
|
+
if (error !== null) {
|
|
522
|
+
body = react_jsx_runtime.jsxs("div", {
|
|
523
|
+
className: S.error,
|
|
524
|
+
children: [
|
|
525
|
+
react_jsx_runtime.jsx("span", {
|
|
526
|
+
children: `${dict("error")}(${dict("errorHint")})`
|
|
527
|
+
}),
|
|
528
|
+
react_jsx_runtime.jsx("button", {
|
|
529
|
+
type: "button",
|
|
530
|
+
className: S.retry,
|
|
531
|
+
onClick: load,
|
|
532
|
+
children: dict("retry")
|
|
533
|
+
})
|
|
534
|
+
]
|
|
535
|
+
});
|
|
536
|
+
} else if (data === null) {
|
|
537
|
+
body = react_jsx_runtime.jsx("div", {
|
|
538
|
+
className: S.loading,
|
|
539
|
+
children: dict("loading")
|
|
540
|
+
});
|
|
541
|
+
} else {
|
|
542
|
+
body = react_jsx_runtime.jsxs("div", {
|
|
543
|
+
className: S.card,
|
|
544
|
+
children: [
|
|
545
|
+
react_jsx_runtime.jsxs("div", {
|
|
546
|
+
className: S.head,
|
|
547
|
+
children: [
|
|
548
|
+
react_jsx_runtime.jsx("span", { className: S.title, children: dict("title") }),
|
|
549
|
+
...stats.map((stat) => react_jsx_runtime.jsxs("span", {
|
|
550
|
+
className: S.stat,
|
|
551
|
+
title: `${stat.label}: ${fmt(stat.value)}`,
|
|
552
|
+
children: [
|
|
553
|
+
react_jsx_runtime.jsx("span", { className: S.statLabel, children: stat.label }),
|
|
554
|
+
react_jsx_runtime.jsx("span", { className: S.statValue, children: fmtCompact(stat.value) })
|
|
555
|
+
]
|
|
556
|
+
}, stat.label)),
|
|
557
|
+
react_jsx_runtime.jsxs("span", {
|
|
558
|
+
className: S.yearNav,
|
|
559
|
+
children: [
|
|
560
|
+
react_jsx_runtime.jsx("button", {
|
|
561
|
+
type: "button",
|
|
562
|
+
className: S.yearBtn,
|
|
563
|
+
disabled: year <= minYear,
|
|
564
|
+
onClick: () => setYear(year - 1),
|
|
565
|
+
"aria-label": dict("prevYear"),
|
|
566
|
+
children: "\u2039"
|
|
567
|
+
}),
|
|
568
|
+
react_jsx_runtime.jsx("span", { className: S.yearLabel, children: String(year) }),
|
|
569
|
+
react_jsx_runtime.jsx("button", {
|
|
570
|
+
type: "button",
|
|
571
|
+
className: S.yearBtn,
|
|
572
|
+
disabled: year >= maxYear,
|
|
573
|
+
onClick: () => setYear(year + 1),
|
|
574
|
+
"aria-label": dict("nextYear"),
|
|
575
|
+
children: "\u203a"
|
|
576
|
+
})
|
|
577
|
+
]
|
|
578
|
+
}),
|
|
579
|
+
react_jsx_runtime.jsx("button", {
|
|
580
|
+
type: "button",
|
|
581
|
+
className: S.refresh,
|
|
582
|
+
disabled: loading,
|
|
583
|
+
onClick: load,
|
|
584
|
+
children: dict("refresh")
|
|
585
|
+
})
|
|
586
|
+
]
|
|
587
|
+
}),
|
|
588
|
+
react_jsx_runtime.jsx("div", {
|
|
589
|
+
className: S.gridWrap,
|
|
590
|
+
children: react_jsx_runtime.jsxs("div", {
|
|
591
|
+
className: S.grid,
|
|
592
|
+
children: [
|
|
593
|
+
react_jsx_runtime.jsx("div", {
|
|
594
|
+
className: S.months,
|
|
595
|
+
children: grid.monthStarts.map((entry, index) => {
|
|
596
|
+
const span = index + 1 < grid.monthStarts.length ? grid.monthStarts[index + 1].index - entry.index : grid.columns.length - entry.index;
|
|
597
|
+
// One column = 10px cell + 2px gap = 12px; the label
|
|
598
|
+
// box must span exactly N columns (no -2: there is no
|
|
599
|
+
// gap between month labels, so a short box would drift
|
|
600
|
+
// left by 2px per month).
|
|
601
|
+
return react_jsx_runtime.jsx("span", {
|
|
602
|
+
className: S.monthLabel,
|
|
603
|
+
style: { width: span * 12 },
|
|
604
|
+
children: uiDict().monthName[Number(entry.month.slice(5)) - 1]
|
|
605
|
+
}, entry.month);
|
|
606
|
+
})
|
|
607
|
+
}),
|
|
608
|
+
...Array.from({ length: 7 }, (_, row) => {
|
|
609
|
+
const label = uiDict().weekdayLabel[row] ?? "";
|
|
610
|
+
return react_jsx_runtime.jsxs("div", {
|
|
611
|
+
className: S.row,
|
|
612
|
+
children: [
|
|
613
|
+
react_jsx_runtime.jsx("span", { className: S.rowLabel, children: label }),
|
|
614
|
+
...grid.columns.map((column) => {
|
|
615
|
+
const cell = column.cells[row];
|
|
616
|
+
if (cell === null) {
|
|
617
|
+
return react_jsx_runtime.jsx("span", { className: S.cell }, `${column.month}-${row}-null`);
|
|
618
|
+
}
|
|
619
|
+
return react_jsx_runtime.jsx("span", {
|
|
620
|
+
className: S.cell,
|
|
621
|
+
style: { background: palette[cell.level] },
|
|
622
|
+
title: translate("cellTokens", { date: dateLabel(cell.key), tokens: fmt(cell.tokens) })
|
|
623
|
+
}, cell.key);
|
|
624
|
+
})
|
|
625
|
+
]
|
|
626
|
+
}, `row-${row}`);
|
|
627
|
+
}),
|
|
628
|
+
react_jsx_runtime.jsx("div", {
|
|
629
|
+
className: S.row,
|
|
630
|
+
children: [
|
|
631
|
+
react_jsx_runtime.jsx("span", { className: S.rowLabel, children: "" }),
|
|
632
|
+
react_jsx_runtime.jsx("span", {
|
|
633
|
+
className: S.legend,
|
|
634
|
+
children: [
|
|
635
|
+
dict("less"),
|
|
636
|
+
...palette.map((color, level) => react_jsx_runtime.jsx("span", {
|
|
637
|
+
className: S.legendCell,
|
|
638
|
+
style: { background: color },
|
|
639
|
+
title: levelLegend[level] ?? ""
|
|
640
|
+
}, `lvl-${level}`)),
|
|
641
|
+
dict("more")
|
|
642
|
+
]
|
|
643
|
+
})
|
|
644
|
+
]
|
|
645
|
+
})
|
|
646
|
+
]
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
]
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return react_jsx_runtime.jsx("div", {
|
|
654
|
+
className: S.dock,
|
|
655
|
+
style: { order: 99 },
|
|
656
|
+
children: body
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
|
|
661
|
+
//#region TokenHeatmapSettingsCard
|
|
662
|
+
/**
|
|
663
|
+
* The plugin settings card, rendered inside the official
|
|
664
|
+
* 设置 → 插件 → 插件配置 tab (the `settings.plugin.item` seat).
|
|
665
|
+
*
|
|
666
|
+
* The Host only exposes its own allowlisted settings namespaces to the
|
|
667
|
+
* Web client, so this card does NOT bind a settingsScope; instead it
|
|
668
|
+
* reads/writes the plugin's own loopback config endpoint and stages a
|
|
669
|
+
* draft until the user saves, mirroring the official card chrome
|
|
670
|
+
* (disclosure header, unsaved badge, discard/save footer).
|
|
671
|
+
* @param props - `t` bound by the slot runtime locale seat.
|
|
672
|
+
*/
|
|
673
|
+
function TokenHeatmapSettingsCard({ t }) {
|
|
674
|
+
const dict = t !== void 0 ? t : (key) => zh[key] ?? key;
|
|
675
|
+
const [open, setOpen] = react.useState(false);
|
|
676
|
+
const [loaded, setLoaded] = react.useState(null);
|
|
677
|
+
const [draft, setDraft] = react.useState(null);
|
|
678
|
+
const [saving, setSaving] = react.useState(false);
|
|
679
|
+
const [saveFailed, setSaveFailed] = react.useState(false);
|
|
680
|
+
const [loadFailed, setLoadFailed] = react.useState(false);
|
|
681
|
+
const load = react.useCallback(async () => {
|
|
682
|
+
try {
|
|
683
|
+
const payload = await fetchJson("/api/token-heatmap/config");
|
|
684
|
+
setLoaded(payload);
|
|
685
|
+
setDraft(payload);
|
|
686
|
+
setLoadFailed(false);
|
|
687
|
+
setSaveFailed(false);
|
|
688
|
+
} catch {
|
|
689
|
+
setLoadFailed(true);
|
|
690
|
+
}
|
|
691
|
+
}, []);
|
|
692
|
+
react.useEffect(() => {
|
|
693
|
+
load();
|
|
694
|
+
}, [load]);
|
|
695
|
+
|
|
696
|
+
const dirty = loaded !== null && draft !== null && (draft.enabled !== loaded.enabled || draft.colorScheme !== loaded.colorScheme);
|
|
697
|
+
const save = react.useCallback(async () => {
|
|
698
|
+
if (draft === null || saving) return;
|
|
699
|
+
setSaving(true);
|
|
700
|
+
setSaveFailed(false);
|
|
701
|
+
try {
|
|
702
|
+
const payload = await postJson("/api/token-heatmap/config", draft);
|
|
703
|
+
setLoaded(payload);
|
|
704
|
+
setDraft(payload);
|
|
705
|
+
} catch {
|
|
706
|
+
setSaveFailed(true);
|
|
707
|
+
} finally {
|
|
708
|
+
setSaving(false);
|
|
709
|
+
}
|
|
710
|
+
}, [draft, saving]);
|
|
711
|
+
const discard = () => {
|
|
712
|
+
if (loaded !== null) setDraft(loaded);
|
|
713
|
+
setSaveFailed(false);
|
|
714
|
+
};
|
|
715
|
+
const toggleEnabled = () => {
|
|
716
|
+
if (draft !== null) setDraft({ ...draft, enabled: !draft.enabled });
|
|
717
|
+
};
|
|
718
|
+
const chooseScheme = (scheme) => {
|
|
719
|
+
if (draft !== null) setDraft({ ...draft, colorScheme: scheme });
|
|
720
|
+
};
|
|
721
|
+
const title = dict("settingsTitle");
|
|
722
|
+
|
|
723
|
+
let bodyContent;
|
|
724
|
+
if (loadFailed) {
|
|
725
|
+
bodyContent = react_jsx_runtime.jsxs("p", {
|
|
726
|
+
className: S.settingsError,
|
|
727
|
+
children: [
|
|
728
|
+
dict("settingsLoadFailed"),
|
|
729
|
+
" ",
|
|
730
|
+
react_jsx_runtime.jsx("button", {
|
|
731
|
+
type: "button",
|
|
732
|
+
className: S.settingsRetry,
|
|
733
|
+
onClick: load,
|
|
734
|
+
children: dict("settingsRetry")
|
|
735
|
+
})
|
|
736
|
+
]
|
|
737
|
+
});
|
|
738
|
+
} else if (loaded === null || draft === null) {
|
|
739
|
+
bodyContent = react_jsx_runtime.jsx("p", {
|
|
740
|
+
className: S.settingsLoading,
|
|
741
|
+
children: dict("loading")
|
|
742
|
+
});
|
|
743
|
+
} else {
|
|
744
|
+
bodyContent = react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
|
|
745
|
+
children: [
|
|
746
|
+
react_jsx_runtime.jsx("div", {
|
|
747
|
+
className: S.settingsField,
|
|
748
|
+
children: react_jsx_runtime.jsxs("div", {
|
|
749
|
+
className: S.settingsSwitchRow,
|
|
750
|
+
children: [
|
|
751
|
+
react_jsx_runtime.jsx("button", {
|
|
752
|
+
type: "button",
|
|
753
|
+
role: "switch",
|
|
754
|
+
"aria-checked": draft.enabled,
|
|
755
|
+
"aria-label": dict("settingsEnabled"),
|
|
756
|
+
disabled: saving,
|
|
757
|
+
className: draft.enabled ? `${S.settingsSwitch} ${S.settingsSwitchOn}` : S.settingsSwitch,
|
|
758
|
+
onClick: toggleEnabled,
|
|
759
|
+
children: react_jsx_runtime.jsx("span", { className: S.settingsThumb })
|
|
760
|
+
}),
|
|
761
|
+
react_jsx_runtime.jsx("span", { className: S.settingsFieldLabel, children: dict("settingsEnabled") })
|
|
762
|
+
]
|
|
763
|
+
})
|
|
764
|
+
}),
|
|
765
|
+
react_jsx_runtime.jsx("p", {
|
|
766
|
+
className: S.settingsFieldHint,
|
|
767
|
+
children: dict("settingsEnabledHint")
|
|
768
|
+
}),
|
|
769
|
+
react_jsx_runtime.jsxs("div", {
|
|
770
|
+
className: S.settingsField,
|
|
771
|
+
children: [
|
|
772
|
+
react_jsx_runtime.jsx("span", { className: S.settingsFieldLabel, children: dict("settingsScheme") }),
|
|
773
|
+
react_jsx_runtime.jsx("div", {
|
|
774
|
+
className: S.settingsSwatches,
|
|
775
|
+
children: Object.keys(COLOR_SCHEMES).map((scheme) => react_jsx_runtime.jsx("button", {
|
|
776
|
+
type: "button",
|
|
777
|
+
disabled: saving,
|
|
778
|
+
className: S.settingsSwatch,
|
|
779
|
+
"data-active": draft.colorScheme === scheme,
|
|
780
|
+
onClick: () => chooseScheme(scheme),
|
|
781
|
+
children: [
|
|
782
|
+
react_jsx_runtime.jsx("span", {
|
|
783
|
+
className: S.settingsSwatchCells,
|
|
784
|
+
children: COLOR_SCHEMES[scheme].slice(1).map((color, index) => react_jsx_runtime.jsx("span", {
|
|
785
|
+
className: S.settingsSwatchCell,
|
|
786
|
+
style: { background: color }
|
|
787
|
+
}, `cell-${index}`))
|
|
788
|
+
}),
|
|
789
|
+
react_jsx_runtime.jsx("span", {
|
|
790
|
+
className: S.settingsSwatchLabel,
|
|
791
|
+
children: dict(`settings${scheme[0].toUpperCase()}${scheme.slice(1)}`)
|
|
792
|
+
})
|
|
793
|
+
]
|
|
794
|
+
}, scheme))
|
|
795
|
+
})
|
|
796
|
+
]
|
|
797
|
+
}),
|
|
798
|
+
react_jsx_runtime.jsx("p", {
|
|
799
|
+
className: S.settingsFieldHint,
|
|
800
|
+
children: dict("settingsSchemeHint")
|
|
801
|
+
}),
|
|
802
|
+
react_jsx_runtime.jsxs("div", {
|
|
803
|
+
className: S.settingsFooter,
|
|
804
|
+
children: [
|
|
805
|
+
saveFailed ? react_jsx_runtime.jsx("p", {
|
|
806
|
+
className: S.settingsFooterError,
|
|
807
|
+
children: dict("settingsSaveFailed")
|
|
808
|
+
}) : null,
|
|
809
|
+
react_jsx_runtime.jsx("button", {
|
|
810
|
+
type: "button",
|
|
811
|
+
className: `${S.settingsBtn} ${S.settingsDiscard}`,
|
|
812
|
+
disabled: !dirty || saving,
|
|
813
|
+
onClick: discard,
|
|
814
|
+
children: dict("settingsDiscard")
|
|
815
|
+
}),
|
|
816
|
+
react_jsx_runtime.jsx("button", {
|
|
817
|
+
type: "button",
|
|
818
|
+
className: `${S.settingsBtn} ${S.settingsSave}`,
|
|
819
|
+
disabled: !dirty || saving,
|
|
820
|
+
onClick: save,
|
|
821
|
+
children: dict(saving ? "settingsSaving" : "settingsSave")
|
|
822
|
+
})
|
|
823
|
+
]
|
|
824
|
+
})
|
|
825
|
+
]
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return react_jsx_runtime.jsxs("li", {
|
|
830
|
+
className: open ? `${S.settingsCard} ${S.settingsCardOpen}` : S.settingsCard,
|
|
831
|
+
children: [
|
|
832
|
+
react_jsx_runtime.jsxs("button", {
|
|
833
|
+
type: "button",
|
|
834
|
+
className: S.settingsHeader,
|
|
835
|
+
"aria-expanded": open,
|
|
836
|
+
"aria-label": `${dict(open ? "settingsCollapse" : "settingsExpand")}: ${title}`,
|
|
837
|
+
onClick: () => setOpen(!open),
|
|
838
|
+
children: [
|
|
839
|
+
react_jsx_runtime.jsxs("span", {
|
|
840
|
+
className: S.settingsHeadText,
|
|
841
|
+
children: [
|
|
842
|
+
react_jsx_runtime.jsx("span", { className: S.settingsName, children: title }),
|
|
843
|
+
react_jsx_runtime.jsx("span", { className: S.settingsDesc, children: dict("settingsDescription") })
|
|
844
|
+
]
|
|
845
|
+
}),
|
|
846
|
+
dirty ? react_jsx_runtime.jsx("span", {
|
|
847
|
+
className: S.settingsPending,
|
|
848
|
+
children: dict("settingsUnsaved")
|
|
849
|
+
}) : null,
|
|
850
|
+
react_jsx_runtime.jsx("svg", {
|
|
851
|
+
width: 14,
|
|
852
|
+
height: 14,
|
|
853
|
+
className: open ? `${S.settingsChevron} ${S.settingsChevronOpen}` : S.settingsChevron,
|
|
854
|
+
viewBox: "0 0 14 14",
|
|
855
|
+
fill: "none",
|
|
856
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
857
|
+
children: react_jsx_runtime.jsx("path", {
|
|
858
|
+
d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
|
|
859
|
+
fill: "currentColor"
|
|
860
|
+
})
|
|
861
|
+
})
|
|
862
|
+
]
|
|
863
|
+
}),
|
|
864
|
+
open ? react_jsx_runtime.jsx("div", {
|
|
865
|
+
className: S.settingsBody,
|
|
866
|
+
children: bodyContent
|
|
867
|
+
}) : null
|
|
868
|
+
]
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
|
|
873
|
+
//#region plugin body
|
|
874
|
+
/** Services required by the client plugin body. */
|
|
875
|
+
const inject = ["slots", "locale"];
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Client plugin body: register the dictionaries and the dock entry.
|
|
879
|
+
* @param ctx - client root context.
|
|
880
|
+
*/
|
|
881
|
+
function apply(ctx) {
|
|
882
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "token-heatmap: dictionaries");
|
|
883
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
884
|
+
name: "conversation.input.dock",
|
|
885
|
+
id: "token-heatmap",
|
|
886
|
+
order: 10,
|
|
887
|
+
locale: NS
|
|
888
|
+
}, TokenHeatmap));
|
|
889
|
+
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
890
|
+
name: "settings.plugin.item",
|
|
891
|
+
id: "token-heatmap",
|
|
892
|
+
order: 30,
|
|
893
|
+
locale: NS
|
|
894
|
+
}, TokenHeatmapSettingsCard));
|
|
895
|
+
}
|
|
896
|
+
//#endregion
|
|
897
|
+
|
|
898
|
+
exports.apply = apply;
|
|
899
|
+
exports.inject = inject;
|
|
900
|
+
exports.TokenHeatmap = TokenHeatmap;
|
|
901
|
+
exports.TokenHeatmapSettingsCard = TokenHeatmapSettingsCard;
|
|
902
|
+
exports.buildGrid = buildGrid;
|
|
903
|
+
exports.levelOf = levelOf;
|
|
904
|
+
exports.CELL_COLORS = CELL_COLORS;
|
|
905
|
+
exports.COLOR_SCHEMES = COLOR_SCHEMES;
|
|
906
|
+
return module.exports;
|
|
907
|
+
}
|
|
908
|
+
});
|