dsh-codearts 1.1.4 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,70 +33,70 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
33
33
  document.head.appendChild(tag);
34
34
  }
35
35
  var CodeArtsPluginCard_module_css_default = {
36
- "card": "xoziZG_card",
36
+ "modelChart": "xoziZG_modelChart",
37
+ "accountRowActions": "xoziZG_accountRowActions",
38
+ "field": "xoziZG_field",
37
39
  "statusLine": "xoziZG_statusLine",
40
+ "meta": "xoziZG_meta",
41
+ "manualForm": "xoziZG_manualForm",
42
+ "usageBar": "xoziZG_usageBar",
43
+ "check": "xoziZG_check",
38
44
  "tabs": "xoziZG_tabs",
45
+ "modelTrack": "xoziZG_modelTrack",
46
+ "modelFill": "xoziZG_modelFill",
47
+ "balance": "xoziZG_balance",
48
+ "creditLink": "xoziZG_creditLink",
39
49
  "actions": "xoziZG_actions",
40
- "choiceRow": "xoziZG_choiceRow",
41
- "modelShare": "xoziZG_modelShare",
42
- "accountRowActions": "xoziZG_accountRowActions",
50
+ "accountName": "xoziZG_accountName",
51
+ "area": "xoziZG_area",
43
52
  "sectionHead": "xoziZG_sectionHead",
44
- "creditFooter": "xoziZG_creditFooter",
45
- "danger": "xoziZG_danger",
46
- "modelRequests": "xoziZG_modelRequests",
53
+ "accountHint": "xoziZG_accountHint",
54
+ "expiryRow": "xoziZG_expiryRow",
55
+ "bar": "xoziZG_bar",
56
+ "accountCard": "xoziZG_accountCard",
57
+ "accountRow": "xoziZG_accountRow",
58
+ "balanceMeta": "xoziZG_balanceMeta",
59
+ "sectionLabel": "xoziZG_sectionLabel",
60
+ "barFill": "xoziZG_barFill",
61
+ "link": "xoziZG_link",
47
62
  "manual": "xoziZG_manual",
63
+ "accountNameRow": "xoziZG_accountNameRow",
48
64
  "fieldLabel": "xoziZG_fieldLabel",
49
- "count": "xoziZG_count",
50
- "modelName": "xoziZG_modelName",
65
+ "creditFooter": "xoziZG_creditFooter",
66
+ "creditBlock": "xoziZG_creditBlock",
67
+ "error": "xoziZG_error",
68
+ "creditRow": "xoziZG_creditRow",
69
+ "modelRow": "xoziZG_modelRow",
70
+ "updated": "xoziZG_updated",
71
+ "creditHint": "xoziZG_creditHint",
72
+ "balanceRow": "xoziZG_balanceRow",
51
73
  "expiryLabel": "xoziZG_expiryLabel",
52
- "name": "xoziZG_name",
53
- "barFill": "xoziZG_barFill",
54
- "balanceUnit": "xoziZG_balanceUnit",
55
- "section": "xoziZG_section",
56
- "usageBar": "xoziZG_usageBar",
57
- "accountHint": "xoziZG_accountHint",
58
- "modelTrack": "xoziZG_modelTrack",
74
+ "count": "xoziZG_count",
59
75
  "accountInfo": "xoziZG_accountInfo",
60
- "balanceRow": "xoziZG_balanceRow",
61
- "modelTokens": "xoziZG_modelTokens",
62
- "accountNameRow": "xoziZG_accountNameRow",
76
+ "balanceUnit": "xoziZG_balanceUnit",
77
+ "danger": "xoziZG_danger",
78
+ "choiceRow": "xoziZG_choiceRow",
79
+ "body": "xoziZG_body",
63
80
  "claimAllRow": "xoziZG_claimAllRow",
64
- "modelRow": "xoziZG_modelRow",
65
- "manualSummary": "xoziZG_manualSummary",
66
- "accountName": "xoziZG_accountName",
67
- "avatar": "xoziZG_avatar",
68
- "choices": "xoziZG_choices",
69
- "creditHint": "xoziZG_creditHint",
70
- "creditRow": "xoziZG_creditRow",
71
- "field": "xoziZG_field",
72
81
  "usageFill": "xoziZG_usageFill",
73
- "updated": "xoziZG_updated",
74
- "expiryRow": "xoziZG_expiryRow",
75
- "accountRow": "xoziZG_accountRow",
76
- "error": "xoziZG_error",
77
- "bar": "xoziZG_bar",
78
82
  "header": "xoziZG_header",
79
83
  "tabPanel": "xoziZG_tabPanel",
80
- "meta": "xoziZG_meta",
81
84
  "desc": "xoziZG_desc",
82
- "balance": "xoziZG_balance",
83
- "modelFill": "xoziZG_modelFill",
84
- "creditRows": "xoziZG_creditRows",
85
- "modelChart": "xoziZG_modelChart",
86
- "headText": "xoziZG_headText",
87
- "accountToolbar": "xoziZG_accountToolbar",
88
- "creditBlock": "xoziZG_creditBlock",
89
- "sectionLabel": "xoziZG_sectionLabel",
90
- "manualForm": "xoziZG_manualForm",
91
- "area": "xoziZG_area",
92
- "balanceMeta": "xoziZG_balanceMeta",
93
- "accountCard": "xoziZG_accountCard",
94
- "check": "xoziZG_check",
95
- "creditLink": "xoziZG_creditLink",
96
- "link": "xoziZG_link",
85
+ "card": "xoziZG_card",
97
86
  "accountEntry": "xoziZG_accountEntry",
98
- "body": "xoziZG_body",
99
- "hint": "xoziZG_hint"
87
+ "hint": "xoziZG_hint",
88
+ "accountToolbar": "xoziZG_accountToolbar",
89
+ "headText": "xoziZG_headText",
90
+ "section": "xoziZG_section",
91
+ "modelName": "xoziZG_modelName",
92
+ "name": "xoziZG_name",
93
+ "creditRows": "xoziZG_creditRows",
94
+ "modelShare": "xoziZG_modelShare",
95
+ "modelRequests": "xoziZG_modelRequests",
96
+ "choices": "xoziZG_choices",
97
+ "manualSummary": "xoziZG_manualSummary",
98
+ "avatar": "xoziZG_avatar",
99
+ "modelTokens": "xoziZG_modelTokens"
100
100
  };
