@y9931666/dsh-plugin-crossfire 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/lib/client.js ADDED
@@ -0,0 +1,3035 @@
1
+ window.__ModuleLoader__.load({ id: "@y9931666/dsh-plugin-crossfire", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ //#region rolldown:runtime
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
+ value: mod,
22
+ enumerable: true
23
+ }) : target, mod));
24
+
25
+ //#endregion
26
+ let react = require("react");
27
+ react = __toESM(react);
28
+ let react_jsx_runtime = require("react/jsx-runtime");
29
+ react_jsx_runtime = __toESM(react_jsx_runtime);
30
+
31
+ //#region src/client/wire.ts
32
+ /** 插件自有 RPC 路由。必须与 host `rpc.ts` 的 `RPC_ROUTE` 逐字一致。 */
33
+ const RPC_ROUTE = "/plugins/dsh-plugin-crossfire/rpc";
34
+ /**
35
+ * POST one RPC call to the plugin's OWN web route.
36
+ *
37
+ * 这条路由与(批 3 起的)快照路由挂在同一个 `webServer` 注册上,因此即使宿主
38
+ * 的通用 connection 通道没有挂载,浏览器也能到达它。不可达或格式错误一律返回
39
+ * 失败包封,而不是让 promise reject —— 调用方不必写 try/catch。
40
+ */
41
+ async function callRpc(endpoint, payload, signal) {
42
+ try {
43
+ const response = await fetch(RPC_ROUTE, {
44
+ method: "POST",
45
+ headers: { "content-type": "application/json" },
46
+ body: JSON.stringify({
47
+ endpoint,
48
+ payload
49
+ }),
50
+ cache: "no-store",
51
+ ...signal === void 0 ? {} : { signal }
52
+ });
53
+ const envelope = await response.json();
54
+ if (envelope === null || typeof envelope !== "object" || typeof envelope.ok !== "boolean") return {
55
+ ok: false,
56
+ error: {
57
+ code: "internal",
58
+ message: `crossfire rpc route returned ${response.status}`
59
+ }
60
+ };
61
+ return envelope;
62
+ } catch (error) {
63
+ return {
64
+ ok: false,
65
+ error: {
66
+ code: "internal",
67
+ message: error instanceof Error ? error.message : String(error)
68
+ }
69
+ };
70
+ }
71
+ }
72
+ /** 快照路由(GET,卡片轮询它)。必须与 host `rpc.ts` 的 `STATE_ROUTE` 一致。 */
73
+ const STATE_ROUTE = "/plugins/dsh-plugin-crossfire/state";
74
+ /** 拉一次评审快照列表。失败一律返回空列表(卡片宁可空着也不该崩)。 */
75
+ async function fetchReviews(session, signal) {
76
+ try {
77
+ const query = session === "" ? "" : `?session=${encodeURIComponent(session)}`;
78
+ const response = await fetch(`${STATE_ROUTE}${query}`, {
79
+ cache: "no-store",
80
+ ...signal === void 0 ? {} : { signal }
81
+ });
82
+ const body = await response.json();
83
+ if (body === null || typeof body !== "object" || !Array.isArray(body.reviews)) return {
84
+ reviews: [],
85
+ note: `state route returned ${response.status}`
86
+ };
87
+ return body;
88
+ } catch (error) {
89
+ return {
90
+ reviews: [],
91
+ note: error instanceof Error ? error.message : String(error)
92
+ };
93
+ }
94
+ }
95
+
96
+ //#endregion
97
+ //#region src/client/brand.ts
98
+ /**
99
+ * Provider 品牌标识(彩色圆角块 + 缩写)—— 零资源依赖。
100
+ *
101
+ * ## 为什么不用真 logo
102
+ *
103
+ * 圆桌会议的做法是 `PROVIDER_BRAND`(正则 → key)+ `brand-logos.generated.ts`
104
+ * (**330 KB 的 base64 图片**)。本插件刻意**不复制那个文件**:它会把包体积撑大
105
+ * 约三倍,而收益只是"块里换成图片"。这里只取同一份方案的**缩写 + 品牌色**:
106
+ * 品牌色圆角块 + 2–3 字母缩写,代价为零。
107
+ *
108
+ * ## 一条必须守住的边界
109
+ *
110
+ * 品牌色是**第三方商标色**,不是主题令牌 —— 主题令牌无法表达"DeepSeek 是蓝的"。
111
+ * 因此它只用于**小面积的身份标记**(头像块),文字一律保持 `currentColor`:
112
+ * 浅色/深色主题下可读性由底色与白字的对比度保证,与主题无关。
113
+ * 除此之外本模块不产生任何颜色。
114
+ *
115
+ * 与圆桌 `RoundTableView.tsx` 的 `PROVIDER_BRAND` 保持同源,但**不共享代码**:
116
+ * 两个插件各自独立演进,跨插件 import 会把一份 1888 行的客户端模块拖进本包的
117
+ * 类型程序(`tsconfig.client.json` 只 include `src/client`)。
118
+ *
119
+ * @module dsh-plugin-crossfire/client/brand
120
+ */
121
+ /** 品牌色前的 `#` 省略写法留在这里,便于统一校验(全部为 6 位十六进制)。 */
122
+ const HEX = "#";
123
+ /**
124
+ * Provider → 品牌。顺序即优先级,**首个命中即返回**。
125
+ *
126
+ * 正则刻意写宽:宿主的 provider id 形态不统一(`deepseek-official`、
127
+ * `zai`、`glm`、`anthropic`…),宽匹配比精确表更耐用。
128
+ */
129
+ const PROVIDER_BRAND = [
130
+ {
131
+ match: /deepseek/i,
132
+ abbr: "DS",
133
+ color: `${HEX}4D6BFE`
134
+ },
135
+ {
136
+ match: /zhipu|glm|z\.ai|智谱/i,
137
+ abbr: "GLM",
138
+ color: `${HEX}3859FF`
139
+ },
140
+ {
141
+ match: /\bzai\b|zai\//i,
142
+ abbr: "ZAI",
143
+ color: `${HEX}3859FF`
144
+ },
145
+ {
146
+ match: /openai|gpt/i,
147
+ abbr: "GPT",
148
+ color: `${HEX}10A37F`
149
+ },
150
+ {
151
+ match: /anthropic|claude/i,
152
+ abbr: "CLD",
153
+ color: `${HEX}D97757`
154
+ },
155
+ {
156
+ match: /qwen|通义/i,
157
+ abbr: "QW",
158
+ color: `${HEX}6E56CF`
159
+ },
160
+ {
161
+ match: /moonshot|kimi/i,
162
+ abbr: "KM",
163
+ color: `${HEX}16181D`
164
+ },
165
+ {
166
+ match: /gemini|google/i,
167
+ abbr: "GM",
168
+ color: `${HEX}4285F4`
169
+ },
170
+ {
171
+ match: /minimax/i,
172
+ abbr: "MX",
173
+ color: `${HEX}0A0A0A`
174
+ }
175
+ ];
176
+ /** 未知 provider 的中性底色(与 `--dsw-alias-state-idle-*` 同调)。 */
177
+ const NEUTRAL_COLOR = `${HEX}8A8A8A`;
178
+ /**
179
+ * provider 名 → 品牌标识。空 / 未识别时退化为中性块。
180
+ *
181
+ * 未识别的兜底与圆桌一致:取名称里的字母数字前两位大写;一个字符都没有时(例如
182
+ * provider 为 `***`)用 `?`,宁可不说话也不编一个假缩写。
183
+ */
184
+ function providerBrand(provider) {
185
+ if (provider === void 0 || provider.trim() === "") return {
186
+ abbr: "?",
187
+ color: NEUTRAL_COLOR
188
+ };
189
+ for (const brand of PROVIDER_BRAND) if (brand.match.test(provider)) return {
190
+ abbr: brand.abbr,
191
+ color: brand.color
192
+ };
193
+ const abbr = provider.replace(/[^a-zA-Z0-9]/g, "").slice(0, 2).toUpperCase();
194
+ return abbr === "" ? {
195
+ abbr: "?",
196
+ color: NEUTRAL_COLOR
197
+ } : {
198
+ abbr,
199
+ color: NEUTRAL_COLOR
200
+ };
201
+ }
202
+ /**
203
+ * provider + model 的**路由标记**缩写。
204
+ *
205
+ * 刻意**只返回 provider 的缩写**:这个块画的是 provider 的品牌色,若里面写的是模型名
206
+ * 的首字母(`deepseek-v4-flash` → `DEFL`),一个颜色块就同时表达了两个互相矛盾的身份
207
+ * —— 用户会以为 `DEFL` 是某个 provider。块的语义必须单一:**底色与字母都只属于
208
+ * provider**;模型是哪一档由块右边那行完整路由文本回答(`routeText`)。
209
+ *
210
+ * 参数收下 `model` 只为了在模型缺失时也能给出同一个标记(两者行为一致),
211
+ * 保留它是为了让调用点读起来是"给一个路由",而不是"给一个 provider"。
212
+ */
213
+ function routeAbbr(provider, model) {
214
+ return providerBrand(provider).abbr;
215
+ }
216
+ /**
217
+ * provider + model 的完整可读串(`deepseek-official / deepseek-v4-flash`)。
218
+ *
219
+ * 缩写块有 `title`,但 `title` 在触屏上读不到 —— 列表行因此同时给出完整路由文本。
220
+ */
221
+ function routeText(provider, model) {
222
+ return [provider, model].filter((part) => part !== void 0 && part.trim() !== "").join(" / ");
223
+ }
224
+
225
+ //#endregion
226
+ //#region \0dsh-css:F:\AI\DSH\1changyong\dsh-plugin-crossfire\src\client\ReviewCard.module.css.mjs
227
+ const css$1 = "/*\n * Crossfire Chat card styles.\n *\n * 硬约束:**每个颜色都来自主题令牌**(`Theme.dsefkao_listTokens` 的 `--dsw-alias-*` 一族),\n * 所以浅色与深色主题都无需自己写 override。圆桌的设置页 CSS 用了硬编码\n * `#ffffff` / `rgba(0,0,0,.8)`,在深色主题下是坏的 —— 这里不重复那个错误。\n */\n\n.dsefkao_root {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.dsefkao_card {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dsefkao_card + .dsefkao_card {\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px dashed var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* 折叠的评审:一行摘要,点一下切换到它。卡片会列出本会话的全部评审,全展开\n 就是一堵墙(验收期间真实攒到 5 场、12 条缺陷)。 */\n.dsefkao_collapsed {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin-top: 6px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.dsefkao_collapsed:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n.dsefkao_header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_title {\n font-weight: 700;\n font-size: 13px;\n letter-spacing: 0.02em;\n}\n\n.dsefkao_subjectTitle {\n font-weight: 600;\n}\n\n.dsefkao_badge {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_badge_draft {\n border-color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.5));\n}\n\n.dsefkao_badge_attacking {\n border-color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_badge_judging {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-brand-primary, inherit);\n}\n\n.dsefkao_badge_settled {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n}\n\n/* ---- 状态区:当前处于哪一态 + 下一步该做什么(唯一主行动) ----------------\n *\n * 这是卡片上**唯一**的强对比区域,其余段落一律保持低对比(标题灰、正文常规)。\n * 主行动是这一区里唯一的主色实心按钮,所以\"现在该干什么\"不需要读完全卡去找。\n */\n.dsefkao_statusStrip {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* 左侧信息块:态名 + 轮次 + 一句话说明;吃掉剩余宽度,主行动因此被推到右侧。 */\n.dsefkao_statusStripInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1 1 240px;\n min-width: 0;\n}\n\n.dsefkao_statusStripHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 态名用反色实心药丸:它与主行动按钮同色,一眼就能配对(\"现在在这里 → 下一步在那里\")。 */\n.dsefkao_statusState {\n font-size: 12px;\n font-weight: 600;\n padding: 2px 10px;\n border-radius: 999px;\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_statusHint {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 右侧行动块:一个主按钮 + 一行\"下一步\"小标题。 */\n.dsefkao_statusStripAction {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin-left: auto;\n}\n\n.dsefkao_statusNextLabel {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 主行动:唯一的主色实心按钮。字号与内边距都比次要按钮大一档,视觉权重随之提高。 */\n.dsefkao_cta {\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n padding: 7px 18px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_cta:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_statusError {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n word-break: break-word;\n}\n\n.dsefkao_statusOk {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n word-break: break-word;\n}\n\n.dsefkao_section {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 底色是**第三方商标色**(深色主题下也一样),文字固定白色 —— 这是本插件里唯一\n * 不使用主题令牌的颜色。面积被刻意压到 18px 见方:它是身份标记,不是信息主体,\n * 主题(浅色/深色)不影响其可读性。\n */\n.dsefkao_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.dsefkao_sectionTitle {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_subject,\n.dsefkao_stance {\n white-space: pre-wrap;\n word-break: break-word;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_stance {\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n/* 立场编辑框:左侧那根品牌色竖线是刻意的 —— 它是\"锋\"的视觉标记,提醒用户\n 这段文字会被红队逐条对攻。 */\n.dsefkao_stanceInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_stanceInput:disabled {\n opacity: 0.7;\n}\n\n.dsefkao_chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n}\n\n.dsefkao_chip {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_chipWarn {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_chipStrike {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.6));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_expertRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 2px 0;\n}\n\n.dsefkao_expertName {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_impactBar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n font-size: 12px;\n}\n\n.dsefkao_countEndorsed {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n font-weight: 600;\n}\n\n.dsefkao_countRejected {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n font-weight: 600;\n}\n\n.dsefkao_countPending {\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_countStrike {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_defect {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_defectHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_quote {\n font-size: 12px;\n padding-left: 8px;\n border-left: 2px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_defectContent {\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidence {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidenceLabel {\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 复审核对区:用虚线框与\"待裁决\"的实线框区分开 —— 它问的是另一个问题\n (修没修),而不是\"这条缺陷成不成立\"。 */\n.dsefkao_recheckBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px dashed var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n}\n\n.dsefkao_recheckHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_rejectReason {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_btnPrimary,\n.dsefkao_btnPrimaryOn,\n.dsefkao_btnDanger,\n.dsefkao_btnDangerOn,\n.dsefkao_btnGhost {\n font: inherit;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_btnPrimaryOn {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnDangerOn {\n border-color: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnGhost {\n border-color: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_btnPrimary:disabled,\n.dsefkao_btnPrimaryOn:disabled,\n.dsefkao_btnDanger:disabled,\n.dsefkao_btnDangerOn:disabled,\n.dsefkao_btnGhost:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_rejectBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.4));\n}\n\n.dsefkao_rejectPrompt {\n font-size: 12px;\n font-weight: 600;\n}\n\n.dsefkao_rejectInput {\n font: inherit;\n font-size: 12px;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_link {\n align-self: flex-start;\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary, #4a6cf7);\n cursor: pointer;\n}\n\n.dsefkao_footer {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.25));\n}\n\n.dsefkao_footerNote {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_muted {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_warn {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_notice {\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.5));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_empty {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ==========================================================================\n * 用户反馈驱动的视觉改造(分区 / 三色表态 / 专家身份 / 可关闭说明 / 折叠按钮)\n *\n * 核心目标:**次要信息让位,主要信息抓得住**。手段只有三种 —— 分隔线、颜色语义、\n * 按钮形状。刻意不引入背景色块堆叠:那只会让\"一堵墙\"变成\"一堵彩色的墙\"。\n * 颜色仍然全部来自主题令牌(品牌色块是唯一例外,见 brand.dsefkao_ts)。\n * ========================================================================== */\n\n/* ---- 分区:带竖线的小标题 + 硬分隔线 ---- */\n\n.dsefkao_sectionDivider {\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.28));\n padding-top: 10px;\n margin-top: 2px;\n}\n\n.dsefkao_sectionHeadRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 分区标题:左侧品牌色竖线 + 加粗,成为扫读时的视觉锚点。 */\n.dsefkao_sectionTitleStrong {\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n padding-left: 8px;\n border-left: 3px solid var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_sectionCount {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 展开 / 折叠:做成按钮,不要看起来像正文链接 ---- */\n\n.dsefkao_foldButton {\n font: inherit;\n font-size: 11px;\n padding: 2px 10px;\n border-radius: 999px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_foldButton:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 可关闭的解释性说明 ---- */\n\n.dsefkao_explain {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainText {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.dsefkao_explainClose {\n flex: none;\n font: inherit;\n font-size: 11px;\n line-height: 1;\n padding: 3px 7px;\n border-radius: 6px;\n cursor: pointer;\n border: 1px solid transparent;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainClose:hover {\n border-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 专家:头像 + 名称 + 维度 + 状态 ---- */\n\n.dsefkao_expertCard {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_expertHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 专家名用正文色与加粗:它是\"这条观点是谁提的\"的答案,必须比元数据更显眼。 */\n.dsefkao_expertNameStrong {\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 缺陷子卡抬头里的作者标识(头像 + 名称)。 */\n.dsefkao_defectAuthor {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 三态表态:绿 / 红 / 灰 -----------------------------------------------\n *\n * 用户要求:三个都做成按钮形状并各对应颜色;点中之后整块填充该颜色、文字变白。\n * 未点中 = 该颜色的描边 + 同色文字(颜色语义仍在,但不抢主行动的视觉权重)。\n */\n.dsefkao_verdictButton {\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 8px;\n cursor: pointer;\n background: transparent;\n}\n\n.dsefkao_verdictEndorse {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.dsefkao_verdictEndorseOn {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n background: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictReject {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_verdictRejectOn {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictPending {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.5));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_verdictPendingOn {\n border: 1px solid var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n background: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictButton:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n/* ---- 立场全貌(可滚动) ---- */\n\n.dsefkao_stanceFull {\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 320px;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* ---- 角色定位编辑器 ---- */\n\n.dsefkao_roleEditor {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.dsefkao_roleInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n";
228
+ const classMap$1 = {
229
+ "listTokens": "dsefkao_listTokens",
230
+ "root": "dsefkao_root",
231
+ "card": "dsefkao_card",
232
+ "collapsed": "dsefkao_collapsed",
233
+ "header": "dsefkao_header",
234
+ "title": "dsefkao_title",
235
+ "subjectTitle": "dsefkao_subjectTitle",
236
+ "badge": "dsefkao_badge",
237
+ "badge_draft": "dsefkao_badge_draft",
238
+ "badge_attacking": "dsefkao_badge_attacking",
239
+ "badge_judging": "dsefkao_badge_judging",
240
+ "badge_settled": "dsefkao_badge_settled",
241
+ "statusStrip": "dsefkao_statusStrip",
242
+ "statusStripInfo": "dsefkao_statusStripInfo",
243
+ "statusStripHead": "dsefkao_statusStripHead",
244
+ "statusState": "dsefkao_statusState",
245
+ "statusHint": "dsefkao_statusHint",
246
+ "statusStripAction": "dsefkao_statusStripAction",
247
+ "statusNextLabel": "dsefkao_statusNextLabel",
248
+ "cta": "dsefkao_cta",
249
+ "statusError": "dsefkao_statusError",
250
+ "statusOk": "dsefkao_statusOk",
251
+ "section": "dsefkao_section",
252
+ "brandBlock": "dsefkao_brandBlock",
253
+ "sectionTitle": "dsefkao_sectionTitle",
254
+ "subject": "dsefkao_subject",
255
+ "stance": "dsefkao_stance",
256
+ "stanceInput": "dsefkao_stanceInput",
257
+ "chips": "dsefkao_chips",
258
+ "chip": "dsefkao_chip",
259
+ "chipWarn": "dsefkao_chipWarn",
260
+ "chipStrike": "dsefkao_chipStrike",
261
+ "expertRow": "dsefkao_expertRow",
262
+ "expertName": "dsefkao_expertName",
263
+ "impactBar": "dsefkao_impactBar",
264
+ "countEndorsed": "dsefkao_countEndorsed",
265
+ "countRejected": "dsefkao_countRejected",
266
+ "countPending": "dsefkao_countPending",
267
+ "countStrike": "dsefkao_countStrike",
268
+ "defect": "dsefkao_defect",
269
+ "defectHead": "dsefkao_defectHead",
270
+ "quote": "dsefkao_quote",
271
+ "defectContent": "dsefkao_defectContent",
272
+ "evidence": "dsefkao_evidence",
273
+ "evidenceLabel": "dsefkao_evidenceLabel",
274
+ "recheckBox": "dsefkao_recheckBox",
275
+ "recheckHead": "dsefkao_recheckHead",
276
+ "rejectReason": "dsefkao_rejectReason",
277
+ "actions": "dsefkao_actions",
278
+ "btnPrimary": "dsefkao_btnPrimary",
279
+ "btnPrimaryOn": "dsefkao_btnPrimaryOn",
280
+ "btnDanger": "dsefkao_btnDanger",
281
+ "btnDangerOn": "dsefkao_btnDangerOn",
282
+ "btnGhost": "dsefkao_btnGhost",
283
+ "rejectBox": "dsefkao_rejectBox",
284
+ "rejectPrompt": "dsefkao_rejectPrompt",
285
+ "rejectInput": "dsefkao_rejectInput",
286
+ "link": "dsefkao_link",
287
+ "footer": "dsefkao_footer",
288
+ "footerNote": "dsefkao_footerNote",
289
+ "muted": "dsefkao_muted",
290
+ "warn": "dsefkao_warn",
291
+ "notice": "dsefkao_notice",
292
+ "empty": "dsefkao_empty",
293
+ "ts": "dsefkao_ts",
294
+ "sectionDivider": "dsefkao_sectionDivider",
295
+ "sectionHeadRow": "dsefkao_sectionHeadRow",
296
+ "sectionTitleStrong": "dsefkao_sectionTitleStrong",
297
+ "sectionCount": "dsefkao_sectionCount",
298
+ "foldButton": "dsefkao_foldButton",
299
+ "explain": "dsefkao_explain",
300
+ "explainText": "dsefkao_explainText",
301
+ "explainClose": "dsefkao_explainClose",
302
+ "expertCard": "dsefkao_expertCard",
303
+ "expertHead": "dsefkao_expertHead",
304
+ "expertNameStrong": "dsefkao_expertNameStrong",
305
+ "defectAuthor": "dsefkao_defectAuthor",
306
+ "verdictButton": "dsefkao_verdictButton",
307
+ "verdictEndorse": "dsefkao_verdictEndorse",
308
+ "verdictEndorseOn": "dsefkao_verdictEndorseOn",
309
+ "verdictReject": "dsefkao_verdictReject",
310
+ "verdictRejectOn": "dsefkao_verdictRejectOn",
311
+ "verdictPending": "dsefkao_verdictPending",
312
+ "verdictPendingOn": "dsefkao_verdictPendingOn",
313
+ "stanceFull": "dsefkao_stanceFull",
314
+ "roleEditor": "dsefkao_roleEditor",
315
+ "roleInput": "dsefkao_roleInput"
316
+ };
317
+ const tagId$1 = "@y9931666/dsh-plugin-crossfire/ReviewCard.module.css";
318
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
319
+ const tag = document.createElement("style");
320
+ tag.dataset.plugin = "@y9931666/dsh-plugin-crossfire";
321
+ tag.dataset.pluginCss = tagId$1;
322
+ tag.textContent = css$1;
323
+ document.head.appendChild(tag);
324
+ }
325
+ var ReviewCard_module_css_default = classMap$1;
326
+
327
+ //#endregion
328
+ //#region src/client/ReviewCard.tsx
329
+ /** 轮询间隔。与圆桌一致:本机 HTTP,1 秒足够跟手。 */
330
+ function CrossfireCard(props) {
331
+ const rpc = typeof props.rpc === "function" ? props.rpc : callRpc;
332
+ const t = typeof props.t === "function" ? props.t : (key) => key;
333
+ /**
334
+ * 当前会话 id —— 来自宿主的**标准注入**(`tool.call.toolview` 的 `standardProps`
335
+ * 里就列着 `sessionId: SessionId`,已用运行时槽查询核实,不是从别处推断的)。
336
+ *
337
+ * 它有两个不可省的用途,而且两个都是真实缺陷的修复:
338
+ * 1. **快照过滤**:不传就会把别的会话的评审一并显示出来;
339
+ * 2. **O1 直推**:`review.advance` 的 attack 分支要用会话 id 换回 live Agent 才能
340
+ * 拉起子代理 —— 不传的话按钮必然拿到 `session-offline`。
341
+ *
342
+ * 拿不到时退化为空串;此时 host 侧**一律拒绝**写操作。刻意不再有"退回记录 owner"
343
+ * 的兜底 —— 那把凭据同时是权限,退回它会开出一条能真正拉起专家的越权通道
344
+ * (见 rpc.ts 的 judgeActor)。
345
+ */
346
+ const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
347
+ const [reviews, setReviews] = (0, react.useState)([]);
348
+ const [note, setNote] = (0, react.useState)(void 0);
349
+ const [notice, setNotice] = (0, react.useState)(void 0);
350
+ const [noticeIsError, setNoticeIsError] = (0, react.useState)(true);
351
+ const [busy, setBusy] = (0, react.useState)(false);
352
+ const [rejectingId, setRejectingId] = (0, react.useState)(null);
353
+ const [rejectDraft, setRejectDraft] = (0, react.useState)("");
354
+ const [expanded, setExpanded] = (0, react.useState)({});
355
+ const [activeReview, setActiveReview] = (0, react.useState)(null);
356
+ const [stanceDrafts, setStanceDrafts] = (0, react.useState)({});
357
+ const [stanceSaved, setStanceSaved] = (0, react.useState)(false);
358
+ const [recheckNotes, setRecheckNotes] = (0, react.useState)({});
359
+ const [roleDrafts, setRoleDrafts] = (0, react.useState)({});
360
+ const [roleEditing, setRoleEditing] = (0, react.useState)(null);
361
+ const [stanceExpanded, setStanceExpanded] = (0, react.useState)({});
362
+ const [recheckApproval, setRecheckApproval] = (0, react.useState)(null);
363
+ const [dismissedHints, setDismissedHints] = (0, react.useState)({});
364
+ const [dismissBusy, setDismissBusy] = (0, react.useState)(false);
365
+ const alive = (0, react.useRef)(true);
366
+ /**
367
+ * 记录一次反馈。
368
+ *
369
+ * 状态区里同时有"失败"与"已保存"两个位置,它们是互斥的:任何新反馈都先清掉上一条
370
+ * 一次性回执,否则"立场已保存"会一直挂在那里,看起来像刚才又保存了一次。
371
+ */
372
+ const reportNotice = (message) => {
373
+ setNotice(message);
374
+ setNoticeIsError(true);
375
+ setStanceSaved(false);
376
+ };
377
+ /** 记录一次**成功**反馈:与失败共用位置,但以正常色渲染(不是红色错误)。 */
378
+ const reportOk = (message) => {
379
+ setNotice(message);
380
+ setNoticeIsError(false);
381
+ setStanceSaved(false);
382
+ };
383
+ (0, react.useEffect)(() => {
384
+ alive.current = true;
385
+ let inflight = false;
386
+ const tick = async () => {
387
+ if (inflight) return;
388
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
389
+ inflight = true;
390
+ const result = await fetchReviews(sessionId);
391
+ inflight = false;
392
+ if (!alive.current) return;
393
+ setReviews(result.reviews);
394
+ setNote(result.note);
395
+ };
396
+ tick();
397
+ const timer = window.setInterval(() => {
398
+ tick();
399
+ }, POLL_MS);
400
+ const onVisible = () => {
401
+ if (document.visibilityState === "visible") tick();
402
+ };
403
+ document.addEventListener("visibilitychange", onVisible);
404
+ return () => {
405
+ alive.current = false;
406
+ window.clearInterval(timer);
407
+ document.removeEventListener("visibilitychange", onVisible);
408
+ };
409
+ }, [sessionId]);
410
+ (0, react.useEffect)(() => {
411
+ (async () => {
412
+ const prefs = await rpc("crossfire/prefs.get", {});
413
+ if (!alive.current) return;
414
+ if (prefs.ok && prefs.value?.prefs !== void 0) setDismissedHints(prefs.value.prefs.dismissedHints ?? {});
415
+ })();
416
+ }, [rpc]);
417
+ /**
418
+ * 关掉一段解释性说明。
419
+ *
420
+ * 写回偏好(而不是只改本地状态),因为用户要的是"删了之后就不会再显示"——刷新页面
421
+ * 也不该回来。写回时把本地已知的键与当前偏好**并集**:否则会覆盖掉设置页那边的恢复
422
+ * 操作(两端各持一份快照,后写者不该把对方的选择抹掉)。
423
+ */
424
+ const dismissHint = async (key) => {
425
+ setDismissedHints((previous) => ({
426
+ ...previous,
427
+ [key]: true
428
+ }));
429
+ setDismissBusy(true);
430
+ const current = await rpc("crossfire/prefs.get", {});
431
+ if (!alive.current) return;
432
+ const saved = await rpc("crossfire/prefs.set", {
433
+ session: sessionId,
434
+ patch: { dismissedHints: {
435
+ ...current.ok ? current.value.prefs.dismissedHints ?? {} : {},
436
+ [key]: true
437
+ } }
438
+ });
439
+ if (!alive.current) return;
440
+ setDismissBusy(false);
441
+ if (!saved.ok) {
442
+ setDismissedHints((previous) => {
443
+ const next = { ...previous };
444
+ delete next[key];
445
+ return next;
446
+ });
447
+ reportNotice(`${t("actionFailed")}:${saved.error.message}`);
448
+ }
449
+ };
450
+ /** 三态表态(**只有人能触发**)。 */
451
+ const setStatus = async (review, defect, status, reason) => {
452
+ setBusy(true);
453
+ const result = await rpc("crossfire/defect.setStatus", {
454
+ reviewId: review.reviewId,
455
+ stateRoot: review.stateRoot,
456
+ session: sessionId,
457
+ defectId: defect.id,
458
+ status,
459
+ rejectReason: reason ?? ""
460
+ });
461
+ if (!alive.current) return;
462
+ setBusy(false);
463
+ if (!result.ok) {
464
+ reportNotice(`${t("actionFailed")}:${result.error.message}`);
465
+ return;
466
+ }
467
+ reportNotice(void 0);
468
+ setReviews((previous) => previous.map((item) => item.reviewId !== review.reviewId ? item : {
469
+ ...item,
470
+ defects: item.defects.map((candidate) => candidate.id !== defect.id ? candidate : {
471
+ ...candidate,
472
+ status,
473
+ ...status === "rejected" ? { rejectReason: reason ?? "" } : {}
474
+ })
475
+ }));
476
+ };
477
+ /**
478
+ * 复审核对结论(只在复审轮出现)。
479
+ *
480
+ * 复审轮的唯一任务是核对**上一轮已认定**的缺陷修没修,所以这里的表态维度不是
481
+ * 认定/驳回,而是 已修复 / 未修复 / 部分修复 —— 换一个维度问同一个问题。
482
+ */
483
+ const setVerdict = async (review, defect, verdict, note$1 = "") => {
484
+ setBusy(true);
485
+ const result = await rpc("crossfire/recheck.setVerdict", {
486
+ reviewId: review.reviewId,
487
+ stateRoot: review.stateRoot,
488
+ session: sessionId,
489
+ defectId: defect.id,
490
+ verdict,
491
+ note: note$1
492
+ });
493
+ if (!alive.current) return;
494
+ setBusy(false);
495
+ if (!result.ok) {
496
+ reportNotice(`${t("actionFailed")}:${result.error.message}`);
497
+ return;
498
+ }
499
+ reportNotice(void 0);
500
+ setReviews((previous) => previous.map((item) => item.reviewId !== review.reviewId ? item : {
501
+ ...item,
502
+ defects: item.defects.map((candidate) => candidate.id !== defect.id ? candidate : {
503
+ ...candidate,
504
+ recheck: {
505
+ pass: candidate.recheck?.pass ?? 1,
506
+ verdict,
507
+ ...note$1.trim() === "" ? {} : { note: note$1.trim() }
508
+ }
509
+ })
510
+ }));
511
+ };
512
+ /** 推进状态机(O1 直推)。 */
513
+ const advance = async (review, step, approved = false) => {
514
+ setBusy(true);
515
+ const result = await rpc("crossfire/review.advance", {
516
+ reviewId: review.reviewId,
517
+ stateRoot: review.stateRoot,
518
+ session: sessionId,
519
+ step,
520
+ approved
521
+ });
522
+ if (!alive.current) return;
523
+ setBusy(false);
524
+ if (!result.ok) {
525
+ reportNotice(`${t("actionFailed")}:${result.error.message}`);
526
+ return;
527
+ }
528
+ reportNotice(void 0);
529
+ const nextStatus = step === "attack" || step === "recheck" ? "attacking" : step === "collect" ? "judging" : step === "finish" ? "settled" : void 0;
530
+ if (nextStatus !== void 0) setReviews((previous) => previous.map((item) => item.reviewId !== review.reviewId ? item : {
531
+ ...item,
532
+ status: nextStatus
533
+ }));
534
+ };
535
+ /** 保存立场。人改过之后 host 会把 origin 记为 edited,自动提炼不再覆盖它。 */
536
+ const saveStance = async (review, text) => {
537
+ setBusy(true);
538
+ const result = await rpc("crossfire/stance.set", {
539
+ reviewId: review.reviewId,
540
+ stateRoot: review.stateRoot,
541
+ session: sessionId,
542
+ text
543
+ });
544
+ if (!alive.current) return;
545
+ setBusy(false);
546
+ if (!result.ok) {
547
+ reportNotice(`${t("actionFailed")}:${result.error.message}`);
548
+ return;
549
+ }
550
+ reportNotice(void 0);
551
+ setStanceSaved(true);
552
+ setStanceDrafts((previous) => {
553
+ const next = { ...previous };
554
+ delete next[review.reviewId];
555
+ return next;
556
+ });
557
+ };
558
+ /**
559
+ * 保存一位专家的角色定位。
560
+ *
561
+ * 它进 persona 的「你的角色定位」段 —— 用户诉求是"我无法对专家列表的专家提示词进行
562
+ * 修改"。两条边界写在这里:**正在攻击中的专家不受影响**(它的 persona 在拉起那一刻
563
+ * 就固定了),改动对下一轮生效;空串表示清除该定位。
564
+ */
565
+ const saveExpertRole = async (review, expert, role) => {
566
+ setBusy(true);
567
+ const result = await rpc("crossfire/expert.setRole", {
568
+ reviewId: review.reviewId,
569
+ stateRoot: review.stateRoot,
570
+ session: sessionId,
571
+ expertKey: expert.key,
572
+ role
573
+ });
574
+ if (!alive.current) return;
575
+ setBusy(false);
576
+ if (!result.ok) {
577
+ reportNotice(`${t("actionFailed")}:${result.error.message}`);
578
+ return;
579
+ }
580
+ reportNotice(void 0);
581
+ setRoleEditing(null);
582
+ setReviews((previous) => previous.map((item) => item.reviewId !== review.reviewId ? item : {
583
+ ...item,
584
+ experts: item.experts.map((candidate) => {
585
+ if (candidate.key !== expert.key) return candidate;
586
+ const next = { ...candidate };
587
+ if (role === "") delete next.role;
588
+ else next.role = role;
589
+ return next;
590
+ })
591
+ }));
592
+ };
593
+ /** 导出为 Markdown 并触发下载(文件同时已落盘到评审目录)。 */
594
+ const exportReview = async (review) => {
595
+ setBusy(true);
596
+ const result = await rpc("crossfire/review.export", {
597
+ reviewId: review.reviewId,
598
+ stateRoot: review.stateRoot,
599
+ session: sessionId
600
+ });
601
+ if (!alive.current) return;
602
+ setBusy(false);
603
+ if (!result.ok) {
604
+ reportNotice(`${t("exportFailed")}:${result.error.message}`);
605
+ return;
606
+ }
607
+ reportOk(`${t("exportDone")}:${result.value.path}`);
608
+ try {
609
+ const blob = new Blob([result.value.markdown], { type: "text/markdown;charset=utf-8" });
610
+ const url = URL.createObjectURL(blob);
611
+ const anchor = document.createElement("a");
612
+ anchor.href = url;
613
+ anchor.download = `${review.reviewId}.md`;
614
+ anchor.click();
615
+ URL.revokeObjectURL(url);
616
+ } catch {}
617
+ };
618
+ /**
619
+ * 轮询间隔(毫秒)。
620
+ *
621
+ * 从 1s 放宽到 2s:这张卡注册在**两个**槽位(`conversation.view` 标签页与
622
+ * `tool.call.toolview` 轨迹卡片),两个实例各持一条定时器;而一次快照要把工作区下
623
+ * 所有评审的 `review.json` 读一遍并整包重发(无 ETag、无变更检测)。1s 意味着每秒
624
+ * 2+ 次全量读盘 + 全量传输,而其中绝大多数字节从未变化。
625
+ *
626
+ * 放宽间隔**不会让界面变迟钝**:所有写操作(表态 / 立场 / 角色定位 / 推进状态机)
627
+ * 都走本地乐观更新,点完立刻见效,轮询只负责把它纠正成服务端真相(含别的窗口的
628
+ * 改动)—— 那种场景下 2s 与 1s 的差别用户感知不到。
629
+ *
630
+ * 彻底的解法是给快照加变更检测(ETag / If-None-Match 或版本号短路),那需要改传输
631
+ * 协议,留给下一轮;这条只是把白付的代价减半。
632
+ */
633
+ const POLL_MS = 2e3;
634
+ const STATUS_LABEL_KEY = {
635
+ draft: "statusDraft",
636
+ attacking: "statusAttacking",
637
+ judging: "statusJudging",
638
+ settled: "statusSettled"
639
+ };
640
+ const EXPERT_STATUS_KEY = {
641
+ pending: "expertPending",
642
+ running: "expertRunning",
643
+ done: "expertDone",
644
+ failed: "expertFailed",
645
+ silent: "expertSilent"
646
+ };
647
+ const VERDICT_KEY = {
648
+ fixed: "verdictFixed",
649
+ unfixed: "verdictUnfixed",
650
+ partial: "verdictPartial"
651
+ };
652
+ /**
653
+ * 每一态的一句话说明:**现在是什么局面、为什么必须由人来做下一步**。
654
+ *
655
+ * 状态区里"态名 + 唯一主行动"已经表达了"能做/该做什么",这一行补的是"为什么" ——
656
+ * 卡片上信息很多,缺的是分层,不是内容。
657
+ */
658
+ const STATUS_HINT_KEY = {
659
+ draft: "statusHintDraft",
660
+ attacking: "statusHintAttacking",
661
+ judging: "statusHintJudging",
662
+ settled: "statusHintSettled"
663
+ };
664
+ /** 复审核对结论的三个选项(顺序即界面顺序)。 */
665
+ const RECHECK_VERDICTS = [
666
+ "fixed",
667
+ "unfixed",
668
+ "partial"
669
+ ];
670
+ /**
671
+ * 从缺陷的 `expertKey` 找回它的作者专家。
672
+ *
673
+ * 缺陷只带 key(数据层就是这么存的),而品牌头像与模型路由在专家身上。找不到时返回
674
+ * `undefined` —— 那条缺陷仍然会显示(用中性块),因为**内容比标识重要**:一条找不到
675
+ * 作者的缺陷不该从裁决列表里消失。
676
+ */
677
+ function authorOf(expertKey, experts) {
678
+ return experts.find((expert) => expert.key === expertKey);
679
+ }
680
+ /** 专家模型路由的可读串;拿不到时退化为给定的兜底文案(通常是"继承主持人")。 */
681
+ function routeOf(expert, fallback) {
682
+ if (expert === void 0 || expert.provider === void 0) return fallback;
683
+ return routeText(expert.provider, expert.model);
684
+ }
685
+ if (reviews.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
686
+ className: ReviewCard_module_css_default.root,
687
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
688
+ className: ReviewCard_module_css_default.header,
689
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
690
+ className: ReviewCard_module_css_default.title,
691
+ children: t("cardTitle")
692
+ })
693
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
694
+ className: ReviewCard_module_css_default.empty,
695
+ children: [
696
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("noReviews") }),
697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
698
+ className: ReviewCard_module_css_default.muted,
699
+ children: t("cardHint")
700
+ }),
701
+ note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
702
+ className: ReviewCard_module_css_default.muted,
703
+ children: note
704
+ })
705
+ ]
706
+ })]
707
+ });
708
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
709
+ className: ReviewCard_module_css_default.root,
710
+ children: [sessionId === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
711
+ className: ReviewCard_module_css_default.notice,
712
+ children: t("noSessionHint")
713
+ }) : null, reviews.map((review) => {
714
+ const activeReviewId = activeReview ?? reviews[0]?.reviewId;
715
+ if (review.reviewId !== activeReviewId) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
716
+ type: "button",
717
+ className: ReviewCard_module_css_default.collapsed,
718
+ onClick: () => {
719
+ setActiveReview(review.reviewId);
720
+ setStanceSaved(false);
721
+ reportNotice(void 0);
722
+ },
723
+ children: [
724
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
725
+ className: `${ReviewCard_module_css_default.badge} ${ReviewCard_module_css_default[`badge_${review.status}`] ?? ""}`,
726
+ children: t(STATUS_LABEL_KEY[review.status])
727
+ }),
728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
729
+ className: ReviewCard_module_css_default.subjectTitle,
730
+ children: review.subjectTitle
731
+ }),
732
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
733
+ className: ReviewCard_module_css_default.muted,
734
+ children: review.counts.total === 0 ? t("defectEmpty") : t("expertDefects").replace("{n}", String(review.counts.total))
735
+ }),
736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
737
+ className: ReviewCard_module_css_default.muted,
738
+ children: "▸"
739
+ })
740
+ ]
741
+ }, review.reviewId);
742
+ const isExpanded = expanded[review.reviewId] === true;
743
+ const reportedCount = review.experts.filter((expert) => (expert.reportCount ?? 0) > 0 || expert.defectCount > 0).length;
744
+ const atRecheckCap = review.reviewPass + 1 > review.maxReviewPass;
745
+ const body = isExpanded || review.subjectContent.length <= 400 ? review.subjectContent : `${review.subjectContent.slice(0, 400)}…`;
746
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
747
+ className: ReviewCard_module_css_default.card,
748
+ children: [
749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
750
+ className: ReviewCard_module_css_default.header,
751
+ children: [
752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
753
+ className: ReviewCard_module_css_default.title,
754
+ children: t("cardTitle")
755
+ }),
756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
757
+ className: ReviewCard_module_css_default.subjectTitle,
758
+ children: review.subjectTitle
759
+ }),
760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
761
+ className: `${ReviewCard_module_css_default.badge} ${ReviewCard_module_css_default[`badge_${review.status}`] ?? ""}`,
762
+ children: t(STATUS_LABEL_KEY[review.status])
763
+ }),
764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
765
+ className: ReviewCard_module_css_default.muted,
766
+ children: t("passBadge").replace("{pass}", String(review.reviewPass)).replace("{max}", String(review.maxReviewPass))
767
+ })
768
+ ]
769
+ }),
770
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
771
+ className: ReviewCard_module_css_default.statusStrip,
772
+ "aria-label": t("statusStrip"),
773
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
774
+ className: ReviewCard_module_css_default.statusStripInfo,
775
+ children: [
776
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
777
+ className: ReviewCard_module_css_default.statusStripHead,
778
+ children: [
779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
780
+ className: ReviewCard_module_css_default.statusState,
781
+ children: t(STATUS_LABEL_KEY[review.status])
782
+ }),
783
+ review.status === "judging" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
784
+ className: ReviewCard_module_css_default.muted,
785
+ children: `${t("countPending")} ${review.counts.pending} / ${t("impactTitle")} ${review.counts.total}`
786
+ }) : null,
787
+ review.status === "attacking" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
788
+ className: ReviewCard_module_css_default.muted,
789
+ children: t("expertSubmitted").replace("{done}", String(reportedCount)).replace("{total}", String(review.experts.length))
790
+ }) : null
791
+ ]
792
+ }),
793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
794
+ className: ReviewCard_module_css_default.statusHint,
795
+ children: t(STATUS_HINT_KEY[review.status])
796
+ }),
797
+ notice === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
798
+ className: noticeIsError ? ReviewCard_module_css_default.statusError : ReviewCard_module_css_default.statusOk,
799
+ children: notice
800
+ }),
801
+ stanceSaved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
802
+ className: ReviewCard_module_css_default.statusOk,
803
+ children: t("stanceSaved")
804
+ }) : null
805
+ ]
806
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
807
+ className: ReviewCard_module_css_default.statusStripAction,
808
+ children: [
809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
810
+ className: ReviewCard_module_css_default.statusNextLabel,
811
+ children: t("statusNext")
812
+ }),
813
+ review.status === "attacking" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
814
+ className: ReviewCard_module_css_default.muted,
815
+ children: t("busyWorking")
816
+ }) : null,
817
+ review.status === "draft" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
818
+ type: "button",
819
+ disabled: busy,
820
+ className: ReviewCard_module_css_default.cta,
821
+ onClick: () => {
822
+ advance(review, "attack");
823
+ },
824
+ children: t("btnAttack")
825
+ }) : null,
826
+ review.status === "attacking" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
827
+ type: "button",
828
+ disabled: busy,
829
+ className: ReviewCard_module_css_default.cta,
830
+ onClick: () => {
831
+ advance(review, "collect");
832
+ },
833
+ children: t("btnCollect")
834
+ }) : null,
835
+ review.status === "judging" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
836
+ type: "button",
837
+ disabled: busy,
838
+ className: ReviewCard_module_css_default.cta,
839
+ onClick: () => {
840
+ advance(review, "finish");
841
+ },
842
+ children: t("btnFinish")
843
+ }) : null,
844
+ review.status === "settled" ? atRecheckCap ? recheckApproval === review.reviewId ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
846
+ className: ReviewCard_module_css_default.muted,
847
+ children: t("recheckApprovalAsk")
848
+ }),
849
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
850
+ type: "button",
851
+ disabled: busy,
852
+ className: ReviewCard_module_css_default.cta,
853
+ onClick: () => {
854
+ setRecheckApproval(null);
855
+ advance(review, "recheck", true);
856
+ },
857
+ children: t("recheckApprovalYes")
858
+ }),
859
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
860
+ type: "button",
861
+ disabled: busy,
862
+ className: ReviewCard_module_css_default.foldButton,
863
+ onClick: () => setRecheckApproval(null),
864
+ children: t("recheckApprovalNo")
865
+ })
866
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
867
+ type: "button",
868
+ disabled: busy,
869
+ className: ReviewCard_module_css_default.cta,
870
+ onClick: () => setRecheckApproval(review.reviewId),
871
+ children: t("btnRecheck")
872
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
873
+ type: "button",
874
+ disabled: busy,
875
+ className: ReviewCard_module_css_default.cta,
876
+ onClick: () => {
877
+ advance(review, "recheck");
878
+ },
879
+ children: t("btnRecheck")
880
+ }) : null
881
+ ]
882
+ })]
883
+ }),
884
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
885
+ className: `${ReviewCard_module_css_default.section} ${ReviewCard_module_css_default.sectionDivider}`,
886
+ children: [
887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
888
+ className: ReviewCard_module_css_default.sectionHeadRow,
889
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
890
+ className: ReviewCard_module_css_default.sectionTitleStrong,
891
+ children: t("subjectLabel")
892
+ })
893
+ }),
894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
895
+ className: ReviewCard_module_css_default.subject,
896
+ children: body
897
+ }),
898
+ review.subjectContent.length > 400 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
899
+ type: "button",
900
+ className: ReviewCard_module_css_default.foldButton,
901
+ onClick: () => setExpanded((previous) => ({
902
+ ...previous,
903
+ [review.reviewId]: !isExpanded
904
+ })),
905
+ children: isExpanded ? t("collapseSection") : t("expandSection")
906
+ }) : null
907
+ ]
908
+ }),
909
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
910
+ className: `${ReviewCard_module_css_default.section} ${ReviewCard_module_css_default.sectionDivider}`,
911
+ children: [
912
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
913
+ className: ReviewCard_module_css_default.sectionHeadRow,
914
+ children: [
915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
916
+ className: ReviewCard_module_css_default.sectionTitleStrong,
917
+ children: t("stanceSectionTitle")
918
+ }),
919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
920
+ className: ReviewCard_module_css_default.sectionCount,
921
+ children: review.stanceOrigin === "edited" ? t("stanceSaved") : ""
922
+ }),
923
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: ReviewCard_module_css_default.spacer }),
924
+ review.stanceText.trim() === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
925
+ type: "button",
926
+ className: ReviewCard_module_css_default.foldButton,
927
+ onClick: () => setStanceExpanded((previous) => ({
928
+ ...previous,
929
+ [review.reviewId]: previous[review.reviewId] !== true
930
+ })),
931
+ children: stanceExpanded[review.reviewId] === true ? t("stanceCollapse") : t("stanceExpand")
932
+ })
933
+ ]
934
+ }),
935
+ stanceExpanded[review.reviewId] === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
936
+ className: ReviewCard_module_css_default.stanceFull,
937
+ children: review.stanceText
938
+ }) : null,
939
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
940
+ className: ReviewCard_module_css_default.stanceInput,
941
+ rows: 3,
942
+ disabled: busy,
943
+ value: stanceDrafts[review.reviewId] ?? review.stanceText,
944
+ placeholder: t("stanceHint"),
945
+ onChange: (event) => {
946
+ const value = event.target.value;
947
+ setStanceDrafts((previous) => ({
948
+ ...previous,
949
+ [review.reviewId]: value
950
+ }));
951
+ setStanceSaved(false);
952
+ }
953
+ }),
954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
955
+ className: ReviewCard_module_css_default.actions,
956
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
957
+ type: "button",
958
+ disabled: busy || (stanceDrafts[review.reviewId] ?? review.stanceText) === review.stanceText,
959
+ className: ReviewCard_module_css_default.cta,
960
+ onClick: () => {
961
+ saveStance(review, stanceDrafts[review.reviewId] ?? review.stanceText);
962
+ },
963
+ children: t("stanceSave")
964
+ }), review.stanceText.trim() === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
965
+ className: ReviewCard_module_css_default.muted,
966
+ children: t("stanceEmpty")
967
+ }) : null]
968
+ }),
969
+ dismissedHints.stanceHint === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
970
+ className: ReviewCard_module_css_default.explain,
971
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
972
+ className: ReviewCard_module_css_default.explainText,
973
+ children: t("stanceHint")
974
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
975
+ type: "button",
976
+ className: ReviewCard_module_css_default.explainClose,
977
+ disabled: dismissBusy,
978
+ title: t("explainDismiss"),
979
+ "aria-label": t("explainDismiss"),
980
+ onClick: () => {
981
+ dismissHint("stanceHint");
982
+ },
983
+ children: "×"
984
+ })]
985
+ })
986
+ ]
987
+ }),
988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
989
+ className: `${ReviewCard_module_css_default.section} ${ReviewCard_module_css_default.sectionDivider}`,
990
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
991
+ className: ReviewCard_module_css_default.sectionHeadRow,
992
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
993
+ className: ReviewCard_module_css_default.sectionTitleStrong,
994
+ children: t("dimensionsLabel")
995
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
996
+ className: ReviewCard_module_css_default.sectionCount,
997
+ children: review.intensityLabel
998
+ })]
999
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1000
+ className: ReviewCard_module_css_default.chips,
1001
+ children: review.dimensions.map((dimension) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1002
+ className: ReviewCard_module_css_default.chip,
1003
+ children: dimension
1004
+ }, dimension))
1005
+ })]
1006
+ }),
1007
+ review.experts.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1008
+ className: `${ReviewCard_module_css_default.section} ${ReviewCard_module_css_default.sectionDivider}`,
1009
+ children: [
1010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1011
+ className: ReviewCard_module_css_default.sectionHeadRow,
1012
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1013
+ className: ReviewCard_module_css_default.sectionTitleStrong,
1014
+ children: t("expertsTitle")
1015
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1016
+ className: ReviewCard_module_css_default.sectionCount,
1017
+ children: t("expertSubmitted").replace("{done}", String(reportedCount)).replace("{total}", String(review.experts.length))
1018
+ })]
1019
+ }),
1020
+ dismissedHints.expertRosterHint === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1021
+ className: ReviewCard_module_css_default.explain,
1022
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1023
+ className: ReviewCard_module_css_default.explainText,
1024
+ children: t("expertRosterHint")
1025
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1026
+ type: "button",
1027
+ className: ReviewCard_module_css_default.explainClose,
1028
+ disabled: dismissBusy,
1029
+ title: t("explainDismiss"),
1030
+ "aria-label": t("explainDismiss"),
1031
+ onClick: () => {
1032
+ dismissHint("expertRosterHint");
1033
+ },
1034
+ children: "×"
1035
+ })]
1036
+ }),
1037
+ review.experts.map((expert) => {
1038
+ const roleKey = `${review.reviewId}::${expert.key}`;
1039
+ const brand = providerBrand(expert.provider);
1040
+ const route = expert.provider === void 0 ? t("modelInherit") : routeText(expert.provider, expert.model);
1041
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1042
+ className: ReviewCard_module_css_default.expertCard,
1043
+ children: [
1044
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1045
+ className: ReviewCard_module_css_default.expertHead,
1046
+ children: [
1047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1048
+ className: ReviewCard_module_css_default.brandBlock,
1049
+ style: { background: brand.color },
1050
+ title: route,
1051
+ children: brand.abbr
1052
+ }),
1053
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1054
+ className: ReviewCard_module_css_default.expertNameStrong,
1055
+ children: expert.key
1056
+ }),
1057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1058
+ className: ReviewCard_module_css_default.chip,
1059
+ children: expert.dimension
1060
+ }),
1061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1062
+ className: ReviewCard_module_css_default.muted,
1063
+ children: t(EXPERT_STATUS_KEY[expert.status] ?? "expertPending")
1064
+ }),
1065
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1066
+ className: ReviewCard_module_css_default.muted,
1067
+ children: expert.defectCount > 0 ? t("expertDefects").replace("{n}", String(expert.defectCount)) : expert.reportCount !== void 0 && expert.reportCount > 0 ? t("expertReported").replace("{n}", String(expert.reportCount)) : t("expertNoOutput")
1068
+ }),
1069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: ReviewCard_module_css_default.spacer }),
1070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1071
+ type: "button",
1072
+ className: ReviewCard_module_css_default.foldButton,
1073
+ disabled: busy || roleEditing === roleKey,
1074
+ onClick: () => {
1075
+ setRoleDrafts((previous) => ({
1076
+ ...previous,
1077
+ [roleKey]: expert.role ?? ""
1078
+ }));
1079
+ setRoleEditing(roleKey);
1080
+ },
1081
+ children: t("expertRoleEdit")
1082
+ })
1083
+ ]
1084
+ }),
1085
+ expert.role === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1086
+ className: ReviewCard_module_css_default.muted,
1087
+ children: `${t("expertRoleLabel")}:${expert.role}`
1088
+ }),
1089
+ roleEditing === roleKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1090
+ className: ReviewCard_module_css_default.roleEditor,
1091
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1092
+ className: ReviewCard_module_css_default.roleInput,
1093
+ rows: 3,
1094
+ autoFocus: true,
1095
+ value: roleDrafts[roleKey] ?? "",
1096
+ placeholder: t("expertRolePlaceholder"),
1097
+ onChange: (event) => {
1098
+ const value = event.target.value;
1099
+ setRoleDrafts((previous) => ({
1100
+ ...previous,
1101
+ [roleKey]: value
1102
+ }));
1103
+ }
1104
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1105
+ className: ReviewCard_module_css_default.actions,
1106
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1107
+ type: "button",
1108
+ className: ReviewCard_module_css_default.btnPrimary,
1109
+ disabled: busy,
1110
+ onClick: () => {
1111
+ saveExpertRole(review, expert, roleDrafts[roleKey] ?? "");
1112
+ },
1113
+ children: t("presetSave")
1114
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1115
+ type: "button",
1116
+ className: ReviewCard_module_css_default.btnGhost,
1117
+ disabled: busy,
1118
+ onClick: () => setRoleEditing(null),
1119
+ children: t("presetCancel")
1120
+ })]
1121
+ })]
1122
+ }) : null,
1123
+ expert.note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1124
+ className: ReviewCard_module_css_default.warn,
1125
+ children: expert.note
1126
+ })
1127
+ ]
1128
+ }, expert.key);
1129
+ })
1130
+ ]
1131
+ }),
1132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1133
+ className: ReviewCard_module_css_default.impactBar,
1134
+ children: [
1135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("impactTitle") }),
1136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1137
+ className: ReviewCard_module_css_default.countEndorsed,
1138
+ children: [
1139
+ t("countEndorsed"),
1140
+ " ",
1141
+ review.counts.endorsed
1142
+ ]
1143
+ }),
1144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1145
+ className: ReviewCard_module_css_default.countRejected,
1146
+ children: [
1147
+ t("countRejected"),
1148
+ " ",
1149
+ review.counts.rejected
1150
+ ]
1151
+ }),
1152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1153
+ className: ReviewCard_module_css_default.countPending,
1154
+ children: [
1155
+ t("countPending"),
1156
+ " ",
1157
+ review.counts.pending
1158
+ ]
1159
+ }),
1160
+ review.counts.strikes === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1161
+ className: ReviewCard_module_css_default.countStrike,
1162
+ children: [
1163
+ t("strikesStance"),
1164
+ " ",
1165
+ review.counts.strikes
1166
+ ]
1167
+ })
1168
+ ]
1169
+ }),
1170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1171
+ className: ReviewCard_module_css_default.sectionDivider,
1172
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1173
+ className: ReviewCard_module_css_default.sectionHeadRow,
1174
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1175
+ className: ReviewCard_module_css_default.sectionTitleStrong,
1176
+ children: t("defectsTitle")
1177
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1178
+ className: ReviewCard_module_css_default.sectionCount,
1179
+ children: review.defects.length === 0 ? "" : t("expertDefects").replace("{n}", String(review.defects.length))
1180
+ })]
1181
+ })
1182
+ }),
1183
+ review.defects.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1184
+ className: ReviewCard_module_css_default.muted,
1185
+ children: t("defectEmpty")
1186
+ }) : review.defects.map((defect) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1187
+ className: ReviewCard_module_css_default.defect,
1188
+ children: [
1189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1190
+ className: ReviewCard_module_css_default.defectHead,
1191
+ children: [
1192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1193
+ className: ReviewCard_module_css_default.chip,
1194
+ children: defect.dimension
1195
+ }),
1196
+ defect.dimensionFallback === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1197
+ className: ReviewCard_module_css_default.chipWarn,
1198
+ children: t("dimensionFallback")
1199
+ }) : null,
1200
+ defect.strikesStance === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1201
+ className: ReviewCard_module_css_default.chipStrike,
1202
+ children: t("strikesStance")
1203
+ }) : null,
1204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: ReviewCard_module_css_default.spacer }),
1205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1206
+ className: ReviewCard_module_css_default.defectAuthor,
1207
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1208
+ className: ReviewCard_module_css_default.brandBlock,
1209
+ style: { background: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).color },
1210
+ title: routeOf(authorOf(defect.expertKey, review.experts), t("modelInherit")),
1211
+ children: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).abbr
1212
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1213
+ className: ReviewCard_module_css_default.expertNameStrong,
1214
+ children: defect.expertKey
1215
+ })]
1216
+ })
1217
+ ]
1218
+ }),
1219
+ defect.quote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1220
+ className: ReviewCard_module_css_default.quote,
1221
+ children: [
1222
+ "“",
1223
+ defect.quote,
1224
+ "”"
1225
+ ]
1226
+ }),
1227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1228
+ className: ReviewCard_module_css_default.defectContent,
1229
+ children: defect.content
1230
+ }),
1231
+ defect.evidence === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1232
+ className: ReviewCard_module_css_default.evidence,
1233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1234
+ className: ReviewCard_module_css_default.evidenceLabel,
1235
+ children: defect.evidence.kind === "repro" ? `[${t("evidenceRepro")}]` : `[${t("evidenceArgument")}]`
1236
+ }), defect.evidence.text]
1237
+ }),
1238
+ defect.recheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
+ className: ReviewCard_module_css_default.recheckBox,
1240
+ children: [
1241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1242
+ className: ReviewCard_module_css_default.recheckHead,
1243
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1244
+ className: ReviewCard_module_css_default.evidenceLabel,
1245
+ children: t("recheckLabel")
1246
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1247
+ className: ReviewCard_module_css_default.muted,
1248
+ children: `#${String(defect.recheck.pass)}`
1249
+ })]
1250
+ }),
1251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1252
+ className: ReviewCard_module_css_default.actions,
1253
+ children: [RECHECK_VERDICTS.map((verdict) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1254
+ type: "button",
1255
+ disabled: busy,
1256
+ className: defect.recheck?.verdict === verdict ? ReviewCard_module_css_default.btnPrimaryOn : ReviewCard_module_css_default.btnGhost,
1257
+ onClick: () => {
1258
+ setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? "");
1259
+ },
1260
+ children: t(VERDICT_KEY[verdict] ?? "verdictUnfixed")
1261
+ }, verdict)), defect.recheck?.verdict === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1262
+ className: ReviewCard_module_css_default.muted,
1263
+ children: t("verdictUndecided")
1264
+ }) : null]
1265
+ }),
1266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1267
+ className: ReviewCard_module_css_default.actions,
1268
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1269
+ className: ReviewCard_module_css_default.rejectInput,
1270
+ disabled: busy,
1271
+ value: recheckNotes[defect.id] ?? defect.recheck?.note ?? "",
1272
+ placeholder: t("recheckNote"),
1273
+ onChange: (event) => {
1274
+ const value = event.target.value;
1275
+ setRecheckNotes((previous) => ({
1276
+ ...previous,
1277
+ [defect.id]: value
1278
+ }));
1279
+ }
1280
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1281
+ type: "button",
1282
+ disabled: busy || defect.recheck?.verdict === void 0 || (recheckNotes[defect.id] ?? defect.recheck?.note ?? "") === (defect.recheck?.note ?? ""),
1283
+ title: defect.recheck?.verdict === void 0 ? t("recheckNoteNeedsVerdict") : void 0,
1284
+ className: ReviewCard_module_css_default.btnGhost,
1285
+ onClick: () => {
1286
+ const verdict = defect.recheck?.verdict;
1287
+ if (verdict === void 0) return;
1288
+ setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? defect.recheck?.note ?? "");
1289
+ },
1290
+ children: t("presetSave")
1291
+ })]
1292
+ }),
1293
+ defect.recheck.note === void 0 || defect.recheck.note === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1294
+ className: ReviewCard_module_css_default.muted,
1295
+ children: defect.recheck.note
1296
+ })
1297
+ ]
1298
+ }),
1299
+ defect.status === "rejected" && defect.rejectReason !== void 0 && defect.rejectReason !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1300
+ className: ReviewCard_module_css_default.rejectReason,
1301
+ children: [
1302
+ t("btnReject"),
1303
+ ":",
1304
+ defect.rejectReason
1305
+ ]
1306
+ }) : null,
1307
+ review.status === "settled" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1308
+ className: ReviewCard_module_css_default.actions,
1309
+ children: [
1310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1311
+ type: "button",
1312
+ disabled: busy,
1313
+ className: defect.status === "endorsed" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorseOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorse}`,
1314
+ onClick: () => {
1315
+ setStatus(review, defect, defect.status === "endorsed" ? "pending" : "endorsed");
1316
+ },
1317
+ children: t("btnEndorse")
1318
+ }),
1319
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1320
+ type: "button",
1321
+ disabled: busy,
1322
+ className: defect.status === "rejected" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictRejectOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictReject}`,
1323
+ onClick: () => {
1324
+ if (defect.status === "rejected") {
1325
+ setStatus(review, defect, "pending");
1326
+ return;
1327
+ }
1328
+ setRejectingId(defect.id);
1329
+ setRejectDraft("");
1330
+ },
1331
+ children: t("btnReject")
1332
+ }),
1333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1334
+ type: "button",
1335
+ disabled: busy,
1336
+ className: defect.status === "pending" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPendingOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPending}`,
1337
+ onClick: () => {
1338
+ setStatus(review, defect, "pending");
1339
+ },
1340
+ children: t("btnPending")
1341
+ })
1342
+ ]
1343
+ }),
1344
+ rejectingId === defect.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
+ className: ReviewCard_module_css_default.rejectBox,
1346
+ children: [
1347
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1348
+ className: ReviewCard_module_css_default.rejectPrompt,
1349
+ children: t("rejectPrompt")
1350
+ }),
1351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1352
+ className: ReviewCard_module_css_default.rejectInput,
1353
+ value: rejectDraft,
1354
+ autoFocus: true,
1355
+ rows: 2,
1356
+ placeholder: t("rejectPlaceholder"),
1357
+ onChange: (event) => setRejectDraft(event.target.value)
1358
+ }),
1359
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1360
+ className: ReviewCard_module_css_default.actions,
1361
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1362
+ type: "button",
1363
+ disabled: busy,
1364
+ className: ReviewCard_module_css_default.btnDangerOn,
1365
+ onClick: () => {
1366
+ const reason = rejectDraft.trim();
1367
+ if (reason === "") {
1368
+ reportNotice(t("rejectPrompt"));
1369
+ return;
1370
+ }
1371
+ setStatus(review, defect, "rejected", reason);
1372
+ setRejectingId(null);
1373
+ setRejectDraft("");
1374
+ },
1375
+ children: t("rejectConfirm")
1376
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1377
+ type: "button",
1378
+ className: ReviewCard_module_css_default.btnGhost,
1379
+ onClick: () => {
1380
+ setRejectingId(null);
1381
+ setRejectDraft("");
1382
+ },
1383
+ children: t("rejectCancel")
1384
+ })]
1385
+ })
1386
+ ]
1387
+ }) : null
1388
+ ]
1389
+ }, defect.id)),
1390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1391
+ className: ReviewCard_module_css_default.footer,
1392
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1393
+ type: "button",
1394
+ disabled: busy,
1395
+ className: ReviewCard_module_css_default.btnGhost,
1396
+ onClick: () => {
1397
+ exportReview(review);
1398
+ },
1399
+ children: t("btnExport")
1400
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1401
+ className: ReviewCard_module_css_default.footerNote,
1402
+ children: t("cardFooterNote")
1403
+ })]
1404
+ })
1405
+ ]
1406
+ }, review.reviewId);
1407
+ })]
1408
+ });
1409
+ }
1410
+
1411
+ //#endregion
1412
+ //#region src/client/merge.ts
1413
+ /**
1414
+ * 把一条预设并入数组:**同 id 就地替换,新 id 才追加**。
1415
+ *
1416
+ * 位置保持不变(编辑不应把条目挪到末尾,否则列表顺序会在每次编辑后跳动)。
1417
+ */
1418
+ function mergePreset(list, preset) {
1419
+ return list.some((item) => item.id === preset.id) ? list.map((item) => item.id === preset.id ? preset : item) : [...list, preset];
1420
+ }
1421
+ /** 阵容版本:与 {@link mergePreset} 同一规则(同 id 就地替换,新 id 追加)。 */
1422
+ function mergeSquad(list, squad) {
1423
+ return list.some((item) => item.id === squad.id) ? list.map((item) => item.id === squad.id ? squad : item) : [...list, squad];
1424
+ }
1425
+
1426
+ //#endregion
1427
+ //#region \0dsh-css:F:\AI\DSH\1changyong\dsh-plugin-crossfire\src\client\SettingsView.module.css.mjs
1428
+ const css = "/*\n * Crossfire settings page styles.\n *\n * 硬约束:**每一个颜色都必须来自主题令牌**(运行时 `Theme.ddxi2lc_listTokens` 的\n * `--dsw-alias-*` 一族),因此浅色与深色主题都无需自己写 override。\n *\n * 这里刻意不照抄圆桌会议的 `RoundTableSettings.ddxi2lc_module.ddxi2lc_css`:那份用的是硬编码\n * `#ffffff` / `rgba(0, 0, 0, 0.8)`,在深色主题下会呈现出白底黑字的割裂块。\n * 令牌已经由宿主按主题解析过,直接用才是正确的做法。\n */\n\n.ddxi2lc_root {\n display: flex;\n flex-direction: column;\n gap: 20px;\n max-width: 620px;\n padding: 8px 0;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.ddxi2lc_notice {\n font-size: 12px;\n line-height: 1.6;\n}\n\n.ddxi2lc_noticeOk {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.ddxi2lc_noticeErr {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.ddxi2lc_section {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.ddxi2lc_sectionTitle {\n font-size: 14px;\n font-weight: 600;\n}\n\n/* 区块抬头行:标题在左,\"新建\"这类**区块主操作**在右。\n *\n * 之前\"+ 新建预设\"与\"编辑/删除\"这类次要操作同级、且淹没在列表下方,用户实测\n * 反馈\"不够显眼\"。提为区块主操作后,它在本区里是唯一的实心按钮。 */\n.ddxi2lc_sectionHead {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_sectionHead .ddxi2lc_sectionTitle {\n flex: 0 1 auto;\n}\n\n/* 把主操作推到右侧;窄屏换行时它自然塌成整行。 */\n.ddxi2lc_sectionHeadGap {\n flex: 1 1 auto;\n}\n\n/* ---- 区块主操作(品牌色实心) ---- */\n\n.ddxi2lc_primaryButton {\n font-size: 13px;\n font-weight: 600;\n padding: 6px 14px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n cursor: pointer;\n}\n\n.ddxi2lc_primaryButton:disabled {\n cursor: not-allowed;\n border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 与 ReviewCard.ddxi2lc_module.ddxi2lc_css 的同名规则刻意各写一份:CSS Modules 的 classMap 是\n * 按模块生成的,跨模块复用类名只能靠把 classMap 传来传去,不值得。两处的大小\n * 差异也是真实的(这里是列表行,那里是密集的专家行)。\n */\n.ddxi2lc_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 22px;\n height: 22px;\n border-radius: 6px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n/* 行内的品牌块 + 路由文本:块给身份,文本给可读的全名(块的 title 在触屏上读不到)。 */\n.ddxi2lc_routeRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_hint {\n font-size: 12px;\n line-height: 1.7;\n color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.9));\n}\n\n.ddxi2lc_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.ddxi2lc_label {\n font-size: 13px;\n font-weight: 600;\n}\n\n.ddxi2lc_input,\n.ddxi2lc_select {\n font-size: 13px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n max-width: 260px;\n}\n\n.ddxi2lc_input:focus,\n.ddxi2lc_select:focus {\n outline: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n outline-offset: 1px;\n}\n\n/* ---- 强度:三选一的分段控件 ---- */\n\n.ddxi2lc_segmented {\n display: inline-flex;\n gap: 0;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-radius: 8px;\n overflow: hidden;\n width: fit-content;\n}\n\n.ddxi2lc_segment {\n font-size: 13px;\n padding: 6px 14px;\n border: none;\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n\n.ddxi2lc_segment + .ddxi2lc_segment {\n border-left: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n}\n\n.ddxi2lc_segmentActive {\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n font-weight: 600;\n}\n\n.ddxi2lc_segment:disabled {\n cursor: progress;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 维度池 ---- */\n\n.ddxi2lc_chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n.ddxi2lc_chip {\n font-size: 12px;\n padding: 3px 10px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 带品牌块的成员 chip:左边品牌色块,右边成员 key。 */\n.ddxi2lc_chipWithBrand {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n padding: 3px 10px 3px 4px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 成员 chip 里的品牌块比列表行的再小一点:chip 本身只有 24px 高。 */\n.ddxi2lc_brandBlockSmall {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.ddxi2lc_chipCustom {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n padding: 3px 6px 3px 10px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.ddxi2lc_chipRemove {\n border: none;\n background: transparent;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n cursor: pointer;\n font-size: 12px;\n padding: 0 2px;\n line-height: 1;\n}\n\n.ddxi2lc_addRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_addButton {\n font-size: 13px;\n padding: 6px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n\n.ddxi2lc_addButton:disabled {\n cursor: not-allowed;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n}\n\n/* ---- 边界说明 ---- */\n\n.ddxi2lc_boundary {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n font-size: 12px;\n line-height: 1.7;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.ddxi2lc_boundaryTitle {\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.ddxi2lc_batchNote {\n font-size: 12px;\n line-height: 1.6;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.5));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ------------------------------------------------------------------ *\n * 角色预设 / 阵容预设(后补的类,颜色同样只用主题令牌)\n * ------------------------------------------------------------------ */\n\n/* 比 .ddxi2lc_input / .ddxi2lc_select 的 260px 宽一些:角色说明与模型名更长。 */\n.ddxi2lc_inputWide {\n font-size: 13px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n width: 100%;\n max-width: 420px;\n}\n\n.ddxi2lc_textarea {\n font-size: 13px;\n font-family: inherit;\n line-height: 1.5;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n width: 100%;\n max-width: 420px;\n resize: vertical;\n}\n\n.ddxi2lc_inputWide:focus,\n.ddxi2lc_textarea:focus {\n outline: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n outline-offset: 1px;\n}\n\n/* ---- 列表 ---- */\n\n.ddxi2lc_list {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.ddxi2lc_listRow {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.ddxi2lc_rowHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.ddxi2lc_rowName {\n font-size: 13px;\n font-weight: 600;\n}\n\n.ddxi2lc_rowMeta {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n word-break: break-word;\n}\n\n/* 把行内按钮推到右侧;列表换行时它自然塌成 0 宽。 */\n.ddxi2lc_spacer {\n flex: 1 1 auto;\n}\n\n.ddxi2lc_rowButton {\n font-size: 12px;\n padding: 3px 10px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n}\n\n.ddxi2lc_rowDanger {\n font-size: 12px;\n padding: 3px 10px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: transparent;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n cursor: pointer;\n}\n\n.ddxi2lc_rowButton:disabled,\n.ddxi2lc_rowDanger:disabled {\n cursor: not-allowed;\n color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.7));\n border-color: var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* ---- 内联编辑器 ---- */\n\n.ddxi2lc_editor {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-layer-1, transparent));\n}\n\n.ddxi2lc_gridTwo {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 10px;\n}\n\n/* ---- 阵容成员行 ---- */\n\n.ddxi2lc_member {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n flex-wrap: wrap;\n padding: 8px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.ddxi2lc_memberFields {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 8px;\n flex: 1 1 320px;\n}\n\n/* 上限提示:与 .ddxi2lc_notice 同字号,但用 warn 令牌(浅深色都可读)。 */\n.ddxi2lc_warn {\n color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.9));\n}\n\n";
1429
+ const classMap = {
1430
+ "listTokens": "ddxi2lc_listTokens",
1431
+ "module": "ddxi2lc_module",
1432
+ "css": "ddxi2lc_css",
1433
+ "root": "ddxi2lc_root",
1434
+ "notice": "ddxi2lc_notice",
1435
+ "noticeOk": "ddxi2lc_noticeOk",
1436
+ "noticeErr": "ddxi2lc_noticeErr",
1437
+ "section": "ddxi2lc_section",
1438
+ "sectionTitle": "ddxi2lc_sectionTitle",
1439
+ "sectionHead": "ddxi2lc_sectionHead",
1440
+ "sectionHeadGap": "ddxi2lc_sectionHeadGap",
1441
+ "primaryButton": "ddxi2lc_primaryButton",
1442
+ "brandBlock": "ddxi2lc_brandBlock",
1443
+ "routeRow": "ddxi2lc_routeRow",
1444
+ "hint": "ddxi2lc_hint",
1445
+ "field": "ddxi2lc_field",
1446
+ "label": "ddxi2lc_label",
1447
+ "input": "ddxi2lc_input",
1448
+ "select": "ddxi2lc_select",
1449
+ "segmented": "ddxi2lc_segmented",
1450
+ "segment": "ddxi2lc_segment",
1451
+ "segmentActive": "ddxi2lc_segmentActive",
1452
+ "chips": "ddxi2lc_chips",
1453
+ "chip": "ddxi2lc_chip",
1454
+ "chipWithBrand": "ddxi2lc_chipWithBrand",
1455
+ "brandBlockSmall": "ddxi2lc_brandBlockSmall",
1456
+ "chipCustom": "ddxi2lc_chipCustom",
1457
+ "chipRemove": "ddxi2lc_chipRemove",
1458
+ "addRow": "ddxi2lc_addRow",
1459
+ "addButton": "ddxi2lc_addButton",
1460
+ "boundary": "ddxi2lc_boundary",
1461
+ "boundaryTitle": "ddxi2lc_boundaryTitle",
1462
+ "batchNote": "ddxi2lc_batchNote",
1463
+ "inputWide": "ddxi2lc_inputWide",
1464
+ "textarea": "ddxi2lc_textarea",
1465
+ "list": "ddxi2lc_list",
1466
+ "listRow": "ddxi2lc_listRow",
1467
+ "rowHead": "ddxi2lc_rowHead",
1468
+ "rowName": "ddxi2lc_rowName",
1469
+ "rowMeta": "ddxi2lc_rowMeta",
1470
+ "spacer": "ddxi2lc_spacer",
1471
+ "rowButton": "ddxi2lc_rowButton",
1472
+ "rowDanger": "ddxi2lc_rowDanger",
1473
+ "editor": "ddxi2lc_editor",
1474
+ "gridTwo": "ddxi2lc_gridTwo",
1475
+ "member": "ddxi2lc_member",
1476
+ "memberFields": "ddxi2lc_memberFields",
1477
+ "warn": "ddxi2lc_warn"
1478
+ };
1479
+ const tagId = "@y9931666/dsh-plugin-crossfire/SettingsView.module.css";
1480
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1481
+ const tag = document.createElement("style");
1482
+ tag.dataset.plugin = "@y9931666/dsh-plugin-crossfire";
1483
+ tag.dataset.pluginCss = tagId;
1484
+ tag.textContent = css;
1485
+ document.head.appendChild(tag);
1486
+ }
1487
+ var SettingsView_module_css_default = classMap;
1488
+
1489
+ //#endregion
1490
+ //#region src/client/SettingsView.tsx
1491
+ /** 与 host `DIMENSION_NAME_MAX` 一致。 */
1492
+ const DIMENSION_NAME_MAX = 12;
1493
+ /** 与 host `CUSTOM_DIMENSION_MAX` 一致。 */
1494
+ const CUSTOM_DIMENSION_MAX = 12;
1495
+ /** 与 host `ROLE_PRESET_MAX` 一致。 */
1496
+ const ROLE_PRESET_MAX = 50;
1497
+ /** 与 host `SQUAD_MAX` 一致。 */
1498
+ const SQUAD_MAX = 20;
1499
+ /** 与 host `SQUAD_MEMBER_MAX` 一致。 */
1500
+ const SQUAD_MEMBER_MAX = 8;
1501
+ const INTENSITY_ORDER = [
1502
+ "quick",
1503
+ "standard",
1504
+ "deep"
1505
+ ];
1506
+ const INTENSITY_LABEL_KEY = {
1507
+ quick: "intensityQuick",
1508
+ standard: "intensityStandard",
1509
+ deep: "intensityDeep"
1510
+ };
1511
+ /** 模型路由后缀使用的符号(无需新增 locale 键)。 */
1512
+ const ROUTE_SEPARATOR = " · ";
1513
+ const ROUTE_JOINER = " / ";
1514
+ const NUMERIC_FIELDS = [
1515
+ {
1516
+ key: "maxRounds",
1517
+ labelKey: "budgetRounds"
1518
+ },
1519
+ {
1520
+ key: "maxTokens",
1521
+ labelKey: "budgetTokens"
1522
+ },
1523
+ {
1524
+ key: "expertMaxTokens",
1525
+ labelKey: "expertMaxTokens"
1526
+ },
1527
+ {
1528
+ key: "expertMaxOpinions",
1529
+ labelKey: "expertMaxOpinions"
1530
+ }
1531
+ ];
1532
+ function draftFrom(prefs) {
1533
+ return {
1534
+ maxRounds: String(prefs.maxRounds),
1535
+ maxTokens: String(prefs.maxTokens),
1536
+ expertMaxTokens: String(prefs.expertMaxTokens),
1537
+ expertMaxOpinions: String(prefs.expertMaxOpinions)
1538
+ };
1539
+ }
1540
+ /** 成员 key 的合法化规则:与 host `sanitizeMemberKey` 逐字一致(小写 + 基本字符集)。 */
1541
+ const MEMBER_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
1542
+ /** 新建条目用的 id:优先浏览器原生 `crypto.randomUUID()`,RPC 只认非空 id。 */
1543
+ function newId() {
1544
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1545
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1546
+ const bytes = new Uint8Array(16);
1547
+ crypto.getRandomValues(bytes);
1548
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1549
+ }
1550
+ return `${String(Date.now())}-${String(Math.random()).slice(2)}`;
1551
+ }
1552
+ /** 表单里的字符串 → 可选字段(修剪后为空即视为"不设置")。 */
1553
+ function optionalText(value) {
1554
+ const trimmed = value.trim();
1555
+ return trimmed === "" ? void 0 : trimmed;
1556
+ }
1557
+ /** 草稿 → 合法预设;缺 name/role 时返回 null(调用方给 presetInvalid)。 */
1558
+ function presetFromDraft(draft) {
1559
+ const name = draft.name.trim();
1560
+ const role = draft.role.trim();
1561
+ if (name === "" || role === "") return null;
1562
+ const provider = optionalText(draft.provider);
1563
+ const model = optionalText(draft.model);
1564
+ return {
1565
+ id: draft.id,
1566
+ name,
1567
+ role,
1568
+ ...provider === void 0 ? {} : { provider },
1569
+ ...model === void 0 ? {} : { model }
1570
+ };
1571
+ }
1572
+ /** 草稿 → 合法阵容;name 为空或 key 不合法/重复/成员为空时返回 null。 */
1573
+ function squadFromDraft(draft) {
1574
+ const name = draft.name.trim();
1575
+ if (name === "") return null;
1576
+ const members = [];
1577
+ const seen = /* @__PURE__ */ new Set();
1578
+ for (const raw of draft.members) {
1579
+ const key = raw.key.trim().toLowerCase();
1580
+ if (!MEMBER_KEY_PATTERN.test(key) || seen.has(key)) return null;
1581
+ seen.add(key);
1582
+ const provider = optionalText(raw.provider);
1583
+ const model = optionalText(raw.model);
1584
+ members.push({
1585
+ key,
1586
+ role: raw.role.trim(),
1587
+ ...provider === void 0 ? {} : { provider },
1588
+ ...model === void 0 ? {} : { model }
1589
+ });
1590
+ }
1591
+ if (members.length === 0) return null;
1592
+ return {
1593
+ id: draft.id,
1594
+ name,
1595
+ members
1596
+ };
1597
+ }
1598
+ function memberDraft() {
1599
+ return {
1600
+ key: "",
1601
+ role: "",
1602
+ provider: "",
1603
+ model: ""
1604
+ };
1605
+ }
1606
+ /** 空行阵容(新建入口用;空成员行不会被净化掉,也不会被提交)。 */
1607
+ function newSquadDraft() {
1608
+ return {
1609
+ id: newId(),
1610
+ name: "",
1611
+ members: [memberDraft()]
1612
+ };
1613
+ }
1614
+ /** 排序用:小写副本,避免 "Alpha" 与 "alpha" 各自成群。 */
1615
+ function sortedByText(items, text) {
1616
+ return [...items].sort((left, right) => {
1617
+ const a = text(left).toLowerCase();
1618
+ const b = text(right).toLowerCase();
1619
+ if (a < b) return -1;
1620
+ return a > b ? 1 : 0;
1621
+ });
1622
+ }
1623
+ /** 角色说明在列表里的摘要长度。 */
1624
+ const ROLE_PREVIEW_MAX = 48;
1625
+ function previewRole(role) {
1626
+ const flat = role.replace(/\s+/g, " ").trim();
1627
+ return flat.length <= ROLE_PREVIEW_MAX ? flat : `${flat.slice(0, ROLE_PREVIEW_MAX)}…`;
1628
+ }
1629
+ /** 预设的模型路由后缀;没有 provider / model 时不显示。 */
1630
+ function presetRoute(preset) {
1631
+ if (preset.provider === void 0 && preset.model === void 0) return null;
1632
+ return ROUTE_SEPARATOR + [preset.provider, preset.model].filter((part) => part !== void 0).join(ROUTE_JOINER);
1633
+ }
1634
+ function CrossfireSettings(props) {
1635
+ const { rpc, t } = props;
1636
+ const [snapshot, setSnapshot] = (0, react.useState)(null);
1637
+ const [draft, setDraft] = (0, react.useState)(null);
1638
+ const [notice, setNotice] = (0, react.useState)("loading");
1639
+ const [newDimension, setNewDimension] = (0, react.useState)("");
1640
+ const [dimensionError, setDimensionError] = (0, react.useState)(false);
1641
+ const [busy, setBusy] = (0, react.useState)(false);
1642
+ const [saving, setSaving] = (0, react.useState)(false);
1643
+ const [providers, setProviders] = (0, react.useState)([]);
1644
+ const [catalog, setCatalog] = (0, react.useState)({ state: "loading" });
1645
+ const [presetEdit, setPresetEdit] = (0, react.useState)(null);
1646
+ const [presetError, setPresetError] = (0, react.useState)(null);
1647
+ const [presetNotice, setPresetNotice] = (0, react.useState)(null);
1648
+ const [squadEdit, setSquadEdit] = (0, react.useState)(null);
1649
+ const [squadError, setSquadError] = (0, react.useState)(null);
1650
+ const [squadNotice, setSquadNotice] = (0, react.useState)(null);
1651
+ const alive = (0, react.useRef)(true);
1652
+ (0, react.useEffect)(() => {
1653
+ alive.current = true;
1654
+ (async () => {
1655
+ const result = await rpc("crossfire/prefs.get", {});
1656
+ if (!alive.current) return;
1657
+ if (result.ok) {
1658
+ setSnapshot(result.value);
1659
+ setDraft(draftFrom(result.value.prefs));
1660
+ setNotice("saved");
1661
+ } else setNotice("loadFailed");
1662
+ const catalog$1 = await rpc("crossfire/models.list", {});
1663
+ if (!alive.current) return;
1664
+ if (catalog$1.ok && Array.isArray(catalog$1.value?.providers)) {
1665
+ setProviders(catalog$1.value.providers);
1666
+ setCatalog(catalog$1.value.providers.length === 0 ? { state: "empty" } : { state: "ready" });
1667
+ } else if (catalog$1.ok) setCatalog({
1668
+ state: "failed",
1669
+ reason: "unexpected payload"
1670
+ });
1671
+ else setCatalog({
1672
+ state: "failed",
1673
+ reason: catalog$1.error.message
1674
+ });
1675
+ })();
1676
+ return () => {
1677
+ alive.current = false;
1678
+ };
1679
+ }, [rpc]);
1680
+ /** 提交一次部分更新;成功后用服务端真相刷新本地快照。 */
1681
+ const save = async (patch) => {
1682
+ setBusy(true);
1683
+ const result = await rpc("crossfire/prefs.set", { patch });
1684
+ if (!alive.current) return false;
1685
+ setBusy(false);
1686
+ if (!result.ok) {
1687
+ setNotice("saveFailed");
1688
+ return false;
1689
+ }
1690
+ setNotice("saved");
1691
+ setSnapshot((previous) => previous === null ? previous : {
1692
+ ...previous,
1693
+ prefs: result.value.prefs,
1694
+ dimensions: result.value.dimensions
1695
+ });
1696
+ setDraft(draftFrom(result.value.prefs));
1697
+ return true;
1698
+ };
1699
+ /** 数字字段失焦提交:非法值回滚为当前服务端值。 */
1700
+ const commitNumber = (key) => {
1701
+ if (snapshot === null || draft === null) return;
1702
+ const raw = draft[key].trim();
1703
+ const parsed = raw === "" ? 0 : Number(raw);
1704
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) {
1705
+ setNotice("saveFailed");
1706
+ setDraft(draftFrom(snapshot.prefs));
1707
+ return;
1708
+ }
1709
+ if (parsed === snapshot.prefs[key]) return;
1710
+ save({ [key]: parsed });
1711
+ };
1712
+ const patchDraft = (key, value) => {
1713
+ setDraft((previous) => previous === null ? previous : {
1714
+ ...previous,
1715
+ [key]: value
1716
+ });
1717
+ };
1718
+ const addDimension = () => {
1719
+ if (snapshot === null) return;
1720
+ const candidate = newDimension.trim();
1721
+ if (candidate === "" || candidate.length > DIMENSION_NAME_MAX || snapshot.pool.includes(candidate) || snapshot.prefs.customDimensions.includes(candidate)) {
1722
+ setDimensionError(true);
1723
+ return;
1724
+ }
1725
+ setDimensionError(false);
1726
+ setNewDimension("");
1727
+ save({ customDimensions: [...snapshot.prefs.customDimensions, candidate] });
1728
+ };
1729
+ const removeDimension = (name) => {
1730
+ if (snapshot === null) return;
1731
+ save({ customDimensions: snapshot.prefs.customDimensions.filter((item) => item !== name) });
1732
+ };
1733
+ /** 打开编辑器(编辑已有条目时逐字段拷贝,绝不共享对象引用)。 */
1734
+ const beginEditPreset = (preset) => {
1735
+ setPresetError(null);
1736
+ setPresetNotice(null);
1737
+ setPresetEdit(preset === null ? {
1738
+ id: newId(),
1739
+ name: "",
1740
+ role: "",
1741
+ provider: "",
1742
+ model: ""
1743
+ } : {
1744
+ id: preset.id,
1745
+ name: preset.name,
1746
+ role: preset.role,
1747
+ provider: preset.provider ?? "",
1748
+ model: preset.model ?? ""
1749
+ });
1750
+ };
1751
+ /** 从列表里拿"服务端当前值":本地乐观写盘被 host 净化掉时用它回退。 */
1752
+ const storedPreset = (id) => snapshot?.prefs.rolePresets.find((item) => item.id === id);
1753
+ const savePreset = () => {
1754
+ if (snapshot === null || presetEdit === null) return;
1755
+ const preset = presetFromDraft(presetEdit);
1756
+ if (preset === null) {
1757
+ setPresetError("invalid");
1758
+ return;
1759
+ }
1760
+ if (storedPreset(preset.id) === void 0 && snapshot.prefs.rolePresets.length >= ROLE_PRESET_MAX) {
1761
+ setPresetError("limit");
1762
+ return;
1763
+ }
1764
+ setPresetError(null);
1765
+ setPresetNotice(null);
1766
+ setSaving(true);
1767
+ (async () => {
1768
+ const ok = await save({ rolePresets: mergePreset(snapshot.prefs.rolePresets, preset) });
1769
+ if (!alive.current) return;
1770
+ setSaving(false);
1771
+ if (!ok) {
1772
+ setPresetNotice("failed");
1773
+ return;
1774
+ }
1775
+ setPresetEdit(null);
1776
+ setPresetNotice("saved");
1777
+ })();
1778
+ };
1779
+ const deletePreset = (id) => {
1780
+ if (snapshot === null) return;
1781
+ setPresetError(null);
1782
+ setPresetNotice(null);
1783
+ if (presetEdit !== null && presetEdit.id === id) setPresetEdit(null);
1784
+ save({ rolePresets: snapshot.prefs.rolePresets.filter((item) => item.id !== id) });
1785
+ };
1786
+ const beginEditSquad = (squad) => {
1787
+ setSquadError(null);
1788
+ setSquadNotice(null);
1789
+ setSquadEdit(squad === null ? newSquadDraft() : {
1790
+ id: squad.id,
1791
+ name: squad.name,
1792
+ members: squad.members.length === 0 ? [memberDraft()] : squad.members.map((member) => ({
1793
+ key: member.key,
1794
+ role: member.role,
1795
+ provider: member.provider ?? "",
1796
+ model: member.model ?? ""
1797
+ }))
1798
+ });
1799
+ };
1800
+ const storedSquad = (id) => snapshot?.prefs.squads.find((item) => item.id === id);
1801
+ const patchSquadDraft = (apply$1) => {
1802
+ setSquadEdit((previous) => previous === null ? previous : apply$1(previous));
1803
+ };
1804
+ const patchSquadMember = (index, patch) => {
1805
+ patchSquadDraft((current) => ({
1806
+ ...current,
1807
+ members: current.members.map((member, position) => position === index ? {
1808
+ ...member,
1809
+ ...patch
1810
+ } : member)
1811
+ }));
1812
+ };
1813
+ const addSquadMember = () => {
1814
+ patchSquadDraft((current) => current.members.length >= SQUAD_MEMBER_MAX ? current : {
1815
+ ...current,
1816
+ members: [...current.members, memberDraft()]
1817
+ });
1818
+ };
1819
+ const removeSquadMember = (index) => {
1820
+ patchSquadDraft((current) => {
1821
+ const members = current.members.filter((_member, position) => position !== index);
1822
+ return {
1823
+ ...current,
1824
+ members: members.length === 0 ? [memberDraft()] : members
1825
+ };
1826
+ });
1827
+ };
1828
+ const saveSquad = () => {
1829
+ if (snapshot === null || squadEdit === null) return;
1830
+ const squad = squadFromDraft(squadEdit);
1831
+ if (squad === null) {
1832
+ setSquadError("invalid");
1833
+ return;
1834
+ }
1835
+ if (storedSquad(squad.id) === void 0 && snapshot.prefs.squads.length >= SQUAD_MAX) {
1836
+ setSquadError("limit");
1837
+ return;
1838
+ }
1839
+ setSquadError(null);
1840
+ setSquadNotice(null);
1841
+ setSaving(true);
1842
+ (async () => {
1843
+ const ok = await save({ squads: mergeSquad(snapshot.prefs.squads, squad) });
1844
+ if (!alive.current) return;
1845
+ setSaving(false);
1846
+ if (!ok) {
1847
+ setSquadNotice("failed");
1848
+ return;
1849
+ }
1850
+ setSquadEdit(null);
1851
+ setSquadNotice("saved");
1852
+ })();
1853
+ };
1854
+ const deleteSquad = (id) => {
1855
+ if (snapshot === null) return;
1856
+ setSquadError(null);
1857
+ setSquadNotice(null);
1858
+ if (squadEdit !== null && squadEdit.id === id) setSquadEdit(null);
1859
+ save({ squads: snapshot.prefs.squads.filter((item) => item.id !== id) });
1860
+ };
1861
+ if (snapshot === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1862
+ className: SettingsView_module_css_default.root,
1863
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1864
+ className: notice === "loadFailed" ? `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeErr}` : `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.hint}`,
1865
+ children: t(notice === "loadFailed" ? "loadFailed" : "loading")
1866
+ })
1867
+ });
1868
+ const activeSpec = snapshot.intensities[snapshot.prefs.defaultIntensity];
1869
+ const atDimensionLimit = snapshot.prefs.customDimensions.length >= CUSTOM_DIMENSION_MAX;
1870
+ const presets = sortedByText(snapshot.prefs.rolePresets, (preset) => preset.name);
1871
+ const squads = sortedByText(snapshot.prefs.squads, (squad) => squad.name);
1872
+ const atPresetLimit = snapshot.prefs.rolePresets.length >= ROLE_PRESET_MAX;
1873
+ const atSquadLimit = snapshot.prefs.squads.length >= SQUAD_MAX;
1874
+ const crossText = activeSpec !== void 0 && activeSpec.crossExamination ? t("intensityCrossYes") : t("intensityCrossNo");
1875
+ const perDimensionText = t("intensityPerDimension").replace("{n}", String(activeSpec === void 0 ? 0 : activeSpec.expertsPerDimension));
1876
+ const recheckCapText = activeSpec === void 0 ? "" : t("intensityRecheckCap").replace("{n}", String(activeSpec.maxReviewPass));
1877
+ const anySaving = busy || saving;
1878
+ const hiddenHintCount = Object.keys(snapshot.prefs.dismissedHints ?? {}).length;
1879
+ const restoreHints = () => {
1880
+ save({ dismissedHints: {} });
1881
+ };
1882
+ const catalogHint = catalog.state === "loading" ? {
1883
+ text: t("modelCatalogLoading"),
1884
+ warn: false
1885
+ } : catalog.state === "failed" ? {
1886
+ text: t("modelCatalogFailed").replace("{reason}", catalog.reason ?? ""),
1887
+ warn: true
1888
+ } : catalog.state === "empty" ? {
1889
+ text: t("modelCatalogEmpty"),
1890
+ warn: true
1891
+ } : null;
1892
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1893
+ className: SettingsView_module_css_default.root,
1894
+ children: [
1895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1896
+ className: SettingsView_module_css_default.section,
1897
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1898
+ className: SettingsView_module_css_default.sectionTitle,
1899
+ children: t("settingsTitle")
1900
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1901
+ className: SettingsView_module_css_default.hint,
1902
+ children: t("settingsIntro")
1903
+ })]
1904
+ }),
1905
+ notice === "saved" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1906
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeOk}`,
1907
+ children: t("saved")
1908
+ }) : null,
1909
+ notice === "saveFailed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1910
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeErr}`,
1911
+ children: t("saveFailed")
1912
+ }) : null,
1913
+ catalogHint === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1914
+ className: catalogHint.warn ? `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.warn}` : SettingsView_module_css_default.hint,
1915
+ children: catalogHint.text
1916
+ }),
1917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1918
+ className: SettingsView_module_css_default.section,
1919
+ children: [
1920
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1921
+ className: SettingsView_module_css_default.label,
1922
+ children: t("defaultIntensity")
1923
+ }),
1924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1925
+ className: SettingsView_module_css_default.segmented,
1926
+ children: INTENSITY_ORDER.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1927
+ type: "button",
1928
+ disabled: busy,
1929
+ className: snapshot.prefs.defaultIntensity === option ? `${SettingsView_module_css_default.segment} ${SettingsView_module_css_default.segmentActive}` : SettingsView_module_css_default.segment,
1930
+ onClick: () => {
1931
+ if (snapshot.prefs.defaultIntensity === option) return;
1932
+ save({ defaultIntensity: option });
1933
+ },
1934
+ children: t(INTENSITY_LABEL_KEY[option])
1935
+ }, option))
1936
+ }),
1937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1938
+ className: SettingsView_module_css_default.hint,
1939
+ children: [
1940
+ t("intensityHint"),
1941
+ " ",
1942
+ `(${perDimensionText} / ${crossText} / ${recheckCapText})`
1943
+ ]
1944
+ })
1945
+ ]
1946
+ }),
1947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1948
+ className: SettingsView_module_css_default.section,
1949
+ children: [
1950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1951
+ className: SettingsView_module_css_default.sectionTitle,
1952
+ children: t("dimensionsTitle")
1953
+ }),
1954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1955
+ className: SettingsView_module_css_default.hint,
1956
+ children: t("dimensionsHint")
1957
+ }),
1958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1959
+ className: SettingsView_module_css_default.label,
1960
+ children: t("poolLabel")
1961
+ }),
1962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1963
+ className: SettingsView_module_css_default.chips,
1964
+ children: snapshot.pool.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1965
+ className: SettingsView_module_css_default.chip,
1966
+ children: name
1967
+ }, name))
1968
+ }),
1969
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1970
+ className: SettingsView_module_css_default.label,
1971
+ children: t("customLabel")
1972
+ }),
1973
+ snapshot.prefs.customDimensions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1974
+ className: SettingsView_module_css_default.hint,
1975
+ children: t("customEmpty")
1976
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1977
+ className: SettingsView_module_css_default.chips,
1978
+ children: snapshot.prefs.customDimensions.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1979
+ className: SettingsView_module_css_default.chipCustom,
1980
+ children: [name, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1981
+ type: "button",
1982
+ className: SettingsView_module_css_default.chipRemove,
1983
+ disabled: busy,
1984
+ title: t("customRemove"),
1985
+ "aria-label": `${t("customRemove")} ${name}`,
1986
+ onClick: () => removeDimension(name),
1987
+ children: "×"
1988
+ })]
1989
+ }, name))
1990
+ }),
1991
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1992
+ className: SettingsView_module_css_default.addRow,
1993
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1994
+ className: SettingsView_module_css_default.input,
1995
+ value: newDimension,
1996
+ maxLength: DIMENSION_NAME_MAX,
1997
+ placeholder: t("customPlaceholder"),
1998
+ "aria-label": t("customAdd"),
1999
+ onChange: (event) => {
2000
+ setNewDimension(event.target.value);
2001
+ setDimensionError(false);
2002
+ },
2003
+ onKeyDown: (event) => {
2004
+ if (event.key === "Enter") addDimension();
2005
+ }
2006
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2007
+ type: "button",
2008
+ className: SettingsView_module_css_default.addButton,
2009
+ disabled: busy || atDimensionLimit,
2010
+ onClick: addDimension,
2011
+ children: t("customAdd")
2012
+ })]
2013
+ }),
2014
+ dimensionError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2015
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeErr}`,
2016
+ children: t("customInvalid").replace("{len}", String(DIMENSION_NAME_MAX))
2017
+ }) : null,
2018
+ atDimensionLimit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2019
+ className: SettingsView_module_css_default.hint,
2020
+ children: t("customLimit").replace("{max}", String(CUSTOM_DIMENSION_MAX)).replace("{len}", String(DIMENSION_NAME_MAX))
2021
+ }) : null
2022
+ ]
2023
+ }),
2024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2025
+ className: SettingsView_module_css_default.section,
2026
+ children: [
2027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2028
+ className: SettingsView_module_css_default.sectionTitle,
2029
+ children: t("budgetTitle")
2030
+ }),
2031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2032
+ className: SettingsView_module_css_default.hint,
2033
+ children: t("budgetHint")
2034
+ }),
2035
+ NUMERIC_FIELDS.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2036
+ className: SettingsView_module_css_default.field,
2037
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2038
+ className: SettingsView_module_css_default.label,
2039
+ htmlFor: `cf-${field.key}`,
2040
+ children: t(field.labelKey)
2041
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2042
+ id: `cf-${field.key}`,
2043
+ className: SettingsView_module_css_default.input,
2044
+ type: "number",
2045
+ min: 0,
2046
+ step: field.key === "maxTokens" ? 1e3 : 1,
2047
+ value: draft === null ? "" : draft[field.key],
2048
+ onChange: (event) => patchDraft(field.key, event.target.value),
2049
+ onBlur: () => commitNumber(field.key)
2050
+ })]
2051
+ }, field.key)),
2052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2053
+ className: SettingsView_module_css_default.hint,
2054
+ children: t("unlimited")
2055
+ })
2056
+ ]
2057
+ }),
2058
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2059
+ className: SettingsView_module_css_default.section,
2060
+ children: [
2061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2062
+ className: SettingsView_module_css_default.sectionHead,
2063
+ children: [
2064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2065
+ className: SettingsView_module_css_default.sectionTitle,
2066
+ children: t("settingsPresetsTitle")
2067
+ }),
2068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsView_module_css_default.sectionHeadGap }),
2069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2070
+ type: "button",
2071
+ className: SettingsView_module_css_default.primaryButton,
2072
+ disabled: busy || atPresetLimit,
2073
+ title: atPresetLimit ? t("presetLimitInline").replace("{max}", String(ROLE_PRESET_MAX)) : void 0,
2074
+ onClick: () => beginEditPreset(null),
2075
+ children: t("presetAddCount").replace("{count}", String(snapshot.prefs.rolePresets.length)).replace("{max}", String(ROLE_PRESET_MAX))
2076
+ })
2077
+ ]
2078
+ }),
2079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2080
+ className: SettingsView_module_css_default.hint,
2081
+ children: t("settingsPresetsHint")
2082
+ }),
2083
+ presetNotice === "saved" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2084
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeOk}`,
2085
+ children: t("presetSaved")
2086
+ }) : null,
2087
+ presetNotice === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2088
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeErr}`,
2089
+ children: t("saveFailed")
2090
+ }) : null,
2091
+ presetError === "invalid" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2092
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeErr}`,
2093
+ children: t("presetInvalid")
2094
+ }) : null,
2095
+ presetError === "limit" || atPresetLimit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2096
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.warn}`,
2097
+ children: t("presetLimit").replace("{max}", String(ROLE_PRESET_MAX))
2098
+ }) : null,
2099
+ snapshot.prefs.rolePresets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2100
+ className: SettingsView_module_css_default.hint,
2101
+ children: t("settingsPresetsEmpty")
2102
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2103
+ className: SettingsView_module_css_default.list,
2104
+ children: presets.map((preset) => {
2105
+ const route = presetRoute(preset);
2106
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2107
+ className: SettingsView_module_css_default.listRow,
2108
+ children: [
2109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2110
+ className: SettingsView_module_css_default.rowHead,
2111
+ children: [
2112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2113
+ className: SettingsView_module_css_default.rowName,
2114
+ children: preset.name
2115
+ }),
2116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsView_module_css_default.spacer }),
2117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2118
+ type: "button",
2119
+ className: SettingsView_module_css_default.rowButton,
2120
+ disabled: busy,
2121
+ onClick: () => beginEditPreset(preset),
2122
+ children: t("presetEdit")
2123
+ }),
2124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2125
+ type: "button",
2126
+ className: SettingsView_module_css_default.rowDanger,
2127
+ disabled: busy,
2128
+ onClick: () => deletePreset(preset.id),
2129
+ children: t("presetDelete")
2130
+ })
2131
+ ]
2132
+ }),
2133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2134
+ className: SettingsView_module_css_default.rowMeta,
2135
+ children: previewRole(preset.role)
2136
+ }),
2137
+ route === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2138
+ className: SettingsView_module_css_default.routeRow,
2139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2140
+ className: SettingsView_module_css_default.brandBlock,
2141
+ style: { background: providerBrand(preset.provider).color },
2142
+ title: routeText(preset.provider, preset.model),
2143
+ children: routeAbbr(preset.provider, preset.model)
2144
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2145
+ className: SettingsView_module_css_default.rowMeta,
2146
+ children: `${t("presetProvider")}${route}`
2147
+ })]
2148
+ })
2149
+ ]
2150
+ }, preset.id);
2151
+ })
2152
+ }),
2153
+ presetEdit === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2154
+ className: SettingsView_module_css_default.editor,
2155
+ children: [
2156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2157
+ className: SettingsView_module_css_default.field,
2158
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2159
+ className: SettingsView_module_css_default.label,
2160
+ htmlFor: "cf-preset-name",
2161
+ children: t("presetName")
2162
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2163
+ id: "cf-preset-name",
2164
+ className: SettingsView_module_css_default.inputWide,
2165
+ value: presetEdit.name,
2166
+ onChange: (event) => {
2167
+ const value = event.target.value;
2168
+ setPresetEdit((previous) => previous === null ? previous : {
2169
+ ...previous,
2170
+ name: value
2171
+ });
2172
+ }
2173
+ })]
2174
+ }),
2175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2176
+ className: SettingsView_module_css_default.field,
2177
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2178
+ className: SettingsView_module_css_default.label,
2179
+ htmlFor: "cf-preset-role",
2180
+ children: t("presetRole")
2181
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2182
+ id: "cf-preset-role",
2183
+ className: SettingsView_module_css_default.textarea,
2184
+ rows: 3,
2185
+ value: presetEdit.role,
2186
+ onChange: (event) => {
2187
+ const value = event.target.value;
2188
+ setPresetEdit((previous) => previous === null ? previous : {
2189
+ ...previous,
2190
+ role: value
2191
+ });
2192
+ }
2193
+ })]
2194
+ }),
2195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2196
+ className: SettingsView_module_css_default.gridTwo,
2197
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2198
+ className: SettingsView_module_css_default.field,
2199
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2200
+ className: SettingsView_module_css_default.label,
2201
+ htmlFor: "cf-preset-provider",
2202
+ children: t("presetProvider")
2203
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2204
+ id: "cf-preset-provider",
2205
+ className: SettingsView_module_css_default.inputWide,
2206
+ value: presetEdit.provider,
2207
+ onChange: (event) => {
2208
+ const value = event.target.value;
2209
+ setPresetEdit((previous) => previous === null ? previous : {
2210
+ ...previous,
2211
+ provider: value,
2212
+ model: ""
2213
+ });
2214
+ },
2215
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2216
+ value: "",
2217
+ children: t("modelInherit")
2218
+ }), providers.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2219
+ value: provider.id,
2220
+ children: provider.name === "" ? provider.id : provider.name
2221
+ }, provider.id))]
2222
+ })]
2223
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2224
+ className: SettingsView_module_css_default.field,
2225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2226
+ className: SettingsView_module_css_default.label,
2227
+ htmlFor: "cf-preset-model",
2228
+ children: t("presetModel")
2229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2230
+ id: "cf-preset-model",
2231
+ className: SettingsView_module_css_default.inputWide,
2232
+ value: presetEdit.model,
2233
+ disabled: presetEdit.provider === "",
2234
+ title: presetEdit.provider === "" ? t("presetProviderNeeded") : void 0,
2235
+ onChange: (event) => {
2236
+ const value = event.target.value;
2237
+ setPresetEdit((previous) => previous === null ? previous : {
2238
+ ...previous,
2239
+ model: value
2240
+ });
2241
+ },
2242
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2243
+ value: "",
2244
+ children: t("modelInherit")
2245
+ }), (providers.find((provider) => provider.id === presetEdit.provider)?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2246
+ value: model.id,
2247
+ children: model.name === "" ? model.id : model.name
2248
+ }, model.id))]
2249
+ })]
2250
+ })]
2251
+ }),
2252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2253
+ className: SettingsView_module_css_default.addRow,
2254
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2255
+ type: "button",
2256
+ className: SettingsView_module_css_default.addButton,
2257
+ disabled: anySaving,
2258
+ onClick: savePreset,
2259
+ children: t("presetSave")
2260
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2261
+ type: "button",
2262
+ className: SettingsView_module_css_default.addButton,
2263
+ disabled: anySaving,
2264
+ onClick: () => {
2265
+ setPresetEdit(null);
2266
+ setPresetError(null);
2267
+ },
2268
+ children: t("presetCancel")
2269
+ })]
2270
+ })
2271
+ ]
2272
+ })
2273
+ ]
2274
+ }),
2275
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2276
+ className: SettingsView_module_css_default.section,
2277
+ children: [
2278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2279
+ className: SettingsView_module_css_default.sectionHead,
2280
+ children: [
2281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2282
+ className: SettingsView_module_css_default.sectionTitle,
2283
+ children: t("settingsSquadsTitle")
2284
+ }),
2285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsView_module_css_default.sectionHeadGap }),
2286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2287
+ type: "button",
2288
+ className: SettingsView_module_css_default.primaryButton,
2289
+ disabled: busy || atSquadLimit,
2290
+ title: atSquadLimit ? t("squadLimit").replace("{max}", String(SQUAD_MAX)).replace("{members}", String(SQUAD_MEMBER_MAX)) : void 0,
2291
+ onClick: () => beginEditSquad(null),
2292
+ children: t("squadAddCount").replace("{count}", String(snapshot.prefs.squads.length)).replace("{max}", String(SQUAD_MAX))
2293
+ })
2294
+ ]
2295
+ }),
2296
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2297
+ className: SettingsView_module_css_default.hint,
2298
+ children: t("settingsSquadsHint")
2299
+ }),
2300
+ squadNotice === "saved" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2301
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeOk}`,
2302
+ children: t("squadSaved")
2303
+ }) : null,
2304
+ squadNotice === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2305
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeErr}`,
2306
+ children: t("saveFailed")
2307
+ }) : null,
2308
+ squadError === "invalid" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2309
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.noticeErr}`,
2310
+ children: t("squadInvalid")
2311
+ }) : null,
2312
+ squadError === "limit" || atSquadLimit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2313
+ className: `${SettingsView_module_css_default.notice} ${SettingsView_module_css_default.warn}`,
2314
+ children: t("squadLimit").replace("{max}", String(SQUAD_MAX)).replace("{members}", String(SQUAD_MEMBER_MAX))
2315
+ }) : null,
2316
+ snapshot.prefs.squads.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2317
+ className: SettingsView_module_css_default.hint,
2318
+ children: t("settingsSquadsEmpty")
2319
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2320
+ className: SettingsView_module_css_default.list,
2321
+ children: squads.map((squad) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2322
+ className: SettingsView_module_css_default.listRow,
2323
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2324
+ className: SettingsView_module_css_default.rowHead,
2325
+ children: [
2326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2327
+ className: SettingsView_module_css_default.rowName,
2328
+ children: squad.name
2329
+ }),
2330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2331
+ className: SettingsView_module_css_default.rowMeta,
2332
+ children: t("squadMemberCount").replace("{count}", String(squad.members.length))
2333
+ }),
2334
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsView_module_css_default.spacer }),
2335
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2336
+ type: "button",
2337
+ className: SettingsView_module_css_default.rowButton,
2338
+ disabled: busy,
2339
+ onClick: () => beginEditSquad(squad),
2340
+ children: t("squadEdit")
2341
+ }),
2342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2343
+ type: "button",
2344
+ className: SettingsView_module_css_default.rowDanger,
2345
+ disabled: busy,
2346
+ onClick: () => deleteSquad(squad.id),
2347
+ children: t("squadDelete")
2348
+ })
2349
+ ]
2350
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2351
+ className: SettingsView_module_css_default.chips,
2352
+ children: squad.members.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2353
+ className: SettingsView_module_css_default.chipWithBrand,
2354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2355
+ className: SettingsView_module_css_default.brandBlockSmall,
2356
+ style: { background: providerBrand(member.provider).color },
2357
+ title: member.provider === void 0 ? t("modelInherit") : routeText(member.provider, member.model),
2358
+ children: providerBrand(member.provider).abbr
2359
+ }), member.key]
2360
+ }, member.key))
2361
+ })]
2362
+ }, squad.id))
2363
+ }),
2364
+ squadEdit === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2365
+ className: SettingsView_module_css_default.editor,
2366
+ children: [
2367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2368
+ className: SettingsView_module_css_default.field,
2369
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2370
+ className: SettingsView_module_css_default.label,
2371
+ htmlFor: "cf-squad-name",
2372
+ children: t("squadName")
2373
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2374
+ id: "cf-squad-name",
2375
+ className: SettingsView_module_css_default.inputWide,
2376
+ value: squadEdit.name,
2377
+ onChange: (event) => {
2378
+ const value = event.target.value;
2379
+ patchSquadDraft((current) => ({
2380
+ ...current,
2381
+ name: value
2382
+ }));
2383
+ }
2384
+ })]
2385
+ }),
2386
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2387
+ className: SettingsView_module_css_default.label,
2388
+ children: t("squadMembers")
2389
+ }),
2390
+ squadEdit.members.map((member, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2391
+ className: SettingsView_module_css_default.member,
2392
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2393
+ className: SettingsView_module_css_default.memberFields,
2394
+ children: [
2395
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2396
+ className: SettingsView_module_css_default.field,
2397
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2398
+ className: SettingsView_module_css_default.label,
2399
+ htmlFor: `cf-squad-key-${String(index)}`,
2400
+ children: t("squadMemberKey")
2401
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2402
+ id: `cf-squad-key-${String(index)}`,
2403
+ className: SettingsView_module_css_default.inputWide,
2404
+ value: member.key,
2405
+ maxLength: 32,
2406
+ onChange: (event) => patchSquadMember(index, { key: event.target.value })
2407
+ })]
2408
+ }),
2409
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2410
+ className: SettingsView_module_css_default.field,
2411
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2412
+ className: SettingsView_module_css_default.label,
2413
+ htmlFor: `cf-squad-role-${String(index)}`,
2414
+ children: t("squadMemberRole")
2415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2416
+ id: `cf-squad-role-${String(index)}`,
2417
+ className: SettingsView_module_css_default.inputWide,
2418
+ value: member.role,
2419
+ onChange: (event) => patchSquadMember(index, { role: event.target.value })
2420
+ })]
2421
+ }),
2422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2423
+ className: SettingsView_module_css_default.field,
2424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2425
+ className: SettingsView_module_css_default.label,
2426
+ htmlFor: `cf-squad-provider-${String(index)}`,
2427
+ children: t("presetProvider")
2428
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2429
+ id: `cf-squad-provider-${String(index)}`,
2430
+ className: SettingsView_module_css_default.inputWide,
2431
+ value: member.provider,
2432
+ onChange: (event) => patchSquadMember(index, {
2433
+ provider: event.target.value,
2434
+ model: ""
2435
+ }),
2436
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2437
+ value: "",
2438
+ children: t("modelInherit")
2439
+ }), providers.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2440
+ value: provider.id,
2441
+ children: provider.name === "" ? provider.id : provider.name
2442
+ }, provider.id))]
2443
+ })]
2444
+ }),
2445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2446
+ className: SettingsView_module_css_default.field,
2447
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2448
+ className: SettingsView_module_css_default.label,
2449
+ htmlFor: `cf-squad-model-${String(index)}`,
2450
+ children: t("presetModel")
2451
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2452
+ id: `cf-squad-model-${String(index)}`,
2453
+ className: SettingsView_module_css_default.inputWide,
2454
+ value: member.model,
2455
+ disabled: member.provider === void 0 || member.provider === "",
2456
+ title: member.provider === void 0 || member.provider === "" ? t("presetProviderNeeded") : void 0,
2457
+ onChange: (event) => patchSquadMember(index, { model: event.target.value }),
2458
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2459
+ value: "",
2460
+ children: t("modelInherit")
2461
+ }), (providers.find((provider) => provider.id === member.provider)?.models ?? []).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2462
+ value: model.id,
2463
+ children: model.name === "" ? model.id : model.name
2464
+ }, model.id))]
2465
+ })]
2466
+ })
2467
+ ]
2468
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2469
+ type: "button",
2470
+ className: SettingsView_module_css_default.rowDanger,
2471
+ title: t("squadRemoveMember"),
2472
+ "aria-label": `${t("squadRemoveMember")} ${String(index + 1)}`,
2473
+ onClick: () => removeSquadMember(index),
2474
+ children: t("squadRemoveMember")
2475
+ })]
2476
+ }, `member-${String(index)}`)),
2477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2478
+ className: SettingsView_module_css_default.addRow,
2479
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2480
+ type: "button",
2481
+ className: SettingsView_module_css_default.addButton,
2482
+ disabled: squadEdit.members.length >= SQUAD_MEMBER_MAX,
2483
+ onClick: addSquadMember,
2484
+ children: t("squadAddMember")
2485
+ })
2486
+ }),
2487
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2488
+ className: SettingsView_module_css_default.addRow,
2489
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2490
+ type: "button",
2491
+ className: SettingsView_module_css_default.addButton,
2492
+ disabled: anySaving,
2493
+ onClick: saveSquad,
2494
+ children: t("squadSave")
2495
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2496
+ type: "button",
2497
+ className: SettingsView_module_css_default.addButton,
2498
+ disabled: anySaving,
2499
+ onClick: () => {
2500
+ setSquadEdit(null);
2501
+ setSquadError(null);
2502
+ },
2503
+ children: t("squadCancel")
2504
+ })]
2505
+ })
2506
+ ]
2507
+ })
2508
+ ]
2509
+ }),
2510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2511
+ className: SettingsView_module_css_default.section,
2512
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2513
+ className: SettingsView_module_css_default.label,
2514
+ children: t("settingsExplainTitle")
2515
+ }), hiddenHintCount === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2516
+ className: SettingsView_module_css_default.hint,
2517
+ children: t("settingsExplainNone")
2518
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2519
+ className: SettingsView_module_css_default.addRow,
2520
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2521
+ className: SettingsView_module_css_default.hint,
2522
+ children: t("settingsExplainCount").replace("{n}", String(hiddenHintCount))
2523
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2524
+ type: "button",
2525
+ className: SettingsView_module_css_default.addButton,
2526
+ disabled: busy,
2527
+ onClick: restoreHints,
2528
+ children: t("settingsExplainRestore")
2529
+ })]
2530
+ })]
2531
+ }),
2532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2533
+ className: SettingsView_module_css_default.boundary,
2534
+ children: [
2535
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2536
+ className: SettingsView_module_css_default.boundaryTitle,
2537
+ children: t("boundaryTitle")
2538
+ }),
2539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("boundaryLine1") }),
2540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("boundaryLine2") }),
2541
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("boundaryLine3") }),
2542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("boundaryLine4") })
2543
+ ]
2544
+ }),
2545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2546
+ className: SettingsView_module_css_default.batchNote,
2547
+ children: t("batchNote")
2548
+ })
2549
+ ]
2550
+ });
2551
+ }
2552
+
2553
+ //#endregion
2554
+ //#region src/client/slot-boundary.tsx
2555
+ /** Class-component boundary (React only catches render errors in class components). */
2556
+ var SlotBoundary = class extends react.Component {
2557
+ state = { error: null };
2558
+ static getDerivedStateFromError(error) {
2559
+ return { error };
2560
+ }
2561
+ componentDidCatch(error, info) {
2562
+ console.error("[crossfire] slot entry render failed:", error, info.componentStack);
2563
+ }
2564
+ render() {
2565
+ const { error } = this.state;
2566
+ if (error === null) return this.props.children;
2567
+ return (0, react.createElement)("div", { style: {
2568
+ margin: "16px",
2569
+ padding: "14px 16px",
2570
+ border: "1px solid var(--dsw-alias-state-error-primary, #d9534f)",
2571
+ borderRadius: "8px",
2572
+ background: "var(--dsw-alias-bg-layer-2, rgba(217, 83, 79, 0.06))",
2573
+ color: "var(--dsw-alias-label-primary, inherit)",
2574
+ font: "inherit",
2575
+ lineHeight: 1.6
2576
+ } }, (0, react.createElement)("div", { style: {
2577
+ fontWeight: 600,
2578
+ marginBottom: "6px"
2579
+ } }, this.props.title), (0, react.createElement)("pre", { style: {
2580
+ margin: "0 0 8px",
2581
+ whiteSpace: "pre-wrap",
2582
+ wordBreak: "break-word",
2583
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
2584
+ fontSize: "12px",
2585
+ opacity: .9
2586
+ } }, `${error.name}: ${error.message}`), (0, react.createElement)("div", { style: {
2587
+ fontSize: "12px",
2588
+ opacity: .75
2589
+ } }, this.props.hint));
2590
+ }
2591
+ };
2592
+ /**
2593
+ * Wrap one slot component so a render crash renders a report instead of
2594
+ * vanishing. The inner component keeps receiving every prop it got before
2595
+ * (owner share + standard seats + the inject face), so this is transparent on
2596
+ * the happy path.
2597
+ *
2598
+ * @param Inner - the registered slot component.
2599
+ * @returns a component with the same props contract.
2600
+ */
2601
+ function withSlotBoundary(Inner) {
2602
+ const Wrapped = (props) => {
2603
+ const face = props;
2604
+ const t = typeof face.t === "function" ? face.t : (key) => key;
2605
+ return (0, react.createElement)(SlotBoundary, {
2606
+ title: t("renderFailed"),
2607
+ hint: t("renderFailedHint")
2608
+ }, (0, react.createElement)(Inner, props));
2609
+ };
2610
+ const named = Inner;
2611
+ Wrapped.displayName = `CrossfireBoundary(${named.displayName ?? named.name ?? "Slot"})`;
2612
+ return Wrapped;
2613
+ }
2614
+
2615
+ //#endregion
2616
+ //#region src/client/locales.ts
2617
+ /**
2618
+ * Locale dictionaries for the Crossfire browser UI.
2619
+ *
2620
+ * 命名空间 `crossfire` 与圆桌会议的 `roundtable` **完全分开**:两个插件共享一个
2621
+ * 命名空间会让任意一方加键就撞上另一方。
2622
+ *
2623
+ * @module dsh-plugin-crossfire/client/locales
2624
+ */
2625
+ const NS = "crossfire";
2626
+ const zh = {
2627
+ renderFailed: "针锋的这一块渲染失败了",
2628
+ renderFailedHint: "这不是数据丢失:刷新页面即可重试。若反复出现,请把控制台里 [crossfire] 开头的那行错误一并反馈。",
2629
+ tab: "针锋",
2630
+ settingsNav: "针锋",
2631
+ settingsTitle: "针锋 Crossfire",
2632
+ settingsIntro: "针锋只做一件事:把一个已经定稿的成果或方案摆上台,让红队专家从指定维度分头攻击,把攻击拆成逐条可裁决的缺陷。下面的设置是**默认值**,只影响新建的评审;已经开始的评审不受影响。",
2633
+ boundaryTitle: "四条边界",
2634
+ boundaryLine1: "1. 表态永远由人做:认定 / 驳回只能在卡片上由你操作,没有\"代表认定缺陷\"的工具。",
2635
+ boundaryLine2: "2. 针锋不产出方案、不改代码、不自动修复——它是验收器,不是执行器。",
2636
+ boundaryLine3: "3. 红队专家只读:它们只挑错,不动你的工作区。",
2637
+ boundaryLine4: "4. 你的历史表态只用于去重与复审递进,绝不会被喂给红队当\"用户画像\"。",
2638
+ defaultIntensity: "默认强度",
2639
+ intensityQuick: "快审",
2640
+ intensityStandard: "标准",
2641
+ intensityDeep: "深挖",
2642
+ intensityHint: "强度只改变专家数、是否追加交叉质证、以及闭环复审的上限,不改变界面结构。",
2643
+ intensityPerDimension: "每维 {n} 位专家",
2644
+ intensityCrossYes: "含交叉质证",
2645
+ intensityCrossNo: "无交叉质证",
2646
+ intensityRecheckCap: "复审上限 {n} 轮",
2647
+ dimensionsTitle: "维度池",
2648
+ dimensionsHint: "维度决定红队从哪些方向攻击。固定池保证覆盖面,自定义维度用来补你自己关心的角度。",
2649
+ poolLabel: "固定维度(始终可用)",
2650
+ customLabel: "自定义维度",
2651
+ customEmpty: "(还没有自定义维度)",
2652
+ customPlaceholder: "如「合规」「成本」",
2653
+ customAdd: "+ 添加",
2654
+ customRemove: "删除",
2655
+ customLimit: "自定义维度最多 {max} 个,每个不超过 {len} 字",
2656
+ customInvalid: "维度名不能为空、不能超过 {len} 字,也不能与固定维度重名",
2657
+ budgetTitle: "预算",
2658
+ budgetHint: "预算是熔断线:达到上限后本场评审会被静音,未完成的攻击直接停止。",
2659
+ budgetRounds: "轮数上限",
2660
+ budgetTokens: "token 预算",
2661
+ expertMaxTokens: "专家单轮输出上限(token)",
2662
+ expertMaxOpinions: "专家单轮最多提几条缺陷",
2663
+ unlimited: "0 = 不限制",
2664
+ loading: "读取设置中…",
2665
+ loadFailed: "读取设置失败。针锋的 host 侧可能没装上,或这一页拿不到插件路由。",
2666
+ saved: "已保存",
2667
+ saveFailed: "保存失败",
2668
+ batchNote: "当前为批 1 骨架:默认强度、维度池与预算可用。专家管理、角色预设与阵容预设将在后续批次补齐。",
2669
+ cardTitle: "针锋",
2670
+ statusDraft: "待开审",
2671
+ statusAttacking: "攻击中",
2672
+ statusJudging: "待裁决",
2673
+ statusSettled: "已定稿",
2674
+ statusNext: "下一步",
2675
+ statusStrip: "当前状态与下一步",
2676
+ statusHintDraft: "红队还没上场。先确认台上的对象与你的立场,再送他们上去。",
2677
+ statusHintAttacking: "红队正在分头攻击。等他们提交后收集观点,才会拆成可裁决的缺陷。",
2678
+ statusHintJudging: "该你了:逐条认定或驳回。机器只负责挑错,结论永远由你下。",
2679
+ statusHintSettled: "已定稿,本轮的认定结果已固化为旧账清单。复审只核对这份旧账,不许开新战线。",
2680
+ passBadge: "第 {pass}/{max} 轮",
2681
+ subjectLabel: "待验证对象",
2682
+ stanceLabel: "我的立场与理由",
2683
+ stanceEmpty: "(没有填写立场——红队因此少了一个靶子)",
2684
+ dimensionsLabel: "攻击维度",
2685
+ expertsLabel: "红队",
2686
+ expertPending: "待命",
2687
+ expertRunning: "攻击中",
2688
+ expertDone: "已提交",
2689
+ expertFailed: "拉起失败",
2690
+ expertSilent: "无产出",
2691
+ expertDefects: "{n} 条缺陷",
2692
+ expertReported: "已提交 {n} 次发言(待收集)",
2693
+ expertSubmitted: "{done}/{total} 位已提交",
2694
+ expertNoOutput: "尚无产出",
2695
+ impactTitle: "表态",
2696
+ countEndorsed: "认定",
2697
+ countRejected: "驳回",
2698
+ countPending: "待定",
2699
+ defectEmpty: "还没有可裁决的缺陷。",
2700
+ defectsTitle: "待裁决缺陷",
2701
+ evidenceRepro: "可复现步骤",
2702
+ evidenceArgument: "论证链",
2703
+ strikesStance: "打穿立场",
2704
+ dimensionFallback: "维度已归一化",
2705
+ recheckLabel: "复核",
2706
+ recheckNote: "核对说明(可选)",
2707
+ verdictFixed: "已修复",
2708
+ verdictUnfixed: "未修复",
2709
+ verdictPartial: "部分修复",
2710
+ verdictUndecided: "尚未判断",
2711
+ recheckNoteNeedsVerdict: "先选一个核对结论,再保存备注(结论只能由你下)",
2712
+ btnAttack: "送红队上场",
2713
+ btnCollect: "收集观点",
2714
+ btnFinish: "定稿",
2715
+ btnRecheck: "开复审",
2716
+ btnExport: "导出",
2717
+ btnEndorse: "认定",
2718
+ btnReject: "驳回",
2719
+ btnPending: "待定",
2720
+ rejectPrompt: "请写明驳回理由(必填)",
2721
+ rejectPlaceholder: "为什么这条不成立?",
2722
+ rejectConfirm: "确认驳回",
2723
+ rejectCancel: "取消",
2724
+ busyWorking: "正在推进…",
2725
+ noReviews: "这个会话还没有针锋评审",
2726
+ cardHint: "在对话里说「用针锋评审一下……」就会开一场。",
2727
+ actionFailed: "操作失败",
2728
+ cardFooterNote: "表态只能由你做:机器只负责挑错。",
2729
+ noSessionHint: "这一页拿不到会话身份,写操作会被拒绝(这是有意的:会话 id 同时是权限凭据)。请刷新页面后重试。",
2730
+ expand: "展开",
2731
+ collapse: "收起",
2732
+ expandSection: "展开",
2733
+ collapseSection: "收起",
2734
+ expertsTitle: "红队专家",
2735
+ expertRosterHint: "每位专家负责一个维度。角色定位会在开打时写进它的系统提示,用来收窄它的专长(例如\"只盯密钥轮换\")——它不能放宽\"只读、不产出方案\"这些硬约束。",
2736
+ expertRoleLabel: "角色定位",
2737
+ expertRolePlaceholder: "如:只盯密钥轮换与权限边界;已有结论不再复述",
2738
+ expertRoleEdit: "改角色定位",
2739
+ expertRoleSaved: "角色定位已保存",
2740
+ expertRoleFromRoster: "(来自阵容预设)",
2741
+ stanceSectionTitle: "我的立场与理由",
2742
+ stanceExpand: "展开看全文",
2743
+ stanceCollapse: "收起立场",
2744
+ explainDismiss: "不再显示这段说明",
2745
+ recheckApprovalAsk: "已达复审上限,继续会超出本轮设定的轮次。确认开新的一轮?",
2746
+ recheckApprovalYes: "确认继续复审",
2747
+ recheckApprovalNo: "取消",
2748
+ settingsExplainTitle: "已隐藏的说明文字",
2749
+ settingsExplainRestore: "恢复显示",
2750
+ settingsExplainNone: "(没有隐藏任何说明)",
2751
+ settingsExplainCount: "你在卡片上关掉了 {n} 段解释性说明,它们不会再出现。",
2752
+ stanceEdit: "我的立场与理由(可编辑)",
2753
+ stanceSave: "保存立场",
2754
+ stanceSaved: "立场已保存",
2755
+ stanceHint: "写清你的判断与理由:红队必须逐条回应,并标注是否被打穿。留空则红队只按维度攻击,不会虚构立场。",
2756
+ exportDone: "已生成导出文件",
2757
+ exportFailed: "导出失败",
2758
+ settingsPresetsTitle: "角色预设",
2759
+ settingsPresetsHint: "自己建几个常用红队角色,开新评审时一次填好角色说明与模型。预设是全局偏好,插件不预置任何内置角色。",
2760
+ settingsPresetsEmpty: "(还没有预设)",
2761
+ presetName: "预设名称",
2762
+ presetRole: "角色说明",
2763
+ presetProvider: "模型路由",
2764
+ presetModel: "模型名",
2765
+ modelInherit: "继承主持人",
2766
+ presetAdd: "+ 新建预设",
2767
+ presetAddCount: "+ 新建预设({count}/{max})",
2768
+ presetLimitInline: "已达上限 {max} 条,删掉不用的才能再建。",
2769
+ modelRoute: "模型路由",
2770
+ modelCatalogLoading: "正在读取模型目录…",
2771
+ modelCatalogFailed: "读取模型目录失败({reason})。两个框因此只剩「继承主持人」—— 这通常是 host 侧还是旧版本:这条端点需要重启 Harness 才会出现(刷新页面不够)。留空即继承主持人,不影响保存预设。",
2772
+ modelCatalogEmpty: "宿主没有报出任何 provider,两个框只剩「继承主持人」。留空即继承主持人,不影响保存预设。",
2773
+ presetProviderNeeded: "先选模型路由(provider),再选模型名",
2774
+ presetSave: "保存",
2775
+ presetCancel: "取消",
2776
+ presetEdit: "编辑",
2777
+ presetDelete: "删除",
2778
+ presetInvalid: "预设名称与角色说明都不能为空",
2779
+ presetLimit: "预设数量已达上限({max} 条),请先删除不用的",
2780
+ presetSaved: "预设已保存",
2781
+ settingsSquadsTitle: "阵容预设",
2782
+ settingsSquadsHint: "阵容只回答「谁来」:它保存几位专家的 key、角色与模型,一次套用。它不描述「怎么开」——没有维度、没有流程。",
2783
+ settingsSquadsEmpty: "(还没有阵容)",
2784
+ squadAdd: "+ 新建阵容",
2785
+ squadAddCount: "+ 新建阵容({count}/{max})",
2786
+ squadName: "阵容名称",
2787
+ squadMembers: "成员",
2788
+ squadMemberKey: "专家 key",
2789
+ squadMemberRole: "角色说明",
2790
+ squadAddMember: "+ 加一位成员",
2791
+ squadRemoveMember: "移除",
2792
+ squadSave: "保存",
2793
+ squadCancel: "取消",
2794
+ squadEdit: "编辑",
2795
+ squadDelete: "删除",
2796
+ squadInvalid: "阵容名称与每位成员的 key 都不能为空",
2797
+ squadSaved: "阵容已保存",
2798
+ squadLimit: "最多 {max} 个阵容,每个最多 {members} 位成员",
2799
+ squadMemberCount: "{count} 位成员"
2800
+ };
2801
+ const en = {
2802
+ renderFailed: "This Crossfire block failed to render",
2803
+ renderFailedHint: "No data was lost: reload the page to retry. If it keeps happening, include the console line starting with [crossfire] in your report.",
2804
+ tab: "Crossfire",
2805
+ settingsNav: "Crossfire",
2806
+ settingsTitle: "Crossfire",
2807
+ settingsIntro: "Crossfire does exactly one thing: put a settled result on the stand, let red-team experts attack it from chosen dimensions, and hand every defect to you for a verdict. The settings below are defaults for NEW reviews only — a review already under way is unaffected.",
2808
+ boundaryTitle: "Four boundaries",
2809
+ boundaryLine1: "1. The verdict is always human: endorse / reject happens on the card, by you. There is no \"endorse on my behalf\" tool.",
2810
+ boundaryLine2: "2. Crossfire produces no plan, writes no code and never fixes anything — it is a verifier, not an executor.",
2811
+ boundaryLine3: "3. Red-team experts are read-only: they find flaws, they never touch your workspace.",
2812
+ boundaryLine4: "4. Your past verdicts only drive de-duplication and re-review progression; they are never fed to the red team as a \"user profile\".",
2813
+ defaultIntensity: "Default intensity",
2814
+ intensityQuick: "Quick",
2815
+ intensityStandard: "Standard",
2816
+ intensityDeep: "Deep",
2817
+ intensityHint: "Intensity changes only the number of experts, whether a cross-examination round is added, and the re-review cap. It does not change the interface.",
2818
+ intensityPerDimension: "{n} expert(s) per dimension",
2819
+ intensityCrossYes: "cross-examination included",
2820
+ intensityCrossNo: "no cross-examination",
2821
+ intensityRecheckCap: "up to {n} pass(es)",
2822
+ dimensionsTitle: "Dimension pool",
2823
+ dimensionsHint: "Dimensions decide which directions the red team attacks from. The fixed pool guarantees coverage; custom dimensions add the angles you care about.",
2824
+ poolLabel: "Fixed dimensions (always available)",
2825
+ customLabel: "Custom dimensions",
2826
+ customEmpty: "(no custom dimensions yet)",
2827
+ customPlaceholder: "e.g. \"compliance\", \"cost\"",
2828
+ customAdd: "+ Add",
2829
+ customRemove: "Remove",
2830
+ customLimit: "Up to {max} custom dimensions, {len} characters each",
2831
+ customInvalid: "A dimension name must be non-empty, at most {len} characters, and must not duplicate a fixed one",
2832
+ budgetTitle: "Budget",
2833
+ budgetHint: "The budget is a circuit breaker: once reached, the review is muted and any unfinished attack stops.",
2834
+ budgetRounds: "Round cap",
2835
+ budgetTokens: "Token budget",
2836
+ expertMaxTokens: "Per-expert output cap (tokens)",
2837
+ expertMaxOpinions: "Max defects per expert per round",
2838
+ unlimited: "0 = unlimited",
2839
+ loading: "Loading settings…",
2840
+ loadFailed: "Failed to load settings. The Crossfire host half may not be installed, or this page cannot reach the plugin route.",
2841
+ saved: "Saved",
2842
+ saveFailed: "Save failed",
2843
+ batchNote: "Batch 1 skeleton: default intensity, dimension pool and budget are live. Expert management, role presets and squad presets arrive in later batches.",
2844
+ cardTitle: "Crossfire",
2845
+ statusDraft: "Ready to attack",
2846
+ statusAttacking: "Attacking",
2847
+ statusJudging: "Awaiting your verdict",
2848
+ statusSettled: "Settled",
2849
+ statusNext: "Next",
2850
+ statusStrip: "Current state and next step",
2851
+ statusHintDraft: "The red team is not on stage yet. Check the subject and your stance first, then send them in.",
2852
+ statusHintAttacking: "The red team is attacking from every dimension. Collect once they report — only then do the findings become adjudicable defects.",
2853
+ statusHintJudging: "Your turn: endorse or reject, one by one. The machine only finds flaws; the conclusion is always yours.",
2854
+ statusHintSettled: "Settled — this pass's endorsements are frozen into the known-flaws checklist. A recheck only verifies that list; it never opens a new front.",
2855
+ passBadge: "Pass {pass}/{max}",
2856
+ subjectLabel: "Under review",
2857
+ stanceLabel: "Your position and reasons",
2858
+ stanceEmpty: "(no stance given — the red team lacks a target)",
2859
+ dimensionsLabel: "Dimensions",
2860
+ expertsLabel: "Red team",
2861
+ expertPending: "Queued",
2862
+ expertRunning: "Attacking",
2863
+ expertDone: "Submitted",
2864
+ expertFailed: "Failed to start",
2865
+ expertSilent: "No output",
2866
+ expertDefects: "{n} defect(s)",
2867
+ expertReported: "{n} report(s) submitted (awaiting collect)",
2868
+ expertSubmitted: "{done}/{total} reported",
2869
+ expertNoOutput: "Nothing yet",
2870
+ impactTitle: "Verdicts",
2871
+ countEndorsed: "Endorsed",
2872
+ countRejected: "Rejected",
2873
+ countPending: "Pending",
2874
+ defectEmpty: "No defects to adjudicate yet.",
2875
+ defectsTitle: "Defects to adjudicate",
2876
+ evidenceRepro: "Repro steps",
2877
+ evidenceArgument: "Argument",
2878
+ strikesStance: "Refutes your stance",
2879
+ dimensionFallback: "dimension normalized",
2880
+ recheckLabel: "Recheck",
2881
+ recheckNote: "Recheck note (optional)",
2882
+ verdictFixed: "Fixed",
2883
+ verdictUnfixed: "Not fixed",
2884
+ verdictPartial: "Partially fixed",
2885
+ verdictUndecided: "Not judged yet",
2886
+ recheckNoteNeedsVerdict: "Pick a verdict first, then save the note (only you decide)",
2887
+ btnAttack: "Send in the red team",
2888
+ btnCollect: "Collect findings",
2889
+ btnFinish: "Settle",
2890
+ btnRecheck: "Open recheck",
2891
+ btnExport: "Export",
2892
+ btnEndorse: "Endorse",
2893
+ btnReject: "Reject",
2894
+ btnPending: "Undecided",
2895
+ rejectPrompt: "A reason is required to reject",
2896
+ rejectPlaceholder: "Why does this not hold?",
2897
+ rejectConfirm: "Confirm reject",
2898
+ rejectCancel: "Cancel",
2899
+ busyWorking: "Working…",
2900
+ noReviews: "No Crossfire review in this session yet",
2901
+ cardHint: "Say \"put this through Crossfire\" in the conversation to open one.",
2902
+ actionFailed: "Action failed",
2903
+ cardFooterNote: "Only you can decide: the machine just finds flaws.",
2904
+ noSessionHint: "This page has no session identity, so writes will be refused (by design: the session id doubles as the permission credential). Reload the page and retry.",
2905
+ expand: "Expand",
2906
+ collapse: "Collapse",
2907
+ expandSection: "Expand",
2908
+ collapseSection: "Collapse",
2909
+ expertsTitle: "Red-team experts",
2910
+ expertRosterHint: "Each expert owns one dimension. The role text is written into its system prompt when the attack starts, to narrow its expertise (e.g. \"key rotation only\") — it can never relax the hard constraints (read-only, no plans).",
2911
+ expertRoleLabel: "Role",
2912
+ expertRolePlaceholder: "e.g. only key rotation and permission boundaries; do not repeat settled points",
2913
+ expertRoleEdit: "Edit role",
2914
+ expertRoleSaved: "Role saved",
2915
+ expertRoleFromRoster: "(from a squad preset)",
2916
+ stanceSectionTitle: "Your position and reasons",
2917
+ stanceExpand: "Expand full text",
2918
+ stanceCollapse: "Collapse stance",
2919
+ explainDismiss: "Hide this explanation",
2920
+ recheckApprovalAsk: "The re-review cap is reached; continuing exceeds this pass's limit. Open one more pass?",
2921
+ recheckApprovalYes: "Approve another pass",
2922
+ recheckApprovalNo: "Cancel",
2923
+ settingsExplainTitle: "Hidden explanations",
2924
+ settingsExplainRestore: "Show again",
2925
+ settingsExplainNone: "(nothing hidden)",
2926
+ settingsExplainCount: "You hid {n} explanation(s) on the card; they stay hidden.",
2927
+ stanceEdit: "Your position and reasons (editable)",
2928
+ stanceSave: "Save stance",
2929
+ stanceSaved: "Stance saved",
2930
+ stanceHint: "State your judgement and your reasons: the red team must answer each one and mark whether it was refuted. Leave it empty and they simply attack by dimension — they will not invent a stance.",
2931
+ exportDone: "Export written",
2932
+ exportFailed: "Export failed",
2933
+ settingsPresetsTitle: "Role presets",
2934
+ settingsPresetsHint: "Build the red-team roles you reuse, so a new review starts with the role text and model already filled in. Presets are global preferences; the plugin ships none.",
2935
+ settingsPresetsEmpty: "(no presets yet)",
2936
+ presetName: "Preset name",
2937
+ presetRole: "Role description",
2938
+ presetProvider: "Provider route",
2939
+ presetModel: "Model",
2940
+ modelInherit: "Inherit host",
2941
+ presetAdd: "+ New preset",
2942
+ presetAddCount: "+ New preset ({count}/{max})",
2943
+ presetLimitInline: "Limit reached ({max}) — delete one to create another.",
2944
+ modelRoute: "Model route",
2945
+ modelCatalogLoading: "Loading the model catalog…",
2946
+ modelCatalogFailed: "Could not load the model catalog ({reason}). Both fields therefore offer only \"Inherit host\" — usually the host half is still an older build: this endpoint only appears after restarting the Harness (reloading the page is not enough). Leaving them empty means \"inherit host\" and still saves fine.",
2947
+ modelCatalogEmpty: "The host reported no providers, so both fields offer only \"Inherit host\". Leaving them empty means \"inherit host\" and still saves fine.",
2948
+ presetProviderNeeded: "Pick a provider first, then a model",
2949
+ presetSave: "Save",
2950
+ presetCancel: "Cancel",
2951
+ presetEdit: "Edit",
2952
+ presetDelete: "Delete",
2953
+ presetInvalid: "A preset needs both a name and a role description",
2954
+ presetLimit: "Preset limit reached ({max}) — delete one first",
2955
+ presetSaved: "Preset saved",
2956
+ settingsSquadsTitle: "Squad presets",
2957
+ settingsSquadsHint: "A squad only answers \"who joins\": it stores several experts' keys, roles and models so you can apply them at once. It says nothing about how the review runs — no dimensions, no flow.",
2958
+ settingsSquadsEmpty: "(no squads yet)",
2959
+ squadAdd: "+ New squad",
2960
+ squadAddCount: "+ New squad ({count}/{max})",
2961
+ squadName: "Squad name",
2962
+ squadMembers: "Members",
2963
+ squadMemberKey: "Expert key",
2964
+ squadMemberRole: "Role description",
2965
+ squadAddMember: "+ Add a member",
2966
+ squadRemoveMember: "Remove",
2967
+ squadSave: "Save",
2968
+ squadCancel: "Cancel",
2969
+ squadEdit: "Edit",
2970
+ squadDelete: "Delete",
2971
+ squadInvalid: "A squad needs a name and every member needs a key",
2972
+ squadSaved: "Squad saved",
2973
+ squadLimit: "Up to {max} squads, {members} members each",
2974
+ squadMemberCount: "{count} member(s)"
2975
+ };
2976
+
2977
+ //#endregion
2978
+ //#region src/client/index.ts
2979
+ /** Required services: the two slot registries plus locale (see module doc). */
2980
+ const inject = ["slots", "locale"];
2981
+ function apply(ctx) {
2982
+ ctx.effect(() => ctx.locale.register(NS, {
2983
+ zh,
2984
+ en
2985
+ }), "crossfire: dictionaries");
2986
+ const slots = ctx.slots;
2987
+ const connection = ctx.get?.("connection");
2988
+ const rpc = async (endpoint, payload) => {
2989
+ const direct = await callRpc(endpoint, payload);
2990
+ if (direct.ok || connection === void 0) return direct;
2991
+ try {
2992
+ return await connection.rpc.call("/crossfire", endpoint, payload);
2993
+ } catch {
2994
+ return direct;
2995
+ }
2996
+ };
2997
+ const settingsInjected = () => ({
2998
+ rpc,
2999
+ t: ctx.locale.bind(NS)
3000
+ });
3001
+ slots.inject("settings.section", () => slots.register({
3002
+ name: "settings.section",
3003
+ id: "crossfire",
3004
+ order: 41,
3005
+ label: () => ctx.locale.bind(NS)("settingsNav"),
3006
+ locale: NS,
3007
+ inject: settingsInjected
3008
+ }, withSlotBoundary(CrossfireSettings)));
3009
+ const viewInjected = () => ({
3010
+ rpc,
3011
+ t: ctx.locale.bind(NS)
3012
+ });
3013
+ slots.inject("conversation.view", () => slots.register({
3014
+ name: "conversation.view",
3015
+ id: "crossfire",
3016
+ order: 31,
3017
+ label: () => ctx.locale.bind(NS)("tab"),
3018
+ locale: NS,
3019
+ inject: viewInjected
3020
+ }, withSlotBoundary(CrossfireCard)));
3021
+ const cardInjected = () => ({
3022
+ rpc,
3023
+ t: ctx.locale.bind(NS)
3024
+ });
3025
+ slots.inject("tool.call.toolview", () => slots.register({
3026
+ name: "tool.call.toolview",
3027
+ key: "crossfire_open",
3028
+ inject: cardInjected
3029
+ }, withSlotBoundary(CrossfireCard)));
3030
+ }
3031
+
3032
+ //#endregion
3033
+ exports.apply = apply;
3034
+ exports.inject = inject;
3035
+ return module.exports; } });