dsh-vibegap 0.1.1 → 0.1.3
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/README.md +16 -4
- package/lib/client.js +186 -41
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
VibeGap 的 DeepSeek Harness 原生插件。Agent 持续运行 18 秒后,dsh web
|
|
4
4
|
右下角会出现拼写单词卡;会话完成或等待确认时,完成当前单词后自动收起或继续。
|
|
5
5
|
插件可独立运行,不要求安装 VibeGap Python 桌面端。
|
|
6
|
-
如果本机 VibeGap
|
|
7
|
-
|
|
6
|
+
如果本机 VibeGap Core 正在运行,插件会自动改用桌面端的当前词书和进度游标;
|
|
7
|
+
Core 在页面打开后才由另一个 Agent 拉起也能自动接入。为避免两张卡重复出现,
|
|
8
|
+
共享模式下页内卡不自动弹出,但仍可用 `Ctrl+Alt+V` 手动呼出。运行中断线时
|
|
9
|
+
保留当前词并自动重连,不会误跳到首次下载界面。
|
|
8
10
|
|
|
9
11
|
## 安装
|
|
10
12
|
|
|
@@ -42,8 +44,11 @@ dsh plugin --profile web remove dsh-vibegap
|
|
|
42
44
|
自动发音偏好通过 dsh 官方 snapshot store 保存在当前浏览器中。无痕模式或浏览器
|
|
43
45
|
拒绝持久化时,卡片仍可使用,但刷新后进度可能丢失。
|
|
44
46
|
|
|
45
|
-
本机
|
|
46
|
-
|
|
47
|
+
本机 Core 可用时,单词和进度改由 `http://127.0.0.1:8765/panel/*` 提供,
|
|
48
|
+
插件每 5 秒低频探测一次,且仅允许来自 `localhost` 或 `127.0.0.1` 的
|
|
49
|
+
HTTP(S) 浏览器 Origin。DSH 单独使用时不会因此启动 Python/Core 进程。
|
|
50
|
+
共享模式目前只发现默认端口 `8765`;若桌面端修改了 `daemon_port`,DSH 会保持
|
|
51
|
+
独立本地模式。
|
|
47
52
|
|
|
48
53
|
## 使用
|
|
49
54
|
|
|
@@ -51,6 +56,7 @@ dsh plugin --profile web remove dsh-vibegap
|
|
|
51
56
|
- 输入错误会清空当前拼写并计数。
|
|
52
57
|
- `Tab` 显示或隐藏答案;看过答案后,本词按 fail 完成并前进。
|
|
53
58
|
- `Esc` 或右上角 `×` 隐藏卡片;同一批运行中的会话不会再次弹出。
|
|
59
|
+
- `Ctrl+Alt+V` 手动呼出或隐藏卡片(dsh 标签页处于前台时有效,不受 18 秒规则限制)。
|
|
54
60
|
- “发音”可手动播放美式发音,“自动发音”可随时开关。
|
|
55
61
|
|
|
56
62
|
## 开发
|
|
@@ -59,6 +65,12 @@ dsh plugin --profile web remove dsh-vibegap
|
|
|
59
65
|
或其他构建步骤。修改 bundle 后刷新页面;修改 `package.json` 或
|
|
60
66
|
`cordis.patch.yml` 后重启 `dsh web`。
|
|
61
67
|
|
|
68
|
+
纯逻辑与 manifest 兼容性测试:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm test
|
|
72
|
+
```
|
|
73
|
+
|
|
62
74
|
服务端 `lib/index.js` 会在本机 VibeGap daemon 存在时继续上报 agent 生命周期;
|
|
63
75
|
连接失败会静默降级,不影响 dsh。
|
|
64
76
|
|
package/lib/client.js
CHANGED
|
@@ -18,6 +18,7 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
var TRANSLATION_MAX_CHARS = 100;
|
|
19
19
|
var DAEMON_URL = "http://127.0.0.1:8765/panel";
|
|
20
20
|
var DAEMON_TIMEOUT_MS = 1000;
|
|
21
|
+
var DAEMON_PROBE_MS = 5000;
|
|
21
22
|
var DICT_URL = "https://raw.githubusercontent.com/RealKai42/qwerty-learner/master/public/dicts/CET6_T.json";
|
|
22
23
|
var STYLE_ID = "vg-card-styles";
|
|
23
24
|
var DONE_NOTICE = "会话已完成 · 拼完当前词后收起";
|
|
@@ -34,12 +35,17 @@ window.__ModuleLoader__.load({
|
|
|
34
35
|
{ autoPronounce: true },
|
|
35
36
|
{ persist: { name: "vibegap.prefs" } },
|
|
36
37
|
);
|
|
38
|
+
var posStore = runtime.createSnapshotStore(
|
|
39
|
+
{ x: null, y: null },
|
|
40
|
+
{ persist: { name: "vibegap.pos" } },
|
|
41
|
+
);
|
|
37
42
|
|
|
38
43
|
var css = [
|
|
39
44
|
".vg-card{position:fixed;right:24px;bottom:24px;width:min(390px,calc(100vw - 32px));box-sizing:border-box;padding:16px;border:1px solid var(--dsw-alias-border-l,#d8d8d8);border-radius:14px;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#202020);box-shadow:0 12px 34px rgba(0,0,0,.18);font-family:inherit;pointer-events:auto;outline:none;z-index:1}",
|
|
40
45
|
".vg-card:focus{border-color:var(--dsw-alias-border-focus,#5794ff);box-shadow:0 12px 34px rgba(0,0,0,.18),0 0 0 2px rgba(87,148,255,.2)}",
|
|
41
|
-
".vg-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}",
|
|
46
|
+
".vg-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;cursor:move;user-select:none;touch-action:none}",
|
|
42
47
|
".vg-title{font-size:13px;font-weight:650;letter-spacing:.02em}",
|
|
48
|
+
".vg-prog{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;font-size:11px;color:var(--dsw-alias-label-tertiary,#888)}",
|
|
43
49
|
".vg-close{min-width:28px!important;padding:0!important;font-size:18px!important}",
|
|
44
50
|
".vg-banner{margin:-2px 0 14px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,#f2f3f5);font-size:12px;line-height:1.4}",
|
|
45
51
|
".vg-word{display:flex;flex-wrap:wrap;justify-content:center;gap:4px;min-height:48px;margin:16px 0 12px}",
|
|
@@ -53,6 +59,7 @@ window.__ModuleLoader__.load({
|
|
|
53
59
|
".vg-hint{margin-top:12px;text-align:center;color:var(--dsw-alias-label-tertiary,#888);font-size:11px}",
|
|
54
60
|
".vg-empty{padding:18px 4px 8px;text-align:center;color:var(--dsw-alias-label-secondary,#666);font-size:13px;line-height:1.6}",
|
|
55
61
|
".vg-error{margin-top:10px;color:var(--dsw-alias-state-danger-primary,#c93c37);font-size:12px}",
|
|
62
|
+
".vg-reconnect{margin:8px 0 0;text-align:center;color:var(--dsw-alias-state-warning-primary,#9a6700);font-size:12px}",
|
|
56
63
|
".vg-shake{animation:vg-shake .28s ease-in-out}",
|
|
57
64
|
"@keyframes vg-blink{50%{border-color:transparent}}",
|
|
58
65
|
"@keyframes vg-shake{25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}",
|
|
@@ -180,21 +187,49 @@ window.__ModuleLoader__.load({
|
|
|
180
187
|
if (payload.error) throw new Error(payload.error);
|
|
181
188
|
return payload;
|
|
182
189
|
}
|
|
190
|
+
function syncLocalProgress(remoteProgress) {
|
|
191
|
+
if (!remoteProgress || !Number.isFinite(Number(remoteProgress.cursor))) return;
|
|
192
|
+
progressStore.update(function (draft) {
|
|
193
|
+
if (!Array.isArray(draft.words) || draft.words.length === 0) return;
|
|
194
|
+
if (Number(remoteProgress.total) !== draft.words.length) return;
|
|
195
|
+
draft.cursor = Math.min(Math.max(Number(remoteProgress.cursor), 0), draft.words.length - 1);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function daemonFailureState(current) {
|
|
199
|
+
if (current.mode === "daemon" || current.mode === "disconnected") {
|
|
200
|
+
return { mode: "disconnected", word: current.word };
|
|
201
|
+
}
|
|
202
|
+
if (current.mode === "probing") return { mode: "local", word: null };
|
|
203
|
+
return current;
|
|
204
|
+
}
|
|
183
205
|
function useDaemonBackend() {
|
|
184
206
|
var backend = React.useState({ mode: "probing", word: null });
|
|
207
|
+
var current = useCell({ mode: "probing", word: null });
|
|
208
|
+
var setBackend = function (next) { current.current = next; backend[1](next); };
|
|
185
209
|
React.useEffect(function () {
|
|
186
210
|
var active = true;
|
|
187
|
-
|
|
211
|
+
var timer = null;
|
|
212
|
+
async function probe() {
|
|
188
213
|
try {
|
|
189
214
|
var state = await panelRequest("/state");
|
|
190
215
|
if (!state.ready) throw new Error("daemon has no wordbook");
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
216
|
+
syncLocalProgress(state.progress);
|
|
217
|
+
var cursor = state.progress && Number(state.progress.cursor);
|
|
218
|
+
var known = current.current.word && Number(current.current.word.position);
|
|
219
|
+
if (current.current.mode !== "daemon" || cursor !== known) {
|
|
220
|
+
var word = await panelRequest("/next-word");
|
|
221
|
+
if (active) setBackend({ mode: "daemon", word: word });
|
|
222
|
+
}
|
|
223
|
+
} catch (_) {
|
|
224
|
+
var failed = daemonFailureState(current.current);
|
|
225
|
+
if (active && failed !== current.current) setBackend(failed);
|
|
226
|
+
}
|
|
227
|
+
if (active) timer = setTimeout(probe, DAEMON_PROBE_MS);
|
|
228
|
+
}
|
|
229
|
+
probe();
|
|
230
|
+
return function () { active = false; if (timer) clearTimeout(timer); };
|
|
196
231
|
}, []);
|
|
197
|
-
return backend;
|
|
232
|
+
return [backend[0], setBackend];
|
|
198
233
|
}
|
|
199
234
|
|
|
200
235
|
function advanceProgress(total) {
|
|
@@ -232,10 +267,16 @@ window.__ModuleLoader__.load({
|
|
|
232
267
|
);
|
|
233
268
|
}
|
|
234
269
|
|
|
235
|
-
function
|
|
270
|
+
function canAutoPopup(backendMode) {
|
|
271
|
+
return backendMode !== "daemon" && backendMode !== "disconnected";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function useLifecycle(sessionState, visible, setVisible, showBanner, onRestart, allowAutoPopup) {
|
|
236
275
|
var ref = useCell({ running: false, rows: {}, suppressed: false, arm: null, pending: null });
|
|
237
276
|
var runningRef = useCell(false);
|
|
238
277
|
var visibleRef = useCell(visible);
|
|
278
|
+
var autoPopupRef = useCell(allowAutoPopup);
|
|
279
|
+
autoPopupRef.current = allowAutoPopup;
|
|
239
280
|
React.useEffect(function () { visibleRef.current = visible; }, [visible]);
|
|
240
281
|
React.useEffect(function () {
|
|
241
282
|
var anyRunning = sessionState.ids.some(function (id) {
|
|
@@ -247,7 +288,7 @@ window.__ModuleLoader__.load({
|
|
|
247
288
|
life.suppressed = false; life.pending = null;
|
|
248
289
|
onRestart();
|
|
249
290
|
if (!visibleRef.current) life.arm = setTimeout(function () {
|
|
250
|
-
if (!runningRef.current || life.suppressed) return;
|
|
291
|
+
if (!runningRef.current || life.suppressed || !autoPopupRef.current) return;
|
|
251
292
|
setVisible(true);
|
|
252
293
|
if (life.pending) { showBanner(life.pending); life.pending = null; }
|
|
253
294
|
}, POPUP_DELAY_MS);
|
|
@@ -267,34 +308,47 @@ window.__ModuleLoader__.load({
|
|
|
267
308
|
return { state: ref, running: runningRef };
|
|
268
309
|
}
|
|
269
310
|
|
|
311
|
+
function createTypingHandler(configRef) {
|
|
312
|
+
return function onKey(event) {
|
|
313
|
+
var config = configRef.current;
|
|
314
|
+
if (document.activeElement !== config.card.current) return;
|
|
315
|
+
if (event.key === "Escape") { event.preventDefault(); config.hide(); return; }
|
|
316
|
+
if (event.key === "Tab") {
|
|
317
|
+
event.preventDefault(); config.setPeeked(true); config.setTyped(0);
|
|
318
|
+
config.setRevealed(function (value) { return !value; }); return;
|
|
319
|
+
}
|
|
320
|
+
if (event.key.length !== 1 || !/[a-zA-Z'\- ]/.test(event.key)) return;
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
if (config.revealed) config.setRevealed(false);
|
|
323
|
+
if (event.key.toLowerCase() === config.word[config.typed].toLowerCase()) {
|
|
324
|
+
var next = config.typed + 1; config.setTyped(next);
|
|
325
|
+
if (next === config.word.length) config.complete();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
config.setTypos(function (value) { return value + 1; });
|
|
329
|
+
config.setTyped(0); config.shake();
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
270
333
|
function useTyping(config) {
|
|
334
|
+
var configRef = useCell(config);
|
|
335
|
+
configRef.current = config;
|
|
271
336
|
React.useEffect(function () {
|
|
272
337
|
if (!config.visible || !config.focused || !config.word || config.busy) return;
|
|
273
|
-
|
|
274
|
-
if (document.activeElement !== config.card.current) return;
|
|
275
|
-
if (event.key === "Escape") { event.preventDefault(); config.hide(); return; }
|
|
276
|
-
if (event.key === "Tab") {
|
|
277
|
-
event.preventDefault(); config.setPeeked(true); config.setTyped(0);
|
|
278
|
-
config.setRevealed(function (value) { return !value; }); return;
|
|
279
|
-
}
|
|
280
|
-
if (event.key.length !== 1 || !/[a-zA-Z'\- ]/.test(event.key)) return;
|
|
281
|
-
event.preventDefault();
|
|
282
|
-
if (config.revealed) config.setRevealed(false);
|
|
283
|
-
if (event.key.toLowerCase() === config.word[config.typed].toLowerCase()) {
|
|
284
|
-
var next = config.typed + 1; config.setTyped(next);
|
|
285
|
-
if (next === config.word.length) config.complete();
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
config.setTypos(function (value) { return value + 1; });
|
|
289
|
-
config.setTyped(0); config.shake();
|
|
290
|
-
}
|
|
338
|
+
var onKey = createTypingHandler(configRef);
|
|
291
339
|
document.addEventListener("keydown", onKey);
|
|
292
340
|
return function () { document.removeEventListener("keydown", onKey); };
|
|
293
|
-
}, [config]);
|
|
341
|
+
}, [config.visible, config.focused, config.word, config.busy]);
|
|
294
342
|
}
|
|
295
343
|
|
|
296
344
|
function CardBody(props) {
|
|
297
|
-
if (!props.word)
|
|
345
|
+
if (!props.word) {
|
|
346
|
+
if (props.connectionLost) {
|
|
347
|
+
return h("div", { className: "vg-empty", role: "status" },
|
|
348
|
+
"桌面端连接已断开,正在自动重连");
|
|
349
|
+
}
|
|
350
|
+
return h(EmptyCard, { status: props.download, setStatus: props.setDownload });
|
|
351
|
+
}
|
|
298
352
|
var translation = props.word.trans.join(";");
|
|
299
353
|
return h(React.Fragment, null,
|
|
300
354
|
h(LetterGrid, {
|
|
@@ -304,15 +358,17 @@ window.__ModuleLoader__.load({
|
|
|
304
358
|
h("div", { className: "vg-trans", title: translation }, translation.slice(0, TRANSLATION_MAX_CHARS)),
|
|
305
359
|
h("div", { className: "vg-meta" },
|
|
306
360
|
props.word.usphone ? "/" + props.word.usphone + "/" : null,
|
|
307
|
-
props.typos ? "
|
|
361
|
+
props.typos ? "错 " + props.typos : null,
|
|
308
362
|
props.outcome ? h("span", { role: "status" }, props.outcome === "fail" ? "已记为需复习" : "拼写正确") : null,
|
|
309
363
|
),
|
|
364
|
+
props.connectionLost ? h("div", { className: "vg-reconnect", role: "status" },
|
|
365
|
+
"桌面端连接已断开,当前词会在重连后继续") : null,
|
|
310
366
|
h("div", { className: "vg-actions" },
|
|
311
367
|
h(Button, { size: "sm", variant: "outline", onClick: props.pronounce }, "发音"),
|
|
312
368
|
h(Button, { size: "sm", variant: "ghost", onClick: props.toggleAuto },
|
|
313
369
|
"自动发音:" + (props.autoPronounce ? "开" : "关")),
|
|
314
370
|
),
|
|
315
|
-
h("div", { className: "vg-hint" }, "点击卡片后拼写 · Tab 查看答案 · Esc 隐藏"),
|
|
371
|
+
h("div", { className: "vg-hint" }, "点击卡片后拼写 · Tab 查看答案 · Esc 隐藏 · Ctrl+Alt+V 呼出"),
|
|
316
372
|
);
|
|
317
373
|
}
|
|
318
374
|
|
|
@@ -323,6 +379,7 @@ window.__ModuleLoader__.load({
|
|
|
323
379
|
peeked: React.useState(false), shaking: React.useState(false), busy: React.useState(false),
|
|
324
380
|
outcome: React.useState(""), holdWord: React.useState(null),
|
|
325
381
|
download: React.useState({ busy: false, error: "" }),
|
|
382
|
+
pos: React.useState(null),
|
|
326
383
|
};
|
|
327
384
|
}
|
|
328
385
|
|
|
@@ -341,12 +398,18 @@ window.__ModuleLoader__.load({
|
|
|
341
398
|
function activeSelection(local, backend) {
|
|
342
399
|
var remote = backend[0];
|
|
343
400
|
if (remote.mode === "daemon" && remote.word) {
|
|
344
|
-
return { ordered: [remote.word], word: remote.word, isDaemon: true
|
|
401
|
+
return { ordered: [remote.word], word: remote.word, isDaemon: true,
|
|
402
|
+
connectionLost: false, remoteUnavailable: false };
|
|
403
|
+
}
|
|
404
|
+
if (remote.mode === "disconnected") {
|
|
405
|
+
return { ordered: remote.word ? [remote.word] : [], word: remote.word, isDaemon: true,
|
|
406
|
+
connectionLost: true, remoteUnavailable: true };
|
|
345
407
|
}
|
|
346
|
-
return { ordered: local.ordered, word: local.word, isDaemon: false
|
|
408
|
+
return { ordered: local.ordered, word: local.word, isDaemon: false,
|
|
409
|
+
connectionLost: false, remoteUnavailable: false };
|
|
347
410
|
}
|
|
348
411
|
|
|
349
|
-
function useLifecycleBridge(sessionState, states, refs) {
|
|
412
|
+
function useLifecycleBridge(sessionState, states, refs, allowAutoPopup) {
|
|
350
413
|
var showBanner = function (message) {
|
|
351
414
|
refs.banner.current = message; states.banner[1](message);
|
|
352
415
|
};
|
|
@@ -355,7 +418,9 @@ window.__ModuleLoader__.load({
|
|
|
355
418
|
clearTimeout(refs.hideTimer.current); refs.hideTimer.current = null;
|
|
356
419
|
showBanner(null); resetInput(states);
|
|
357
420
|
};
|
|
358
|
-
var tracker = useLifecycle(
|
|
421
|
+
var tracker = useLifecycle(
|
|
422
|
+
sessionState, states.visible[0], states.visible[1], showBanner, onRestart, allowAutoPopup,
|
|
423
|
+
);
|
|
359
424
|
return { tracker: tracker, showBanner: showBanner };
|
|
360
425
|
}
|
|
361
426
|
function resetInput(states) {
|
|
@@ -398,16 +463,17 @@ window.__ModuleLoader__.load({
|
|
|
398
463
|
advanceProgress(model.selection.ordered.length); return;
|
|
399
464
|
}
|
|
400
465
|
try {
|
|
401
|
-
await panelRequest("/commit", {
|
|
466
|
+
var result = await panelRequest("/commit", {
|
|
402
467
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
403
468
|
body: JSON.stringify({
|
|
404
469
|
result: model.states.peeked[0] ? "fail" : "pass",
|
|
405
470
|
typo_count: model.states.typos[0],
|
|
406
471
|
}),
|
|
407
472
|
});
|
|
473
|
+
syncLocalProgress(result);
|
|
408
474
|
var word = await panelRequest("/next-word");
|
|
409
475
|
model.backend[1]({ mode: "daemon", word: word });
|
|
410
|
-
} catch (_) { model.backend[1](
|
|
476
|
+
} catch (_) { model.backend[1](daemonFailureState(model.backend[0])); }
|
|
411
477
|
}
|
|
412
478
|
|
|
413
479
|
function shakeCard(states) {
|
|
@@ -415,6 +481,31 @@ window.__ModuleLoader__.load({
|
|
|
415
481
|
setTimeout(function () { states.shaking[1](false); }, SHAKE_MS);
|
|
416
482
|
}
|
|
417
483
|
|
|
484
|
+
function startDrag(model, event) {
|
|
485
|
+
if (event.target.closest("button,a,input,textarea,select")) return;
|
|
486
|
+
var card = model.refs.card.current;
|
|
487
|
+
if (!card || event.button !== 0) return;
|
|
488
|
+
event.preventDefault();
|
|
489
|
+
var rect = card.getBoundingClientRect();
|
|
490
|
+
var offsetX = event.clientX - rect.left;
|
|
491
|
+
var offsetY = event.clientY - rect.top;
|
|
492
|
+
var last = null;
|
|
493
|
+
function move(ev) {
|
|
494
|
+
last = {
|
|
495
|
+
x: Math.min(Math.max(ev.clientX - offsetX, 0), Math.max(window.innerWidth - rect.width, 0)),
|
|
496
|
+
y: Math.min(Math.max(ev.clientY - offsetY, 0), Math.max(window.innerHeight - 48, 0)),
|
|
497
|
+
};
|
|
498
|
+
model.states.pos[1](last);
|
|
499
|
+
}
|
|
500
|
+
function up() {
|
|
501
|
+
document.removeEventListener("pointermove", move);
|
|
502
|
+
document.removeEventListener("pointerup", up);
|
|
503
|
+
if (last) try { posStore.set(last); } catch (_) {}
|
|
504
|
+
}
|
|
505
|
+
document.addEventListener("pointermove", move);
|
|
506
|
+
document.addEventListener("pointerup", up);
|
|
507
|
+
}
|
|
508
|
+
|
|
418
509
|
function useCardEffects(model) {
|
|
419
510
|
var word = displayedWord(model);
|
|
420
511
|
React.useEffect(function () { resetInput(model.states); }, [word && word.name]);
|
|
@@ -424,6 +515,29 @@ window.__ModuleLoader__.load({
|
|
|
424
515
|
React.useEffect(function () {
|
|
425
516
|
return function () { if (model.refs.hideTimer.current) clearTimeout(model.refs.hideTimer.current); };
|
|
426
517
|
}, []);
|
|
518
|
+
React.useEffect(function () {
|
|
519
|
+
// 页面内手动唤醒:Ctrl+Alt+V 唤出/隐藏(dsh 标签页聚焦时有效)
|
|
520
|
+
function onSummon(event) {
|
|
521
|
+
if (!event.ctrlKey || !event.altKey || event.key.toLowerCase() !== "v") return;
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
if (model.states.visible[0]) { hideCard(model, true); return; }
|
|
524
|
+
model.lifecycle.tracker.state.current.suppressed = false;
|
|
525
|
+
model.states.visible[1](true);
|
|
526
|
+
}
|
|
527
|
+
document.addEventListener("keydown", onSummon);
|
|
528
|
+
return function () { document.removeEventListener("keydown", onSummon); };
|
|
529
|
+
}, [model.states.visible[0]]);
|
|
530
|
+
React.useEffect(function () {
|
|
531
|
+
// 恢复上次拖到的位置;越出当前视口则放弃,回默认右下角
|
|
532
|
+
try {
|
|
533
|
+
var saved = posStore.getSnapshot();
|
|
534
|
+
if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y) &&
|
|
535
|
+
saved.x >= 0 && saved.y >= 0 &&
|
|
536
|
+
saved.x < window.innerWidth - 60 && saved.y < window.innerHeight - 48) {
|
|
537
|
+
model.states.pos[1]({ x: saved.x, y: saved.y });
|
|
538
|
+
}
|
|
539
|
+
} catch (_) {}
|
|
540
|
+
}, []);
|
|
427
541
|
}
|
|
428
542
|
|
|
429
543
|
function useCardTyping(model) {
|
|
@@ -432,7 +546,8 @@ window.__ModuleLoader__.load({
|
|
|
432
546
|
useTyping({
|
|
433
547
|
visible: states.visible[0], focused: states.focused[0],
|
|
434
548
|
word: word && word.name,
|
|
435
|
-
busy: states.busy[0]
|
|
549
|
+
busy: states.busy[0] || model.selection.remoteUnavailable,
|
|
550
|
+
typed: states.typed[0], revealed: states.revealed[0],
|
|
436
551
|
card: model.refs.card, setTyped: states.typed[1], setTypos: states.typos[1],
|
|
437
552
|
setRevealed: states.revealed[1], setPeeked: states.peeked[1],
|
|
438
553
|
hide: function () { hideCard(model, true); },
|
|
@@ -446,13 +561,21 @@ window.__ModuleLoader__.load({
|
|
|
446
561
|
return h(React.Fragment, null, h("style", { id: STYLE_ID }, css),
|
|
447
562
|
h("section", {
|
|
448
563
|
className: "vg-card", tabIndex: 0, ref: model.refs.card, "aria-label": "VibeGap 单词卡",
|
|
564
|
+
style: states.pos[0]
|
|
565
|
+
? { left: states.pos[0].x + "px", top: states.pos[0].y + "px", right: "auto", bottom: "auto" }
|
|
566
|
+
: undefined,
|
|
449
567
|
onFocus: function (event) { if (event.target === event.currentTarget) states.focused[1](true); },
|
|
450
568
|
onBlur: function () { states.focused[1](false); },
|
|
451
569
|
onMouseDown: function (event) {
|
|
452
570
|
if (!event.target.closest("button,a,input,textarea,select")) model.refs.card.current.focus();
|
|
453
571
|
},
|
|
454
572
|
},
|
|
455
|
-
h("div", {
|
|
573
|
+
h("div", {
|
|
574
|
+
className: "vg-head", title: "按住拖动",
|
|
575
|
+
onPointerDown: function (event) { startDrag(model, event); },
|
|
576
|
+
},
|
|
577
|
+
h("div", { className: "vg-title" }, "VibeGap"),
|
|
578
|
+
model.progressText ? h("span", { className: "vg-prog" }, model.progressText) : null,
|
|
456
579
|
h(Button, { className: "vg-close", size: "sm", variant: "ghost", title: "隐藏",
|
|
457
580
|
onClick: function () { hideCard(model, true); } }, "×")),
|
|
458
581
|
states.banner[0] ? h("div", { className: "vg-banner", role: "status" }, states.banner[0]) : null,
|
|
@@ -467,6 +590,7 @@ window.__ModuleLoader__.load({
|
|
|
467
590
|
word: word, typed: states.typed[0], typos: states.typos[0], revealed: states.revealed[0],
|
|
468
591
|
focused: states.focused[0], shaking: states.shaking[0], download: states.download[0],
|
|
469
592
|
outcome: states.outcome[0], setDownload: states.download[1], autoPronounce: model.autoPronounce,
|
|
593
|
+
connectionLost: model.selection.connectionLost,
|
|
470
594
|
pronounce: function () { if (word) safePronounce(word.name); },
|
|
471
595
|
toggleAuto: function () { prefsStore.set({ autoPronounce: !model.autoPronounce }); },
|
|
472
596
|
};
|
|
@@ -480,12 +604,24 @@ window.__ModuleLoader__.load({
|
|
|
480
604
|
var prefs = useSnapshot(prefsStore);
|
|
481
605
|
var states = useCardStates();
|
|
482
606
|
var refs = { card: useCell(null), hideTimer: useCell(null), banner: useCell(null) };
|
|
483
|
-
var lifecycle = useLifecycleBridge(sessionState, states, refs);
|
|
484
607
|
var backend = useDaemonBackend();
|
|
608
|
+
var lifecycle = useLifecycleBridge(
|
|
609
|
+
sessionState, states, refs, canAutoPopup(backend[0].mode),
|
|
610
|
+
);
|
|
485
611
|
var selection = activeSelection(useWordSelection(progress), backend);
|
|
612
|
+
var word = selection.word;
|
|
613
|
+
var progressText = "";
|
|
614
|
+
if (selection.isDaemon && word && Number.isFinite(Number(word.total))) {
|
|
615
|
+
progressText = word.position + "/" + word.total +
|
|
616
|
+
(selection.connectionLost ? " · 桌面端重连中" : " · 共享桌面进度");
|
|
617
|
+
} else if (selection.ordered.length) {
|
|
618
|
+
var cursorShown = Math.min(Math.max(Number(progress.cursor) || 0, 0), selection.ordered.length - 1);
|
|
619
|
+
progressText = cursorShown + "/" + selection.ordered.length + " · 本地进度";
|
|
620
|
+
}
|
|
486
621
|
var model = {
|
|
487
622
|
states: states, refs: refs, lifecycle: lifecycle, selection: selection, backend: backend,
|
|
488
623
|
autoPronounce: !prefs || prefs.autoPronounce !== false,
|
|
624
|
+
progressText: progressText,
|
|
489
625
|
};
|
|
490
626
|
useCardEffects(model);
|
|
491
627
|
useCardTyping(model);
|
|
@@ -494,6 +630,15 @@ window.__ModuleLoader__.load({
|
|
|
494
630
|
|
|
495
631
|
exports.apply = apply;
|
|
496
632
|
exports.inject = inject;
|
|
633
|
+
exports.__test = {
|
|
634
|
+
activeSelection: activeSelection,
|
|
635
|
+
canAutoPopup: canAutoPopup,
|
|
636
|
+
createTypingHandler: createTypingHandler,
|
|
637
|
+
daemonFailureState: daemonFailureState,
|
|
638
|
+
normalizeWords: normalizeWords,
|
|
639
|
+
transitionNotice: transitionNotice,
|
|
640
|
+
useTyping: useTyping,
|
|
641
|
+
};
|
|
497
642
|
return module.exports;
|
|
498
643
|
},
|
|
499
644
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vibegap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Self-contained vocabulary flashcards for DeepSeek Harness web sessions",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
},
|
|
11
11
|
"keywords": ["dsh-plugin", "vibegap", "vocabulary", "productivity"],
|
|
12
12
|
"files": ["lib", "cordis.patch.yml", "README.md"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test test/*.test.js"
|
|
15
|
+
},
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
18
|
"url": "git+https://github.com/ktao732084-arch/vibegap.git",
|