101
101
  //#endregion
102
102
  //#region src/client/CodeArtsPluginCard.tsx
@@ -142,6 +142,11 @@ function claimMessagesOf(data, accountIds, t) {
142
142
  for (const id of accountIds) out[id] = claimText(data, t);
143
143
  return out;
144
144
  }
145
+ /** 开关的兜底值:后端没这条路由(老版本)时按「都显示」渲染,与原有行为一致。 */
146
+ const DOCK_DEFAULT = {
147
+ credits: true,
148
+ queue: true
149
+ };
145
150
  /** 一个 Tab 面板;未选中的用 hidden 留在 DOM 里,草稿和滚动位置都别丢。 */
146
151
  function TabPanel({ id, labelledBy, active, children }) {
147
152
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -166,7 +171,7 @@ function claimText(data, t) {
166
171
  if (data?.state === "unavailable") return t("claimUnavailable");
167
172
  return t("claimFailed", { message: typeof data?.message === "string" && data.message !== "" ? data.message : "request failed" });
168
173
  }
169
- function formatClock(ts) {
174
+ function formatClock$1(ts) {
170
175
  if (ts === void 0 || !Number.isFinite(ts)) return "";
171
176
  try {
172
177
  return new Date(ts).toLocaleTimeString([], {
@@ -204,7 +209,7 @@ function CreditBlock({ credits, error, t, onClaim, claiming, claimResult, justCl
204
209
  used: formatCredits(c.total.used),
205
210
  total: formatCredits(c.total.total)
206
211
  });
207
- const updated = formatClock(c?.fetchedAt);
212
+ const updated = formatClock$1(c?.fetchedAt);
208
213
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
209
214
  className: CodeArtsPluginCard_module_css_default.creditBlock,
210
215
  children: [
@@ -552,6 +557,56 @@ function CodeArtsPluginCard({ t }) {
552
557
  const [manualOpen, setManualOpen] = (0, react.useState)(false);
553
558
  const [tab, setTab] = (0, react.useState)("accounts");
554
559
  const tabIdBase = (0, react.useId)();
560
+ /** 对话底部那一行的两个显隐开关(后端 = 插件 settings 里的两个布尔)。 */
561
+ const [dock, setDock] = (0, react.useState)(DOCK_DEFAULT);
562
+ const [dockWritable, setDockWritable] = (0, react.useState)(false);
563
+ const [dockSaving, setDockSaving] = (0, react.useState)(false);
564
+ const [dockError, setDockError] = (0, react.useState)(null);
565
+ async function refreshDock() {
566
+ try {
567
+ const res = await fetch("/api/codearts/dock", { headers: { Accept: "application/json" } });
568
+ if (!res.ok) return;
569
+ const data = await res.json();
570
+ if (data.dock !== void 0) setDock(data.dock);
571
+ setDockWritable(data.writable === true);
572
+ } catch {}
573
+ }
574
+ /**
575
+ * 改一个开关:先按新值渲染,写失败再退回去。
576
+ *
577
+ * 这里不像模型勾选那样攒草稿再点保存 —— 只有两个布尔,攒草稿反而让用户猜
578
+ * 「我刚才那一下到底生效没有」。
579
+ */
580
+ async function toggleDock(key, on) {
581
+ const previous = dock;
582
+ setDock({
583
+ ...dock,
584
+ [key]: on
585
+ });
586
+ setDockSaving(true);
587
+ setDockError(null);
588
+ try {
589
+ const res = await fetch("/api/codearts/dock", {
590
+ method: "POST",
591
+ headers: {
592
+ "Content-Type": "application/json",
593
+ Accept: "application/json"
594
+ },
595
+ body: JSON.stringify({ [key]: on })
596
+ });
597
+ if (!res.ok) {
598
+ const data = await res.json().catch(() => ({}));
599
+ throw new Error(typeof data.error === "string" ? data.error : `HTTP ${res.status}`);
600
+ }
601
+ const data = await res.json();
602
+ if (data.dock !== void 0) setDock(data.dock);
603
+ } catch (error) {
604
+ setDock(previous);
605
+ setDockError(error instanceof Error ? error.message : String(error));
606
+ } finally {
607
+ setDockSaving(false);
608
+ }
609
+ }
555
610
  /**
556
611
  * 领每日签到积分。给 id 就只领那个号,不给就逐个号都领一遍。
557
612
  *
@@ -620,6 +675,9 @@ function CodeArtsPluginCard({ t }) {
620
675
  (0, react.useEffect)(() => {
621
676
  refreshSelection();
622
677
  }, []);
678
+ (0, react.useEffect)(() => {
679
+ refreshDock();
680
+ }, []);
623
681
  (0, react.useEffect)(() => {
624
682
  if (status?.status === "signed-in") refreshSelection();
625
683
  }, [status?.status]);
@@ -838,12 +896,19 @@ function CodeArtsPluginCard({ t }) {
838
896
  label: t("tabUsage"),
839
897
  id: tabId("usage"),
840
898
  panelId: panelId("usage")
899
+ },
900
+ display: {
901
+ value: "display",
902
+ label: t("tabDisplay"),
903
+ id: tabId("display"),
904
+ panelId: panelId("display")
841
905
  }
842
906
  };
843
907
  const tabs = [
844
908
  tabItems.accounts,
845
909
  tabItems.models,
846
- ...usageAvailable ? [tabItems.usage] : []
910
+ ...usageAvailable ? [tabItems.usage] : [],
911
+ tabItems.display
847
912
  ];
848
913
  const activeTab = tabs.some((item) => item.value === tab) ? tab : "accounts";
849
914
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1095,6 +1160,10 @@ function CodeArtsPluginCard({ t }) {
1095
1160
  choice.ratio !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1096
1161
  tone: "quiet",
1097
1162
  children: choice.ratio
1163
+ }),
1164
+ choice.supportsImages === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1165
+ tone: "quiet",
1166
+ children: t("modelImages")
1098
1167
  })
1099
1168
  ]
1100
1169
  }, choice.id))
@@ -1138,6 +1207,57 @@ function CodeArtsPluginCard({ t }) {
1138
1207
  accounts,
1139
1208
  t
1140
1209
  })
1210
+ }),
1211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TabPanel, {
1212
+ id: panelId("display"),
1213
+ labelledBy: tabId("display"),
1214
+ active: activeTab === "display",
1215
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1216
+ className: CodeArtsPluginCard_module_css_default.section,
1217
+ children: [
1218
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1219
+ className: CodeArtsPluginCard_module_css_default.sectionHead,
1220
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1221
+ className: CodeArtsPluginCard_module_css_default.sectionLabel,
1222
+ children: t("displayTitle")
1223
+ })
1224
+ }),
1225
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1226
+ className: CodeArtsPluginCard_module_css_default.hint,
1227
+ children: t("displayHint")
1228
+ }),
1229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1230
+ className: CodeArtsPluginCard_module_css_default.choices,
1231
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1232
+ className: CodeArtsPluginCard_module_css_default.choiceRow,
1233
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1234
+ className: CodeArtsPluginCard_module_css_default.check,
1235
+ checked: dock.credits,
1236
+ disabled: !dockWritable || dockSaving,
1237
+ onChange: (on) => void toggleDock("credits", on),
1238
+ label: t("displayCreditsToggle")
1239
+ })
1240
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1241
+ className: CodeArtsPluginCard_module_css_default.choiceRow,
1242
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
1243
+ className: CodeArtsPluginCard_module_css_default.check,
1244
+ checked: dock.queue,
1245
+ disabled: !dockWritable || dockSaving,
1246
+ onChange: (on) => void toggleDock("queue", on),
1247
+ label: t("displayQueueToggle")
1248
+ })
1249
+ })]
1250
+ }),
1251
+ !dockWritable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1252
+ className: CodeArtsPluginCard_module_css_default.hint,
1253
+ children: t("displayNotWritable")
1254
+ }),
1255
+ dockError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1256
+ className: CodeArtsPluginCard_module_css_default.error,
1257
+ children: t("displayFailed", { message: dockError })
1258
+ })
1259
+ ]
1260
+ })
1141
1261
  })
1142
1262
  ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1143
1263
  className: CodeArtsPluginCard_module_css_default.section,
@@ -1195,17 +1315,102 @@ function CodeArtsPluginCard({ t }) {
1195
1315
  });
1196
1316
  }
1197
1317
  //#endregion
1318
+ //#region src/client/dock-line.ts
1319
+ /** 段分隔符 —— 与邻居插件一致,改这里等于改整行的样子。 */
1320
+ const DOCK_SEGMENT_SEPARATOR = " · ";
1321
+ function buildDockLine(parts, t) {
1322
+ const rest = [];
1323
+ rest.push(t("dockProvider", { provider: parts.provider }));
1324
+ rest.push(parts.model === void 0 ? t("dockNoModel") : t("dockModel", { model: parts.model }));
1325
+ if (parts.ratio !== void 0) rest.push(parts.ratio);
1326
+ if (parts.queue !== void 0) rest.push(parts.queue);
1327
+ return {
1328
+ ...parts.credits === void 0 ? {} : { credits: parts.credits },
1329
+ rest: rest.join(DOCK_SEGMENT_SEPARATOR)
1330
+ };
1331
+ }
1332
+ //#endregion
1333
+ //#region src/client/dock-suppress.ts
1334
+ /**
1335
+ * 选中我们的模型时,把对话底部那一行里**别人**的条目暂时压掉。
1336
+ *
1337
+ * 别人注册什么我们管不了 —— `conversation.composer.dock` 是 list 槽位,每条注册由
1338
+ * 宿主平铺成同一个 `[data-composer-dock]` 行里的一个直接子节点。所以我们能做的是:
1339
+ * 在自己这条的 effect 里认出兄弟节点、给它们打个属性,再用一条我们注入的 CSS 隐藏。
1340
+ * 切走或卸载时逐个摘掉属性,绝不留下别人的行永久消失。
1341
+ *
1342
+ * 不碰的:宿主自己的轮次/token 统计 pill(带 `data-composer-stat`),以及上下文占用
1343
+ * 那一格 —— 那些不是「别人的 provider 行」,压掉就是抢宿主的 UI。
1344
+ *
1345
+ * 这里刻意不写 `instanceof HTMLElement`:结构够了就能用,逻辑也就能在没装 jsdom 的
1346
+ * 测试环境里用假节点跑(见 test/dock-suppress.spec.ts)。
1347
+ */
1348
+ /** 标记属性:带上它的节点被我们隐藏。 */
1349
+ const SUPPRESS_ATTR = "data-codearts-dock-suppressed";
1350
+ /** 注入的那条样式表 id。 */
1351
+ const SUPPRESS_STYLE_ID = "dsh-codearts-dock-suppress";
1352
+ /**
1353
+ * 这个兄弟节点该不该被压掉。
1354
+ *
1355
+ * 该压:别人的一条注册(既不是我们自己,也不包含我们,也不是宿主统计 pill)。
1356
+ * 不该压:我们自己、我们自己的子孙(有些宿主会包一层)、宿主的统计 pill。
1357
+ */
1358
+ function isForeignDockEntry(node, self) {
1359
+ if (node === self) return false;
1360
+ if (node.contains?.(self) === true) return false;
1361
+ if (node.hasAttribute("data-composer-stat")) return false;
1362
+ if (node.querySelector("[data-composer-stat]") !== null) return false;
1363
+ return true;
1364
+ }
1365
+ /**
1366
+ * 扫一遍并给新发现的「别人」打标记。返回**这次**新标记的节点,调用方负责在清理时
1367
+ * 逐个摘掉(只摘自己打的那批,别动别人的状态)。
1368
+ */
1369
+ function markForeignDockEntries(parent, self) {
1370
+ const marked = [];
1371
+ for (const child of Array.from(parent.children)) {
1372
+ if (!isForeignDockEntry(child, self)) continue;
1373
+ if (child.getAttribute("data-codearts-dock-suppressed") === "") continue;
1374
+ child.setAttribute(SUPPRESS_ATTR, "");
1375
+ marked.push(child);
1376
+ }
1377
+ return marked;
1378
+ }
1379
+ /** 摘掉标记 —— 切走 provider 或组件卸载时调用。 */
1380
+ function releaseDockEntries(nodes) {
1381
+ for (const node of nodes) node.removeAttribute(SUPPRESS_ATTR);
1382
+ }
1383
+ /** 注入一次隐藏规则;重复调用无副作用。没有 document 的环境(SSR/测试)直接跳过。 */
1384
+ function ensureSuppressStyle() {
1385
+ if (typeof document === "undefined") return;
1386
+ if (document.getElementById("dsh-codearts-dock-suppress") !== null) return;
1387
+ const style = document.createElement("style");
1388
+ style.id = SUPPRESS_STYLE_ID;
1389
+ style.textContent = `[${SUPPRESS_ATTR}]{display:none!important}`;
1390
+ document.head.appendChild(style);
1391
+ }
1392
+ //#endregion
1198
1393
  //#region src/client/CodeArtsCreditDock.tsx
1199
1394
  /**
1200
- * 对话底部的积分条。
1395
+ * 对话底部的「模型 + 积分」条,以及点开后的积分弹层。
1201
1396
  *
1202
- * 用户最初的诉求就是「对话底部要显示积分」—— 这一条挂在
1203
- * `conversation.composer.dock` 槽位上(与宿主 session-stats strip 同位)。
1204
- * 只在当前 provider 是 codearts 时才拉取并显示一行「积分 N」,别的 provider 下
1205
- * 完全不渲染,不抢别人的行。
1397
+ * 这一条挂在 `conversation.composer.dock` 槽位上(与宿主 session-stats strip 同位)。
1398
+ * 只在当前 provider 是 codearts 时才渲染,别的 provider 下完全不出现,不抢别人的行。
1399
+ *
1400
+ * 第一段是 `Provider CodeArts · <模型名>`,模型名直接用目录里的 `name`
1401
+ * (`catalog.ts` 的 `modelDisplayName` 已经把倍率写进去了,如
1402
+ * `DeepSeek-V4.1-Flash-1017 · x0.18`),所以这一行自带倍率、不必另算。
1403
+ * 这段是**我们自己的**身份行:邻居插件(如 dsh-codebuddy-cli)对任何 provider
1404
+ * 都会画一行 `Provider <id> · Model <id>`,在 codearts 下那行只有模型 id、没有
1405
+ * 倍率;两边都开就会重复,所以优先我们这条,邻居那条应在它自己的设置里关掉。
1406
+ *
1407
+ * 那一行是个按钮:点开就是逐号明细(多账号下聚合数字本身没意义,得能看出是哪个号)。
1408
+ * 显隐由设置卡片「显示」页那两个开关管着,开关值随 `/status` 一起下发 —— 底部条本来
1409
+ * 就在轮询它,不必为两个布尔开一条新链路。代价是关掉开关最多要等一个轮询周期(60 秒)
1410
+ * 才看得见效果,这个延迟在设置卡片上写明了。
1206
1411
  *
1207
1412
  * 积分数据走插件自己的 `/api/codearts/status`(带 TTL 缓存,见 src/credits.ts),
1208
- * 不会因为这条 dock 让上游多挨一次请求。
1413
+ * 不会因为这条 dock 让上游多挨一次请求;弹层上那个「刷新积分」是唯一绕缓存的出口。
1209
1414
  */
1210
1415
  /** 与 src/adapter.ts 的 CODEARTS_PROVIDER 对齐 —— client 不能 import host 模块。 */
1211
1416
  const CODEARTS_PROVIDER = "codearts";
@@ -1215,6 +1420,12 @@ const QUEUE_PATH = "/api/codearts/queue";
1215
1420
  const REFRESH_INTERVAL_MS = 6e4;
1216
1421
  /** 排队位置变得快:官方客户端也是 5 秒一次;这是本机路由,打不疼上游。 */
1217
1422
  const QUEUE_INTERVAL_MS = 5e3;
1423
+ /** 弹层宽度固定,好算它会不会顶出屏幕右边。 */
1424
+ const PANEL_WIDTH = 280;
1425
+ /** 弹层与那一行之间的缝隙。 */
1426
+ const PANEL_GAP = 8;
1427
+ /** 那一行在输入框下方,往上开才够空间;给弹层留的最小高度,不够就翻到下面。 */
1428
+ const PANEL_MIN_ABOVE = 260;
1218
1429
  function readSelection(useProjection) {
1219
1430
  if (useProjection === void 0) return void 0;
1220
1431
  try {
@@ -1223,51 +1434,138 @@ function readSelection(useProjection) {
1223
1434
  return;
1224
1435
  }
1225
1436
  }
1437
+ function currentOf(selection) {
1438
+ return selection?.next ?? selection?.lastUsed;
1439
+ }
1226
1440
  function isCodeArts(selection) {
1227
- return (selection?.next ?? selection?.lastUsed)?.provider === CODEARTS_PROVIDER;
1441
+ return currentOf(selection)?.provider === CODEARTS_PROVIDER;
1442
+ }
1443
+ function formatNumber(value) {
1444
+ return new Intl.NumberFormat().format(Math.round(value));
1445
+ }
1446
+ function formatClock(ts) {
1447
+ if (ts === void 0 || !Number.isFinite(ts)) return "";
1448
+ try {
1449
+ return new Date(ts).toLocaleTimeString([], {
1450
+ hour: "2-digit",
1451
+ minute: "2-digit"
1452
+ });
1453
+ } catch {
1454
+ return "";
1455
+ }
1456
+ }
1457
+ /** 账号名与设置卡片同一套取法:备注 > 用户名 > id。 */
1458
+ function accountName(account) {
1459
+ return account.label?.trim() || account.userName?.trim() || account.id;
1460
+ }
1461
+ /**
1462
+ * 逐号明细。
1463
+ *
1464
+ * 老后端只有聚合的一份 `credits`,那时单账号就退回它 —— 别因为后端没升级就把
1465
+ * 弹层弹成一句「没有数据」。
1466
+ */
1467
+ function rowsOf(data) {
1468
+ const byId = new Map((data.creditsByAccount ?? []).map((entry) => [entry.id, entry]));
1469
+ return (data.accounts ?? []).map((account) => {
1470
+ const entry = byId.get(account.id) ?? (byId.size === 0 && (data.accounts?.length ?? 0) === 1 ? {
1471
+ id: account.id,
1472
+ credits: data.credits,
1473
+ creditsError: data.creditsError
1474
+ } : void 0);
1475
+ const snapshot = entry?.credits;
1476
+ return {
1477
+ id: account.id,
1478
+ name: accountName(account),
1479
+ remain: snapshot?.credits.total.remain ?? null,
1480
+ expiring: snapshot?.credits.total.expiring ?? 0,
1481
+ ...snapshot?.credits.fetchedAt === void 0 ? {} : { fetchedAt: snapshot.credits.fetchedAt },
1482
+ ...entry?.creditsError === void 0 ? {} : { error: entry.creditsError }
1483
+ };
1484
+ });
1485
+ }
1486
+ /** 把弹层钉在按钮边上:优先往上开(那一行本来就在输入框下面),放不下就翻到下面。 */
1487
+ function anchorOf(rect) {
1488
+ const above = rect.top > PANEL_MIN_ABOVE;
1489
+ const width = Math.min(PANEL_WIDTH, window.innerWidth - 16);
1490
+ return {
1491
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)),
1492
+ top: above ? rect.top - PANEL_GAP : rect.bottom + PANEL_GAP,
1493
+ above
1494
+ };
1228
1495
  }
1229
1496
  function CodeArtsCreditDock({ useProjection, t }) {
1230
- const active = isCodeArts(readSelection(useProjection));
1497
+ const selection = readSelection(useProjection);
1498
+ const active = isCodeArts(selection);
1231
1499
  const [remain, setRemain] = (0, react.useState)(null);
1232
1500
  const [total, setTotal] = (0, react.useState)(0);
1501
+ const [rows, setRows] = (0, react.useState)([]);
1502
+ /**
1503
+ * 模型 id -> 目录条目(显示名 + 已格式化的倍率)。
1504
+ *
1505
+ * 显示名自带 ` · x0.18` 尾巴,倍率单独放一段,所以两个都要拿:
1506
+ * 名字用 `stripModelSuffixes` 剥掉尾巴,倍率直接用服务端算好的 `ratio`。
1507
+ */
1508
+ const [modelInfo, setModelInfo] = (0, react.useState)({});
1509
+ const [flags, setFlags] = (0, react.useState)({
1510
+ credits: true,
1511
+ queue: true
1512
+ });
1233
1513
  const [queued, setQueued] = (0, react.useState)(null);
1514
+ const [anchor, setAnchor] = (0, react.useState)(null);
1515
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
1234
1516
  const mounted = (0, react.useRef)(true);
1517
+ const triggerRef = (0, react.useRef)(null);
1518
+ const panelRef = (0, react.useRef)(null);
1519
+ /** 这一行的根节点 —— 它的 parentElement 就是宿主那条共享 flex 行。 */
1520
+ const rootRef = (0, react.useRef)(null);
1235
1521
  (0, react.useEffect)(() => {
1236
1522
  mounted.current = true;
1237
1523
  return () => {
1238
1524
  mounted.current = false;
1239
1525
  };
1240
1526
  }, []);
1527
+ const loadStatus = (0, react.useCallback)(async (force) => {
1528
+ try {
1529
+ const res = await fetch(force ? `${STATUS_PATH}?force=1` : STATUS_PATH, {
1530
+ headers: { Accept: "application/json" },
1531
+ credentials: "same-origin"
1532
+ });
1533
+ if (!res.ok) return;
1534
+ const data = await res.json().catch(() => void 0);
1535
+ if (data === void 0 || !mounted.current) return;
1536
+ setFlags(data.dock ?? {
1537
+ credits: true,
1538
+ queue: true
1539
+ });
1540
+ setRows(rowsOf(data));
1541
+ setModelInfo(Object.fromEntries((data.models ?? []).map((model) => [model.id, {
1542
+ name: model.name,
1543
+ ...model.ratio === void 0 ? {} : { ratio: model.ratio }
1544
+ }])));
1545
+ const snapshots = (data.creditsByAccount ?? []).map((entry) => entry.credits).filter((value) => value !== void 0);
1546
+ const pool = snapshots.length > 0 ? snapshots : data.credits === void 0 ? [] : [data.credits];
1547
+ if (pool.length === 0) {
1548
+ setRemain(null);
1549
+ return;
1550
+ }
1551
+ setRemain(Math.round(pool.reduce((sum, item) => sum + item.credits.total.remain, 0)));
1552
+ setTotal(Math.round(pool.reduce((sum, item) => sum + item.credits.total.total, 0)));
1553
+ } catch {}
1554
+ }, []);
1241
1555
  (0, react.useEffect)(() => {
1242
1556
  if (!active) {
1243
1557
  setRemain(null);
1558
+ setRows([]);
1559
+ setAnchor(null);
1244
1560
  return;
1245
1561
  }
1246
1562
  let timer;
1247
- const refresh = async () => {
1248
- try {
1249
- const res = await fetch(STATUS_PATH, {
1250
- headers: { Accept: "application/json" },
1251
- credentials: "same-origin"
1252
- });
1253
- if (!res.ok) return;
1254
- const data = await res.json().catch(() => void 0);
1255
- if (data === void 0) return;
1256
- const snapshots = (data.creditsByAccount ?? []).map((entry) => entry.credits).filter((value) => value !== void 0);
1257
- const pool = snapshots.length > 0 ? snapshots : data.credits === void 0 ? [] : [data.credits];
1258
- if (pool.length === 0) return;
1259
- if (mounted.current) {
1260
- setRemain(Math.round(pool.reduce((sum, item) => sum + item.credits.total.remain, 0)));
1261
- setTotal(Math.round(pool.reduce((sum, item) => sum + item.credits.total.total, 0)));
1262
- }
1263
- } catch {}
1264
- };
1265
- refresh();
1266
- timer = window.setInterval(refresh, REFRESH_INTERVAL_MS);
1563
+ loadStatus(false);
1564
+ timer = window.setInterval(() => void loadStatus(false), REFRESH_INTERVAL_MS);
1267
1565
  return () => {
1268
1566
  if (timer !== void 0) window.clearInterval(timer);
1269
1567
  };
1270
- }, [active]);
1568
+ }, [active, loadStatus]);
1271
1569
  (0, react.useEffect)(() => {
1272
1570
  if (!active) {
1273
1571
  setQueued(null);
@@ -1291,21 +1589,96 @@ function CodeArtsCreditDock({ useProjection, t }) {
1291
1589
  if (timer !== void 0) window.clearInterval(timer);
1292
1590
  };
1293
1591
  }, [active]);
1294
- if (!active || remain === null && queued === null) return null;
1295
- const text = remain === null ? void 0 : t("creditsDock", { remain: new Intl.NumberFormat().format(remain) });
1296
- const title = queued !== null ? t("queueDockTitle", {
1592
+ const close = (0, react.useCallback)(() => setAnchor(null), []);
1593
+ (0, react.useEffect)(() => {
1594
+ if (anchor === null) return;
1595
+ const onPointerDown = (event) => {
1596
+ const target = event.target;
1597
+ if (target === null) return;
1598
+ if (panelRef.current?.contains(target) === true || triggerRef.current?.contains(target) === true) return;
1599
+ close();
1600
+ };
1601
+ const onKeyDown = (event) => {
1602
+ if (event.key === "Escape") close();
1603
+ };
1604
+ window.addEventListener("pointerdown", onPointerDown, true);
1605
+ window.addEventListener("keydown", onKeyDown);
1606
+ window.addEventListener("resize", close);
1607
+ window.addEventListener("scroll", close, true);
1608
+ return () => {
1609
+ window.removeEventListener("pointerdown", onPointerDown, true);
1610
+ window.removeEventListener("keydown", onKeyDown);
1611
+ window.removeEventListener("resize", close);
1612
+ window.removeEventListener("scroll", close, true);
1613
+ };
1614
+ }, [anchor, close]);
1615
+ (0, react.useEffect)(() => {
1616
+ if (!active) return;
1617
+ const self = rootRef.current;
1618
+ const slot = self?.closest("[data-slot=\"conversation.composer.dock\"]") ?? null;
1619
+ if (self === null || slot === null) return;
1620
+ const node = slot;
1621
+ const me = self;
1622
+ const marked = /* @__PURE__ */ new Set();
1623
+ const sweep = () => {
1624
+ for (const found of markForeignDockEntries(node, me)) marked.add(found);
1625
+ };
1626
+ ensureSuppressStyle();
1627
+ sweep();
1628
+ const observer = new MutationObserver(sweep);
1629
+ observer.observe(slot, { childList: true });
1630
+ return () => {
1631
+ observer.disconnect();
1632
+ releaseDockEntries(marked);
1633
+ marked.clear();
1634
+ };
1635
+ }, [active]);
1636
+ if (!active) return null;
1637
+ const showCredits = flags.credits && remain !== null;
1638
+ const showQueue = flags.queue && queued !== null;
1639
+ const modelId = currentOf(selection)?.model;
1640
+ const entry = modelId === void 0 ? void 0 : modelInfo[modelId];
1641
+ const modelLabel = entry === void 0 ? modelId ?? "" : stripModelSuffixes(entry.name);
1642
+ const ratio = entry?.ratio;
1643
+ const line = buildDockLine({
1644
+ ...showCredits ? { credits: t("creditsDock", { remain: formatNumber(remain ?? 0) }) } : {},
1645
+ provider: "CodeArts",
1646
+ ...modelLabel === "" ? {} : { model: modelLabel },
1647
+ ...ratio === void 0 ? {} : { ratio },
1648
+ ...showQueue && queued !== null ? { queue: t("queueDock", { position: queued.position }) } : {}
1649
+ }, t);
1650
+ const queueTitle = queued === null ? void 0 : t("queueDockTitle", {
1297
1651
  model: queued.model,
1298
1652
  position: queued.position,
1299
1653
  length: queued.length
1300
- }) : t("creditsDockTitle", {
1654
+ });
1655
+ const creditsTitle = showCredits ? t("creditsDockTitle", {
1301
1656
  remain: remain ?? 0,
1302
1657
  total
1303
- });
1658
+ }) : void 0;
1659
+ const fetchedAt = rows.reduce((latest, row) => Math.max(latest, row.fetchedAt ?? 0), 0);
1660
+ const lastUpdated = fetchedAt > 0 ? formatClock(fetchedAt) : "";
1661
+ const toggle = (event) => {
1662
+ if (anchor !== null) {
1663
+ close();
1664
+ return;
1665
+ }
1666
+ setAnchor(anchorOf(event.currentTarget.getBoundingClientRect()));
1667
+ };
1668
+ const forceRefresh = async () => {
1669
+ setRefreshing(true);
1670
+ try {
1671
+ await loadStatus(true);
1672
+ } finally {
1673
+ setRefreshing(false);
1674
+ }
1675
+ };
1304
1676
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1677
+ ref: rootRef,
1305
1678
  style: {
1306
1679
  display: "inline-flex",
1307
1680
  alignItems: "center",
1308
- gap: 6,
1681
+ gap: 0,
1309
1682
  color: "var(--dsw-alias-label-tertiary)",
1310
1683
  fontSize: "var(--dsh-content-font-size-secondary, 13px)",
1311
1684
  lineHeight: "18px",
@@ -1314,8 +1687,142 @@ function CodeArtsCreditDock({ useProjection, t }) {
1314
1687
  flex: "0 0 auto",
1315
1688
  minWidth: "max-content"
1316
1689
  },
1317
- title,
1318
- children: [queued !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("queueDock", { position: queued.position }) }), text !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text })]
1690
+ title: queueTitle ?? creditsTitle,
1691
+ children: [
1692
+ line.credits !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1693
+ ref: triggerRef,
1694
+ type: "button",
1695
+ "aria-haspopup": "dialog",
1696
+ "aria-expanded": anchor !== null,
1697
+ "aria-label": t("creditsDockPanelTitle"),
1698
+ title: creditsTitle,
1699
+ onClick: toggle,
1700
+ style: {
1701
+ all: "unset",
1702
+ cursor: "pointer",
1703
+ font: "inherit",
1704
+ color: "inherit",
1705
+ fontVariantNumeric: "inherit",
1706
+ lineHeight: "inherit",
1707
+ whiteSpace: "inherit"
1708
+ },
1709
+ children: line.credits
1710
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1711
+ "aria-hidden": "true",
1712
+ children: " · "
1713
+ })] }),
1714
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: line.rest }),
1715
+ anchor !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1716
+ ref: panelRef,
1717
+ role: "dialog",
1718
+ "aria-label": t("creditsDockPanelTitle"),
1719
+ style: {
1720
+ position: "fixed",
1721
+ left: anchor.left,
1722
+ top: anchor.top,
1723
+ transform: anchor.above ? "translateY(-100%)" : void 0,
1724
+ width: PANEL_WIDTH,
1725
+ maxWidth: "calc(100vw - 16px)",
1726
+ zIndex: 1e3,
1727
+ display: "flex",
1728
+ flexDirection: "column",
1729
+ gap: 8,
1730
+ padding: 12,
1731
+ boxSizing: "border-box",
1732
+ borderRadius: "var(--dsw-radius-lg, 10px)",
1733
+ border: "0.5px solid var(--dsw-alias-settings-card-stroke, rgba(127, 127, 127, 0.28))",
1734
+ background: "var(--dsw-alias-settings-card-fill, rgba(32, 32, 32, 0.96))",
1735
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.28)",
1736
+ color: "var(--dsw-alias-label-primary)",
1737
+ fontSize: "var(--dsh-content-font-size-secondary, 13px)",
1738
+ lineHeight: "18px",
1739
+ whiteSpace: "normal"
1740
+ },
1741
+ children: [
1742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1743
+ style: {
1744
+ display: "flex",
1745
+ alignItems: "center",
1746
+ justifyContent: "space-between",
1747
+ gap: 8
1748
+ },
1749
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1750
+ style: { fontWeight: 600 },
1751
+ children: t("creditsDockPanelTitle")
1752
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1753
+ variant: "outline",
1754
+ size: "sm",
1755
+ disabled: refreshing,
1756
+ onClick: () => void forceRefresh(),
1757
+ children: refreshing ? t("creditsDockRefreshing") : t("creditsDockRefresh")
1758
+ })]
1759
+ }),
1760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1761
+ style: {
1762
+ display: "grid",
1763
+ gridTemplateColumns: "minmax(0, 1fr) auto",
1764
+ gap: "4px 12px",
1765
+ alignItems: "center",
1766
+ borderTop: "0.5px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, 0.22))",
1767
+ paddingTop: 8,
1768
+ color: "var(--dsw-alias-label-tertiary)"
1769
+ },
1770
+ children: [
1771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("creditsDockColAccount") }),
1772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1773
+ style: { justifySelf: "end" },
1774
+ children: t("creditsDockColBalance")
1775
+ }),
1776
+ rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1777
+ style: { gridColumn: "1 / -1" },
1778
+ children: t("creditsDockEmpty")
1779
+ }),
1780
+ rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1781
+ style: { display: "contents" },
1782
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1783
+ style: {
1784
+ display: "flex",
1785
+ alignItems: "center",
1786
+ gap: 6,
1787
+ minWidth: 0
1788
+ },
1789
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: row.remain === null ? "warning" : "done" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1790
+ style: {
1791
+ overflow: "hidden",
1792
+ textOverflow: "ellipsis",
1793
+ whiteSpace: "nowrap",
1794
+ color: "var(--dsw-alias-label-primary)"
1795
+ },
1796
+ title: row.error === void 0 ? row.name : row.error,
1797
+ children: row.name
1798
+ })]
1799
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1800
+ style: {
1801
+ justifySelf: "end",
1802
+ fontVariantNumeric: "tabular-nums"
1803
+ },
1804
+ children: row.remain === null ? "—" : formatNumber(row.remain)
1805
+ })]
1806
+ }, row.id))
1807
+ ]
1808
+ }),
1809
+ rows.filter((row) => row.expiring > 0).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1810
+ style: {
1811
+ margin: 0,
1812
+ color: "var(--dsw-alias-state-warn-label, var(--dsw-alias-label-secondary))"
1813
+ },
1814
+ children: t("creditsDockExpiring", {
1815
+ account: row.name,
1816
+ expiring: formatNumber(row.expiring)
1817
+ })
1818
+ }, `${row.id}-expiring`)),
1819
+ lastUpdated !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1820
+ style: { color: "var(--dsw-alias-label-tertiary)" },
1821
+ children: t("creditsDockUpdated", { time: lastUpdated })
1822
+ })
1823
+ ]
1824
+ })
1825
+ ]
1319
1826
  });
