dsh-prompt-predict 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js CHANGED
@@ -11,12 +11,163 @@ window.__ModuleLoader__.load({
11
11
  var useCallback = React.useCallback;
12
12
 
13
13
  var name = "dsh-prompt-predict";
14
- var inject = ["slots"];
14
+ var inject = ["slots", "locale"];
15
+
16
+ // ---------- 文案:中英双语,跟随 DSH 的「设置 → 语言」 ----------
17
+ // 注册进 DSH 的 locale 服务后语言切换即时生效(无需刷新),并同步 <html lang>。
18
+ // 取值顺序:框架注入的 props.t → 这里绑定的 t → 本词典的中文。
19
+ // 最后一层兜底很重要:旧运行时没有 locale 服务时,界面应是中文而不是一串 key。
20
+ var NS = "dsh-prompt-predict";
21
+ var DICT = {
22
+ zh: {
23
+ "effort.off": "关闭", "effort.minimal": "最小", "effort.low": "低",
24
+ "effort.medium": "中", "effort.high": "高", "effort.max": "最大",
25
+ "ghost.acceptHint": "Tab 采纳 · Alt+→ 逐段采纳",
26
+ "ghost.reroll": "换一条建议",
27
+ "ghost.dismiss": "Esc 忽略",
28
+ "settings.title": "提示词联想",
29
+ "settings.desc": "助手回复结束后,在输入框里直接给出灰色建议(Tab 采纳、Esc 忽略、开始输入自动消失)。",
30
+ "settings.enable": "启用联想",
31
+ "settings.on": "已开启",
32
+ "settings.off": "已关闭",
33
+ "settings.provider": "提供商",
34
+ "settings.model": "模型",
35
+ "settings.followSession": "跟随当前会话模型",
36
+ "settings.modelHint": "默认跟随当前会话模型;也可固定到某个模型——选快模型更省额度。",
37
+ "settings.advanced": " 高级",
38
+ "settings.timeout": "响应超时",
39
+ "settings.timeoutUnit": "秒(1–60)",
40
+ "settings.contextSize": "上下文规模",
41
+ "settings.contextSmall": "少:最近 3 条消息",
42
+ "settings.contextStandard": "标准:最近 8 条(默认)",
43
+ "settings.contextLarge": "多:最近 12 条",
44
+ "settings.contextHint": "越多越准但越慢",
45
+ "settings.maxChars": "上下文预算",
46
+ "settings.maxCharsHint": "字符上限(1000–20000),越小越省 token",
47
+ "settings.maxTokens": "输出上限",
48
+ "settings.maxTokensHint": "token(16–512),建议够一句话即可",
49
+ "settings.effort": "思考深度",
50
+ "settings.effortDefault": "跟随模型默认",
51
+ "settings.effortDefaultNoLevels": "跟随模型默认(该模型未公布可调档位)",
52
+ "settings.effortFastest": "(最快最省)",
53
+ "settings.effortHint": "联想宜浅;过深会把 token 烧在推理上",
54
+ "settings.effortFromSession": "档位按当前会话模型生成:{provider} / {model}",
55
+ "settings.effortNoSession": "档位按当前会话模型生成(暂时取不到会话模型)",
56
+ "settings.save": "保存",
57
+ "settings.saving": "保存中…",
58
+ "settings.reset": "恢复默认",
59
+ "settings.saved": "已保存,立即生效",
60
+ "settings.saveFailed": "保存失败",
61
+ "settings.enabled": "已开启联想",
62
+ "settings.disabled": "已关闭联想",
63
+ "settings.actionFailed": "操作失败",
64
+ "settings.resetDone": "已恢复默认",
65
+ "settings.loading": "加载中…",
66
+ "run.none": "上次预测:暂无记录(本轮还沒触发过联想)",
67
+ "run.line": "上次预测:{time} · {detail}",
68
+ "run.failed": "上次预测:{time} · 失败 — {reason}",
69
+ "run.effortDropped": "已自动降级思考档位",
70
+ "run.dedupRetry": "已换一条",
71
+ "dock.noSuggestion": "预测未返回有效建议",
72
+ "dock.rerollFailed": "换一条失败",
73
+ },
74
+ en: {
75
+ "effort.off": "Off", "effort.minimal": "Minimal", "effort.low": "Low",
76
+ "effort.medium": "Medium", "effort.high": "High", "effort.max": "Max",
77
+ "ghost.acceptHint": "Tab to accept · Alt+→ piece by piece",
78
+ "ghost.reroll": "Suggest another",
79
+ "ghost.dismiss": "Esc to dismiss",
80
+ "settings.title": "Prompt suggestions",
81
+ "settings.desc": "Shows a gray suggestion in the composer right after each reply (Tab to accept, Esc to dismiss, start typing and it goes away).",
82
+ "settings.enable": "Enable predictions",
83
+ "settings.on": "On",
84
+ "settings.off": "Off",
85
+ "settings.provider": "Provider",
86
+ "settings.model": "Model",
87
+ "settings.followSession": "Follow the current session model",
88
+ "settings.modelHint": "Follows the current session model by default; pin a cheaper model to save quota.",
89
+ "settings.advanced": " Advanced",
90
+ "settings.timeout": "Response timeout",
91
+ "settings.timeoutUnit": "seconds (1–60)",
92
+ "settings.contextSize": "Context size",
93
+ "settings.contextSmall": "Small: last 3 messages",
94
+ "settings.contextStandard": "Standard: last 8 (default)",
95
+ "settings.contextLarge": "Large: last 12 messages",
96
+ "settings.contextHint": "More is more accurate but slower",
97
+ "settings.maxChars": "Context budget",
98
+ "settings.maxCharsHint": "Character cap (1000–20000); lower saves tokens",
99
+ "settings.maxTokens": "Output cap",
100
+ "settings.maxTokensHint": "tokens (16–512); enough for one sentence",
101
+ "settings.effort": "Reasoning effort",
102
+ "settings.effortDefault": "Follow the model default",
103
+ "settings.effortDefaultNoLevels": "Follow the model default (this model publishes no levels)",
104
+ "settings.effortFastest": " (fastest, cheapest)",
105
+ "settings.effortHint": "Keep it shallow; deeper levels burn tokens on reasoning",
106
+ "settings.effortFromSession": "Levels are generated from the current session model: {provider} / {model}",
107
+ "settings.effortNoSession": "Levels are generated from the current session model (unavailable right now)",
108
+ "settings.save": "Save",
109
+ "settings.saving": "Saving…",
110
+ "settings.reset": "Reset to defaults",
111
+ "settings.saved": "Saved — takes effect immediately",
112
+ "settings.saveFailed": "Save failed",
113
+ "settings.enabled": "Predictions enabled",
114
+ "settings.disabled": "Predictions disabled",
115
+ "settings.actionFailed": "Action failed",
116
+ "settings.resetDone": "Defaults restored",
117
+ "settings.loading": "Loading…",
118
+ "run.none": "Last prediction: no record yet (nothing triggered this round)",
119
+ "run.line": "Last prediction: {time} · {detail}",
120
+ "run.failed": "Last prediction: {time} · failed — {reason}",
121
+ "run.effortDropped": "reasoning level auto-downgraded",
122
+ "run.dedupRetry": "rerolled",
123
+ "dock.noSuggestion": "Prediction returned no usable suggestion",
124
+ "dock.rerollFailed": "Reroll failed",
125
+ },
126
+ };
15
127
 
16
- var EFFORT_LABELS = {
17
- off: "关闭", none: "关闭", minimal: "最小", low: "低", medium: "中", high: "高", max: "最大",
128
+ var EFFORT_LABEL_KEYS = {
129
+ off: "effort.off", none: "effort.off", minimal: "effort.minimal",
130
+ low: "effort.low", medium: "effort.medium", high: "effort.high", max: "effort.max",
18
131
  };
19
132
 
133
+ // tt() 自己实现 {name} 插值,所以不依赖 t 是否支持参数,词序完全交给各语言的模板。
134
+ var boundTranslate = null;
135
+ var localeService = null;
136
+ function fallbackTranslate(key) { return (DICT.zh && DICT.zh[key]) || key; }
137
+
138
+ /**
139
+ * 当前界面语言。Host 端要靠它来决定用哪种语言回错误原因,所以每次请求都重新探:
140
+ * 用户在「设置 → 语言」里切换后必须立刻跟上。
141
+ * 先读 locale 服务的快照(它知道浏览器自动判定出来的语言),读不到再拿一个
142
+ * 已知 key 去问翻译、跟词典比对。
143
+ */
144
+ function currentLang() {
145
+ try {
146
+ var snap = localeService && typeof localeService.getSnapshot === "function" ? localeService.getSnapshot() : null;
147
+ var v = snap && (snap.active || snap.locale || snap.id);
148
+ if (typeof v === "string" && v) return v.toLowerCase().indexOf("zh") === 0 ? "zh" : "en";
149
+ } catch { /* 下面再试比对 */ }
150
+ try {
151
+ if (boundTranslate) {
152
+ var probe = boundTranslate("settings.title");
153
+ if (probe === DICT.en["settings.title"]) return "en";
154
+ }
155
+ } catch { /* 保守回中文 */ }
156
+ return "zh";
157
+ }
158
+ function tt(t, key, vars) {
159
+ var fn = t || boundTranslate || fallbackTranslate;
160
+ var text = null;
161
+ try { text = fn(key); } catch { text = null; }
162
+ if (typeof text !== "string" || !text || text === key) text = fallbackTranslate(key);
163
+ if (vars) {
164
+ text = text.replace(/\{(\w+)\}/g, function (m, k) {
165
+ return vars[k] === undefined || vars[k] === null ? m : String(vars[k]);
166
+ });
167
+ }
168
+ return text;
169
+ }
170
+
20
171
  // ---------- 设置页样式 ----------
21
172
  var SS = {
22
173
  card: { border: "1px solid var(--dsh-border, #e7e8ee)", borderRadius: 14, padding: "20px 22px", maxWidth: 560, background: "var(--dsh-bg, #fff)" },
@@ -40,9 +191,10 @@ window.__ModuleLoader__.load({
40
191
  function api(path, body) {
41
192
  var controller = new AbortController();
42
193
  var timer = setTimeout(function () { controller.abort(); }, 15000);
43
- var opts = body === undefined ? {} : {
44
- method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body),
45
- };
194
+ var langHeader = { "x-dsh-locale": currentLang() };
195
+ var opts = body === undefined
196
+ ? { headers: langHeader }
197
+ : { method: "POST", headers: Object.assign({ "content-type": "application/json" }, langHeader), body: JSON.stringify(body) };
46
198
  return fetch("./dsh-prompt-predict/" + path, Object.assign({}, opts, { signal: controller.signal }))
47
199
  .then(function (r) { return r.json(); })
48
200
  .finally(function () { clearTimeout(timer); });
@@ -57,11 +209,67 @@ window.__ModuleLoader__.load({
57
209
  return scoped || document.querySelector('[data-composer-input="true"]');
58
210
  }
59
211
 
212
+ // ============ 模块级共享状态(composer dock 写、设置页读) ============
213
+ // 设置页本身拿不到会话上下文,但「思考档位」必须按**会话实际模型**生成,
214
+ // 否则用户选到的档位可能不被真实调用的模型支持(表现为「设置都对但永远没有建议」)。
215
+ var liveSessionId = null;
216
+ // 上一次预测的结果快照:设置页据此显示「上次预测:模型 / 耗时 / 错误」。
217
+ // 之前失败是静默的,用户和作者都看不出是没触发、超时还是模型报错。
218
+ var lastRun = null;
219
+ function recordRun(result, error) {
220
+ lastRun = {
221
+ at: Date.now(),
222
+ ms: result && typeof result.ms === "number" ? result.ms : null,
223
+ provider: (result && result.provider) || null,
224
+ model: (result && result.model) || null,
225
+ effortDropped: Boolean(result && result.effortDropped),
226
+ dedupRetry: Boolean(result && result.dedupRetry),
227
+ error: error || null,
228
+ };
229
+ window.dispatchEvent(new CustomEvent("dsh-prompt-predict:run", { detail: lastRun }));
230
+ }
231
+
232
+ // 逐段采纳用:英文按一个词(含其后空格),中日韩一次 2 个字,其余按单字符。
233
+ // 中文没有空格分词,按固定宽度切手感最接近「一点点往里填」。
234
+ function takeChunk(text) {
235
+ var m = text.match(/^\s*[A-Za-z0-9_@.\/#+\-:!?]+[\s]*/);
236
+ if (m && m[0].trim()) return m[0];
237
+ m = text.match(/^[\u3400-\u9fff\uf900-\ufaff]{1,2}/);
238
+ if (m) return m[0];
239
+ m = text.match(/^\s*\S/);
240
+ return m ? m[0] : text;
241
+ }
242
+
243
+ // 档位下拉到底按哪个模型取的:跟随会话模型时这点必须让用户看见,
244
+ // 否则「设置里的档位」和「真实调用用的模型」对不上时完全无从察觉。
245
+ function effortSourceHint(st, t) {
246
+ var eff = (st && st.effective) || {};
247
+ var pinned = eff.provider && eff.provider !== "auto" && eff.model && eff.model !== "auto";
248
+ if (pinned) return tt(t, "settings.effortHint");
249
+ var cur = (st && st.sessionModel && st.sessionModel.model ? st.sessionModel : (st && st.current)) || {};
250
+ if (!cur.provider || !cur.model) return tt(t, "settings.effortNoSession");
251
+ return tt(t, "settings.effortFromSession", { provider: cur.provider, model: cur.model });
252
+ }
253
+
254
+ // 上次预测的运行情况:模型 / 耗时 / 是否降级 / 是否换过 / 失败原因
255
+ function runText(run, t) {
256
+ if (!run) return tt(t, "run.none");
257
+ var time = new Date(run.at).toLocaleTimeString();
258
+ if (run.error) return tt(t, "run.failed", { time: time, reason: String(run.error) });
259
+ var bits = [(run.provider || "?") + " / " + (run.model || "?")];
260
+ if (typeof run.ms === "number") bits.push(run.ms + " ms");
261
+ if (run.effortDropped) bits.push(tt(t, "run.effortDropped"));
262
+ if (run.dedupRetry) bits.push(tt(t, "run.dedupRetry"));
263
+ return tt(t, "run.line", { time: time, detail: bits.join(" · ") });
264
+ }
265
+
60
266
  // 行内 ghost text 覆盖层:不写进 Lexical,只有 Tab/点击采纳时才 insertText。
61
267
  function GhostOverlay(props) {
62
268
  var suggestion = props.suggestion;
63
269
  var sessionId = props.sessionId;
64
270
  var onAccept = props.onAccept;
271
+ var onReroll = props.onReroll;
272
+ var t = props.t;
65
273
  var _p = useState(null), pos = _p[0], setPos = _p[1];
66
274
 
67
275
  var measure = useCallback(function () {
@@ -117,14 +325,22 @@ window.__ModuleLoader__.load({
117
325
  }, suggestion),
118
326
  h("button", {
119
327
  type: "button", onClick: onAccept,
120
- title: "Tab 采纳 · Esc 忽略",
328
+ title: tt(t, "ghost.acceptHint"),
121
329
  style: {
122
330
  pointerEvents: "auto", flex: "none", padding: "0 7px", height: 19, borderRadius: 5,
123
331
  border: "1px solid var(--dsw-alias-border-l1, #d1d5db)", background: "var(--dsw-alias-bg-secondary, #f8fafc)",
124
332
  color: "#9aa3b2", fontSize: 10, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", cursor: "pointer",
125
333
  },
126
334
  }, "Tab"),
127
- h("span", { style: { color: "#b0b7c3", fontSize: 10, whiteSpace: "nowrap" } }, "Esc 忽略"));
335
+ h("button", {
336
+ type: "button", onClick: onReroll, title: tt(t, "ghost.reroll"),
337
+ style: {
338
+ pointerEvents: "auto", flex: "none", width: 19, height: 19, padding: 0, borderRadius: 5,
339
+ border: "1px solid var(--dsw-alias-border-l1, #d1d5db)", background: "var(--dsw-alias-bg-secondary, #f8fafc)",
340
+ color: "#9aa3b2", fontSize: 11, lineHeight: 1, cursor: "pointer",
341
+ },
342
+ }, "↻"),
343
+ h("span", { style: { color: "#b0b7c3", fontSize: 10, whiteSpace: "nowrap" } }, tt(t, "ghost.dismiss")));
128
344
  }
129
345
 
130
346
  // ================= 建议(composer dock 仅提供生命周期;显示覆盖到输入框内) =================
@@ -133,18 +349,27 @@ window.__ModuleLoader__.load({
133
349
  var useSession = props.useSession;
134
350
  var useInput = props.useInput;
135
351
  var inputActions = props.inputActions;
352
+ var t = props.t;
136
353
  var running = useSession(function (value) { return Boolean(value && value.running); });
137
354
  var blank = useSession(function (value) { return Boolean(value && value.blank); });
138
355
  var removed = useSession(function (value) { return Boolean(value && value.removed); });
139
356
  var draft = useInput(function (value) { return String(value && value.draft || ""); });
140
357
  var _s = useState(""), suggestion = _s[0], setSuggestion = _s[1];
358
+ var _ac = useState(""), accepted = _ac[0], setAccepted = _ac[1]; // 逐段采纳已写进输入框的前缀
141
359
  var _en = useState(true), enabled = _en[0], setEnabled = _en[1];
142
360
  var armed = useRef(true);
143
361
  var abortRef = useRef(null);
144
362
  var sessionRef = useRef(sessionId);
145
363
  var dismissedByDraft = useRef(false);
146
- if (draft.trim() !== "") dismissedByDraft.current = true;
147
- var visibleSuggestion = dismissedByDraft.current ? "" : suggestion;
364
+ // 逐段采纳的前缀,只有「输入框里确实还留着它」时才算数:
365
+ // 用户把它删掉后必须重新完整展示整条建议,否则会出现从中间截断的鬼影。
366
+ var prefixKept = accepted !== "" && suggestion.indexOf(accepted) === 0 && draft === accepted;
367
+ var remaining = prefixKept ? suggestion.slice(accepted.length) : suggestion;
368
+ // 打字即隐藏,两种例外:
369
+ // (a) 草稿又被删空 —— 用户回到了「需要建议」的状态,让它重新浮现(只有 Esc 才是永久忽略)
370
+ // (b) 草稿正是我们逐段写进去、且没有改动的前缀 —— 不能把自己插的内容误判成用户打字
371
+ dismissedByDraft.current = draft.trim() !== "" && !prefixKept;
372
+ var visibleSuggestion = dismissedByDraft.current ? "" : remaining;
148
373
 
149
374
  // 首次读取启用状态;设置页保存后通过事件让当前会话立即生效。
150
375
  useEffect(function () {
@@ -165,9 +390,11 @@ window.__ModuleLoader__.load({
165
390
 
166
391
  useEffect(function () {
167
392
  sessionRef.current = sessionId;
393
+ liveSessionId = sessionId; // 给设置页取「会话实际模型」用
168
394
  armed.current = true;
169
395
  dismissedByDraft.current = false;
170
396
  setSuggestion("");
397
+ setAccepted("");
171
398
  if (abortRef.current) abortRef.current.abort();
172
399
  }, [sessionId]);
173
400
 
@@ -177,6 +404,7 @@ window.__ModuleLoader__.load({
177
404
  armed.current = true;
178
405
  dismissedByDraft.current = false;
179
406
  setSuggestion("");
407
+ setAccepted("");
180
408
  if (abortRef.current) abortRef.current.abort();
181
409
  return;
182
410
  }
@@ -189,9 +417,15 @@ window.__ModuleLoader__.load({
189
417
  api("suggest", { sessionId: sessionId })
190
418
  .then(function (result) {
191
419
  if (controller.signal.aborted || sessionRef.current !== sessionId) return;
192
- if (result && result.ok && typeof result.suggestion === "string") setSuggestion(result.suggestion.trim());
420
+ if (result && result.ok && typeof result.suggestion === "string") {
421
+ setSuggestion(result.suggestion.trim());
422
+ recordRun(result, null);
423
+ } else {
424
+ // 失败照样记下来:设置页会显示原因(以前是彻底静默,排查全靠猜)
425
+ recordRun(null, (result && result.reason) || tt(t, "dock.noSuggestion"));
426
+ }
193
427
  })
194
- .catch(function () { /* 预测失败静默,不打断正常输入 */ });
428
+ .catch(function (error) { recordRun(null, String(error)); });
195
429
  }, 350);
196
430
  return function () {
197
431
  clearTimeout(timer);
@@ -201,20 +435,61 @@ window.__ModuleLoader__.load({
201
435
  }, [sessionId, running, blank, removed, draft, enabled]);
202
436
 
203
437
  var accept = useCallback(function () {
204
- if (!visibleSuggestion || !inputActions || draft.trim() !== "") return false;
438
+ if (!visibleSuggestion || !inputActions || dismissedByDraft.current) return false;
205
439
  try {
206
440
  var span = inputActions.captureInsertion();
207
441
  if (!inputActions.insertText(visibleSuggestion, span)) return false;
208
442
  setSuggestion("");
443
+ setAccepted("");
209
444
  return true;
210
445
  } catch { return false; }
211
446
  }, [visibleSuggestion, inputActions, draft]);
212
447
 
448
+ // 逐段采纳:一次塞一小段(Alt+→),可以中途反悔,也可以一路按到整条都进去
449
+ var acceptChunk = useCallback(function () {
450
+ if (!visibleSuggestion || !inputActions || dismissedByDraft.current) return false;
451
+ var chunk = takeChunk(visibleSuggestion);
452
+ if (!chunk) return false;
453
+ try {
454
+ var span = inputActions.captureInsertion();
455
+ if (!inputActions.insertText(chunk, span)) return false;
456
+ } catch { return false; }
457
+ if (chunk.length >= visibleSuggestion.length) { setSuggestion(""); setAccepted(""); }
458
+ else setAccepted(accepted + chunk);
459
+ return true;
460
+ }, [visibleSuggestion, accepted, inputActions, draft]);
461
+
462
+ // 换一条:把当前这条作为 avoid 传回去,让模型必须给出不同的建议
463
+ var reroll = useCallback(function () {
464
+ if (!sessionId || !suggestion) return;
465
+ var controller = new AbortController();
466
+ abortRef.current = controller;
467
+ api("suggest", { sessionId: sessionId, reroll: true, avoid: suggestion })
468
+ .then(function (result) {
469
+ if (controller.signal.aborted || sessionRef.current !== sessionId) return;
470
+ if (result && result.ok && typeof result.suggestion === "string") {
471
+ setSuggestion(result.suggestion.trim());
472
+ setAccepted("");
473
+ recordRun(result, null);
474
+ } else {
475
+ recordRun(null, (result && result.reason) || tt(t, "dock.rerollFailed"));
476
+ }
477
+ })
478
+ .catch(function (error) { recordRun(null, String(error)); });
479
+ }, [sessionId, suggestion]);
480
+
213
481
  useEffect(function () {
214
482
  if (!visibleSuggestion) return;
215
483
  var onKey = function (event) {
216
484
  if (event.isComposing) return;
217
- if (event.key === "Escape") { event.preventDefault(); setSuggestion(""); return; }
485
+ if (event.key === "Escape") { event.preventDefault(); setSuggestion(""); setAccepted(""); return; }
486
+ // Option/Alt + → :逐段采纳(对齐 Codex 的手感)
487
+ if (event.key === "ArrowRight" && event.altKey) {
488
+ if (!acceptChunk()) return;
489
+ event.preventDefault();
490
+ event.stopPropagation();
491
+ return;
492
+ }
218
493
  if (event.key !== "Tab" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
219
494
  if (!accept()) return;
220
495
  event.preventDefault();
@@ -222,30 +497,36 @@ window.__ModuleLoader__.load({
222
497
  };
223
498
  window.addEventListener("keydown", onKey, true);
224
499
  return function () { window.removeEventListener("keydown", onKey, true); };
225
- }, [visibleSuggestion, accept]);
500
+ }, [visibleSuggestion, accept, acceptChunk]);
226
501
 
227
502
  // 建议直接以灰色 ghost text 覆盖在 Lexical 输入框首字符处。
228
- if (!enabled || draft.trim() !== "" || !visibleSuggestion) return null;
229
- return h(GhostOverlay, { suggestion: visibleSuggestion, sessionId: sessionId, onAccept: accept });
503
+ if (!enabled || dismissedByDraft.current || !visibleSuggestion) return null;
504
+ return h(GhostOverlay, { suggestion: visibleSuggestion, sessionId: sessionId, onAccept: accept, onReroll: reroll, t: t });
230
505
  }
231
506
 
232
507
  // ================= 设置页 =================
233
- function SettingsSection() {
508
+ function SettingsSection(props) {
509
+ var t = props.t;
234
510
  var _st = useState(null), st = _st[0], setSt = _st[1];
235
511
  var _cat = useState(null), cat = _cat[0], setCat = _cat[1];
236
512
  var _pv = useState(""), provider = _pv[0], setProvider = _pv[1];
237
513
  var _md = useState(""), model = _md[0], setModel = _md[1];
238
514
  var _to = useState(12), timeoutSec = _to[0], setTimeoutSec = _to[1];
239
515
  var _ctx = useState(8), ctxSize = _ctx[0], setCtxSize = _ctx[1];
516
+ var _mc = useState(6000), maxChars = _mc[0], setMaxChars = _mc[1];
517
+ var _mt = useState(64), maxTokens = _mt[0], setMaxTokens = _mt[1];
240
518
  var _ef = useState("off"), effort = _ef[0], setEffort = _ef[1];
241
519
  var _eo = useState([]), effortOptions = _eo[0], setEffortOptions = _eo[1];
520
+ var _run = useState(lastRun), run = _run[0], setRun = _run[1];
242
521
  var _busy = useState(false), busy = _busy[0], setBusy = _busy[1];
243
522
  var _msg = useState(null), msg = _msg[0], setMsg = _msg[1];
244
523
  var _adv = useState(false), adv = _adv[0], setAdv = _adv[1];
245
524
  var _dirty = useState(false), dirty = _dirty[0], setDirty = _dirty[1];
246
525
 
247
526
  function loadAll() {
248
- api("status").then(function (j) {
527
+ // 带上当前会话 id:这样「跟随会话模型」时档位下拉拿到的是会话真实模型,
528
+ // 而不是全局默认模型——两者不同时会让用户选到不被支持的档位。
529
+ api("status" + (liveSessionId ? "?sessionId=" + encodeURIComponent(liveSessionId) : "")).then(function (j) {
249
530
  if (!j || !j.ok) return;
250
531
  setSt(j);
251
532
  var eff = j.effective || {};
@@ -253,6 +534,8 @@ window.__ModuleLoader__.load({
253
534
  setModel(eff.model || "");
254
535
  setTimeoutSec(Math.round((eff.timeoutMs || 12000) / 1000));
255
536
  setCtxSize(eff.maxMessages || 8);
537
+ setMaxChars(eff.maxChars || 6000);
538
+ setMaxTokens(eff.maxTokens || 64);
256
539
  setEffort(eff.reasoningEffort || "off");
257
540
  setDirty(false);
258
541
  }).catch(function () { });
@@ -260,12 +543,20 @@ window.__ModuleLoader__.load({
260
543
  }
261
544
  useEffect(function () { loadAll(); }, []);
262
545
 
546
+ // 设置页打开时若已有预测记录,直接显示;之后靠事件实时刷新
547
+ useEffect(function () {
548
+ var on = function (event) { setRun(event.detail); };
549
+ window.addEventListener("dsh-prompt-predict:run", on);
550
+ return function () { window.removeEventListener("dsh-prompt-predict:run", on); };
551
+ }, []);
552
+
263
553
  // 思考档位跟随所选模型动态生成(模型不支持的档位不出现,避免保存被拒);
264
- // 选「跟随当前会话模型」时,按当前会话实际使用的模型取档位。
554
+ // 选「跟随当前会话模型」时优先用**该会话实际使用的模型**(st.sessionModel),
555
+ // 拿不到才退回全局默认——否则下拉里的档位可能和真实调用用的模型对不上。
265
556
  useEffect(function () {
266
557
  var p = provider, m = model;
267
558
  if (p === "auto" || m === "auto") {
268
- var cur = (st && st.current) || {};
559
+ var cur = (st && (st.sessionModel && st.sessionModel.model ? st.sessionModel : st.current)) || {};
269
560
  p = cur.provider; m = cur.model;
270
561
  }
271
562
  if (!p || !m) { setEffortOptions([]); return; }
@@ -286,46 +577,48 @@ window.__ModuleLoader__.load({
286
577
  provider: provider, model: model,
287
578
  timeoutMs: timeoutSec * 1000,
288
579
  maxMessages: ctxSize,
580
+ maxChars: maxChars,
581
+ maxTokens: maxTokens,
289
582
  reasoningEffort: effort,
290
583
  }).then(function (j) {
291
- if (j && j.ok) { notifySettings(j.effective); setMsg({ ok: true, text: "已保存,立即生效" }); setDirty(false); loadAll(); }
292
- else setMsg({ ok: false, text: (j && j.reason) || "保存失败" });
584
+ if (j && j.ok) { notifySettings(j.effective); setMsg({ ok: true, text: tt(t, "settings.saved") }); setDirty(false); loadAll(); }
585
+ else setMsg({ ok: false, text: (j && j.reason) || tt(t, "settings.saveFailed") });
293
586
  }).catch(function (e) { setMsg({ ok: false, text: String(e) }); })
294
587
  .finally(function () { setBusy(false); });
295
588
  }
296
589
  function toggleEnabled(on) {
297
590
  setBusy(true); setMsg(null);
298
591
  api("settings", { enabled: on }).then(function (j) {
299
- if (j && j.ok) { notifySettings(j.effective); setSt(Object.assign({}, st, { effective: j.effective })); loadAll(); setMsg({ ok: true, text: on ? "已开启联想" : "已关闭联想" }); }
300
- else setMsg({ ok: false, text: (j && j.reason) || "操作失败" });
592
+ if (j && j.ok) { notifySettings(j.effective); setSt(Object.assign({}, st, { effective: j.effective })); loadAll(); setMsg({ ok: true, text: on ? tt(t, "settings.enabled") : tt(t, "settings.disabled") }); }
593
+ else setMsg({ ok: false, text: (j && j.reason) || tt(t, "settings.actionFailed") });
301
594
  }).catch(function (e) { setMsg({ ok: false, text: String(e) }); })
302
595
  .finally(function () { setBusy(false); });
303
596
  }
304
597
  function resetDefaults() {
305
598
  setBusy(true); setMsg(null);
306
599
  api("settings", { reset: true }).then(function (j) {
307
- if (j && j.ok) { notifySettings(j.effective); setMsg({ ok: true, text: "已恢复默认" }); loadAll(); }
600
+ if (j && j.ok) { notifySettings(j.effective); setMsg({ ok: true, text: tt(t, "settings.resetDone") }); loadAll(); }
308
601
  }).finally(function () { setBusy(false); });
309
602
  }
310
603
 
311
- if (!st) return h("div", { style: SS.card }, h("p", { style: SS.muted }, "加载中…"));
604
+ if (!st) return h("div", { style: SS.card }, h("p", { style: SS.muted }, tt(t, "settings.loading")));
312
605
  var eff = st.effective || {};
313
606
  var enabled = eff.enabled !== false;
314
607
 
315
608
  return h("div", { style: SS.card },
316
- h("p", { style: SS.title }, "提示词联想"),
317
- h("p", { style: SS.muted }, "助手回复结束后,在输入框里直接给出灰色建议(Tab 采纳、Esc 忽略、开始输入自动消失)。"),
609
+ h("p", { style: SS.title }, tt(t, "settings.title")),
610
+ h("p", { style: SS.muted }, tt(t, "settings.desc")),
318
611
 
319
612
  h("div", { style: SS.row },
320
- h("span", { style: SS.label }, "启用联想"),
613
+ h("span", { style: SS.label }, tt(t, "settings.enable")),
321
614
  h("button", { type: "button", style: SS.toggleTrack(enabled), onClick: function () { toggleEnabled(!enabled); }, disabled: busy, "aria-pressed": enabled },
322
615
  h("span", { style: SS.toggleThumb(enabled) })),
323
- h("span", { style: SS.muted }, enabled ? "已开启" : "已关闭")),
616
+ h("span", { style: SS.muted }, enabled ? tt(t, "settings.on") : tt(t, "settings.off"))),
324
617
 
325
618
  h("div", { style: SS.divider }),
326
619
 
327
620
  h("div", { style: SS.row },
328
- h("span", { style: SS.label }, "提供商"),
621
+ h("span", { style: SS.label }, tt(t, "settings.provider")),
329
622
  h("select", {
330
623
  style: SS.select, value: provider, disabled: !enabled || busy,
331
624
  onChange: function (e) {
@@ -336,56 +629,76 @@ window.__ModuleLoader__.load({
336
629
  setEffort("auto"); setDirty(true);
337
630
  },
338
631
  },
339
- h("option", { value: "auto" }, "跟随当前会话模型"),
632
+ h("option", { value: "auto" }, tt(t, "settings.followSession")),
340
633
  providerGroups().map(function (g) {
341
634
  return h("option", { key: g.id, value: g.id }, g.name + "(" + g.id + ")");
342
635
  }))),
343
636
  h("div", { style: SS.row },
344
- h("span", { style: SS.label }, "模型"),
637
+ h("span", { style: SS.label }, tt(t, "settings.model")),
345
638
  h("select", {
346
639
  style: SS.select, value: model, disabled: !enabled || busy || provider === "auto",
347
640
  onChange: function (e) { setModel(e.target.value); setEffort("auto"); setDirty(true); },
348
641
  },
349
642
  provider === "auto"
350
- ? h("option", { value: "auto" }, "跟随当前会话模型")
643
+ ? h("option", { value: "auto" }, tt(t, "settings.followSession"))
351
644
  : modelsOf(provider).map(function (m) {
352
645
  return h("option", { key: m.id, value: m.id }, m.name + "(" + m.id + ")");
353
646
  }))),
354
- h("p", { style: SS.muted }, "默认跟随当前会话模型;也可固定到某个模型——选快模型更省额度。"),
647
+ h("p", { style: SS.muted }, tt(t, "settings.modelHint")),
355
648
 
356
- h("p", null, h("button", { style: SS.advBtn, onClick: function () { setAdv(!adv); } }, (adv ? "▾" : "▸") + " 高级")),
649
+ h("p", null, h("button", { style: SS.advBtn, onClick: function () { setAdv(!adv); } }, (adv ? "▾" : "▸") + tt(t, "settings.advanced"))),
357
650
  adv ? h("div", null,
358
651
  h("div", { style: SS.row },
359
- h("span", { style: SS.label }, "响应超时"),
652
+ h("span", { style: SS.label }, tt(t, "settings.timeout")),
360
653
  h("input", { style: SS.input, type: "number", min: 1, max: 60, value: timeoutSec, disabled: !enabled || busy, onChange: function (e) { setTimeoutSec(Number(e.target.value)); setDirty(true); } }),
361
- h("span", { style: SS.muted }, "秒(1–60)")),
654
+ h("span", { style: SS.muted }, tt(t, "settings.timeoutUnit"))),
362
655
  h("div", { style: SS.row },
363
- h("span", { style: SS.label }, "上下文规模"),
656
+ h("span", { style: SS.label }, tt(t, "settings.contextSize")),
364
657
  h("select", { style: SS.select, value: ctxSize, disabled: !enabled || busy, onChange: function (e) { setCtxSize(Number(e.target.value)); setDirty(true); } },
365
- h("option", { value: 3 }, "少:最近 3 条消息"),
366
- h("option", { value: 8 }, "标准:最近 8 条(默认)"),
367
- h("option", { value: 12 }, "多:最近 12 条")),
368
- h("span", { style: SS.muted }, "越多越准但越慢")),
658
+ h("option", { value: 3 }, tt(t, "settings.contextSmall")),
659
+ h("option", { value: 8 }, tt(t, "settings.contextStandard")),
660
+ h("option", { value: 12 }, tt(t, "settings.contextLarge"))),
661
+ h("span", { style: SS.muted }, tt(t, "settings.contextHint"))),
662
+ h("div", { style: SS.row },
663
+ h("span", { style: SS.label }, tt(t, "settings.maxChars")),
664
+ h("input", { style: SS.input, type: "number", min: 1000, max: 20000, step: 500, value: maxChars, disabled: !enabled || busy, onChange: function (e) { setMaxChars(Number(e.target.value)); setDirty(true); } }),
665
+ h("span", { style: SS.muted }, tt(t, "settings.maxCharsHint"))),
369
666
  h("div", { style: SS.row },
370
- h("span", { style: SS.label }, "思考深度"),
667
+ h("span", { style: SS.label }, tt(t, "settings.maxTokens")),
668
+ h("input", { style: SS.input, type: "number", min: 16, max: 512, step: 8, value: maxTokens, disabled: !enabled || busy, onChange: function (e) { setMaxTokens(Number(e.target.value)); setDirty(true); } }),
669
+ h("span", { style: SS.muted }, tt(t, "settings.maxTokensHint"))),
670
+ h("div", { style: SS.row },
671
+ h("span", { style: SS.label }, tt(t, "settings.effort")),
371
672
  h("select", { style: SS.select, value: effort, disabled: !enabled || busy, onChange: function (e) { setEffort(e.target.value); setDirty(true); } },
372
- h("option", { value: "auto" }, effortOptions.length === 0 ? "跟随模型默认(该模型未公布可调档位)" : "跟随模型默认"),
673
+ h("option", { value: "auto" }, effortOptions.length === 0 ? tt(t, "settings.effortDefaultNoLevels") : tt(t, "settings.effortDefault")),
373
674
  (function () {
374
675
  var ids = effortOptions.slice();
375
676
  if (effort && effort !== "auto" && ids.indexOf(effort) === -1) ids.push(effort); // 保留当前已保存值,避免下拉显示错位
376
677
  return ids.map(function (id) {
377
- return h("option", { key: id, value: id }, (EFFORT_LABELS[id] || id) + (id === "off" ? "(最快最省)" : ""));
678
+ return h("option", { key: id, value: id }, tt(t, EFFORT_LABEL_KEYS[id] || id) + (id === "off" ? tt(t, "settings.effortFastest") : ""));
378
679
  });
379
680
  })()),
380
- h("span", { style: SS.muted }, "联想宜浅;过深会把 token 烧在推理上")) ) : null,
681
+ h("span", { style: SS.muted }, effortSourceHint(st, t))) ) : null,
682
+
683
+ h("div", { style: SS.divider }),
684
+ h("p", { style: SS.muted }, runText(run, t)),
381
685
 
382
686
  h("div", { style: { display: "flex", gap: 10, marginTop: 16 } },
383
- h("button", { style: dim(SS.btnPrimary, busy || !dirty), onClick: save, disabled: busy || !dirty }, busy ? "保存中…" : "保存"),
384
- h("button", { style: dim(SS.btnGhost, busy), onClick: resetDefaults, disabled: busy }, "恢复默认")),
687
+ h("button", { style: dim(SS.btnPrimary, busy || !dirty), onClick: save, disabled: busy || !dirty }, busy ? tt(t, "settings.saving") : tt(t, "settings.save")),
688
+ h("button", { style: dim(SS.btnGhost, busy), onClick: resetDefaults, disabled: busy }, tt(t, "settings.reset"))),
385
689
  msg ? h("p", { style: msg.ok ? SS.ok : SS.err }, msg.text) : null);
386
690
  }
387
691
 
388
692
  function apply(ctx) {
693
+ // 注册词典并绑定翻译函数:DSH 的 locale 服务会把语言切换即时应用到已挂载的界面;
694
+ // 没有该服务(旧运行时)时保持中文兜底,不会退化成显示 key。
695
+ try {
696
+ if (ctx.locale && typeof ctx.locale.register === "function") {
697
+ ctx.locale.register(NS, { zh: DICT.zh, en: DICT.en });
698
+ boundTranslate = ctx.locale.bind(NS);
699
+ localeService = ctx.locale; // currentLang() 要读它的快照
700
+ }
701
+ } catch { /* 保持中文 */ }
389
702
  ctx.slots.inject("conversation.composer.dock", function () {
390
703
  return ctx.slots.register({
391
704
  name: "conversation.composer.dock",
@@ -398,7 +711,7 @@ window.__ModuleLoader__.load({
398
711
  name: "settings.section",
399
712
  id: "prompt-predict",
400
713
  order: 2,
401
- label: function () { return "提示词联想"; },
714
+ label: function () { return tt(boundTranslate, "settings.title"); },
402
715
  inject: function () { return {}; },
403
716
  }, SettingsSection);
404
717
  });