dsh-prompt-predict 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/CHANGELOG.zh.md +54 -0
- package/README.md +140 -94
- package/README.zh.md +216 -0
- package/client/client.js +364 -51
- package/lib/host-route.mjs +113 -33
- package/lib/messages.mjs +96 -0
- package/lib/predict.mjs +28 -8
- package/package.json +7 -4
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
|
|
17
|
-
off: "
|
|
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
|
|
44
|
-
|
|
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:
|
|
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("
|
|
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
|
-
|
|
147
|
-
|
|
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")
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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 },
|
|
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 }, "
|
|
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 },
|
|
366
|
-
h("option", { value: 8 },
|
|
367
|
-
h("option", { value: 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 }, (
|
|
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 },
|
|
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
|
});
|