1320
1827
  }
1321
1828
  //#endregion
@@ -1360,6 +1867,7 @@ const zh = {
1360
1867
  benefit: "福利",
1361
1868
  benefitBlocked: "福利今日不可用",
1362
1869
  benefitBlockedHint: "{n} 个福利模型今天被上游判定没有权益(活动领完或渠道未开),发出去会直接报 benefit not found。",
1870
+ modelImages: "识图",
1363
1871
  refreshModels: "刷新模型列表",
1364
1872
  refreshing: "刷新中…",
1365
1873
  refreshed: "已刷新:{n} 个模型,其中 {benefit} 个走福利路由",
@@ -1405,6 +1913,24 @@ const zh = {
1405
1913
  creditsNeedSignIn: "这个账号需要重新登录",
1406
1914
  creditsDock: "积分 {remain}",
1407
1915
  creditsDockTitle: "CodeArts 剩余积分 {remain} / {total}",
1916
+ creditsDockPanelTitle: "CodeArts 积分",
1917
+ creditsDockRefresh: "刷新积分",
1918
+ creditsDockRefreshing: "刷新中…",
1919
+ creditsDockColAccount: "账号",
1920
+ creditsDockColBalance: "剩余积分",
1921
+ creditsDockEmpty: "暂时没有积分数据",
1922
+ creditsDockExpiring: "{account}:{expiring} 积分将在本周期结束时作废",
1923
+ creditsDockUpdated: "最近刷新 {time}",
1924
+ dockProvider: "Provider {provider}",
1925
+ dockModel: "Model {model}",
1926
+ dockNoModel: "未选择模型",
1927
+ tabDisplay: "显示",
1928
+ displayTitle: "对话底部显示",
1929
+ displayHint: "这两项只管输入框下方那一行的积分与排队两段;中间那段「Provider CodeArts · Model 模型 · 倍率」一直显示,不受开关影响。底部那条每 60 秒读一次设置,所以关掉最多要等一分钟才见效。",
1930
+ displayCreditsToggle: "显示剩余积分(点开可看各账号明细)",
1931
+ displayQueueToggle: "显示上游排队位置",
1932
+ displayNotWritable: "这个宿主没给插件开配置存储,开关只能看不能改。",
1933
+ displayFailed: "保存失败:{message}",
1408
1934
  queueDock: "排队第 {position} 位",
1409
1935
  queueDockTitle: "上游排队:{model} · 第 {position} 位 / 共 {length} 条",
1410
1936
  claimButton: "签到领积分",
@@ -1457,6 +1983,7 @@ const en = {
1457
1983
  benefit: "Benefit",
1458
1984
  benefitBlocked: "Benefit unavailable today",
1459
1985
  benefitBlockedHint: "{n} benefit model(s) were told they have no entitlement today (campaign exhausted or channel not opened); sending to them fails with benefit not found.",
1986
+ modelImages: "Images",
1460
1987
  refreshModels: "Refresh models",
1461
1988
  refreshing: "Refreshing…",
1462
1989
  refreshed: "Refreshed: {n} models, {benefit} on the benefit route",
@@ -1502,6 +2029,24 @@ const en = {
1502
2029
  creditsNeedSignIn: "This account needs signing in again",
1503
2030
  creditsDock: "Credits {remain}",
1504
2031
  creditsDockTitle: "CodeArts credits left {remain} / {total}",
2032
+ creditsDockPanelTitle: "CodeArts credits",
2033
+ creditsDockRefresh: "Refresh credits",
2034
+ creditsDockRefreshing: "Refreshing…",
2035
+ creditsDockColAccount: "Account",
2036
+ creditsDockColBalance: "Credits left",
2037
+ creditsDockEmpty: "No credit data yet",
2038
+ creditsDockExpiring: "{account}: {expiring} credits expire when this cycle ends",
2039
+ creditsDockUpdated: "Last updated {time}",
2040
+ dockProvider: "Provider {provider}",
2041
+ dockModel: "Model {model}",
2042
+ dockNoModel: "No model selected",
2043
+ tabDisplay: "Display",
2044
+ displayTitle: "Under the chat input box",
2045
+ displayHint: "These two only control the credits and queue segments of that line under the input box; the middle “Provider CodeArts · Model … · rate” segments always show. The bar re-reads settings every 60 seconds, so hiding the rest can take up to a minute.",
2046
+ displayCreditsToggle: "Show remaining credits (click it for per-account details)",
2047
+ displayQueueToggle: "Show the upstream queue position",
2048
+ displayNotWritable: "This host gives the plugin nowhere to persist settings, so the switches are read-only.",
2049
+ displayFailed: "Failed to save: {message}",
1505
2050
  queueDock: "Queued #{position}",
1506
2051
  queueDockTitle: "Upstream queue: {model} · position {position} of {length}",
1507
2052
  claimButton: "Claim daily credits",