dsh-genui 0.1.3 → 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/README.md +8 -3
- package/README.zh-CN.md +8 -3
- package/lib/client.js +165 -9
- package/lib/index.js +6134 -6047
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,7 +153,11 @@ DSH locks in its plugin set at startup, so **nothing changes in the UI until you
|
|
|
153
153
|
1. Go back to the terminal running DSH and press `Ctrl+C`
|
|
154
154
|
2. Start it again: `dsh web`
|
|
155
155
|
3. Refresh the browser
|
|
156
|
-
4. **Start a new conversation**
|
|
156
|
+
4. **Start a new conversation**
|
|
157
|
+
|
|
158
|
+
### Turn the authoring prompt on
|
|
159
|
+
|
|
160
|
+
Click the sparkle button in the composer tool row to turn GenUI authoring on. The toggle starts off after DSH restarts or the plugin reloads, and it only controls the authoring guidance sent to the model. Card rendering stays installed either way.
|
|
157
161
|
|
|
158
162
|
### Check that it worked
|
|
159
163
|
|
|
@@ -193,7 +197,7 @@ DSH needs pnpm to manage plugins. Run `corepack enable` (or `npm i -g pnpm`), th
|
|
|
193
197
|
|
|
194
198
|
**The AI doesn't use interfaces on its own?**
|
|
195
199
|
|
|
196
|
-
|
|
200
|
+
If the sparkle toggle is off, the model has no GenUI authoring guidance. Turn it on, then say something like "give me a form / chart / interface."
|
|
197
201
|
|
|
198
202
|
**Does this change how DSH normally works?**
|
|
199
203
|
|
|
@@ -209,7 +213,7 @@ Yes. Run `dsh plugin remove`, restart, and DSH is back to its original state wit
|
|
|
209
213
|
|
|
210
214
|
In short: the model no longer writes only text — it also writes a *description of an interface*, and the browser renders that description into real components.
|
|
211
215
|
|
|
212
|
-
A bit more detail: the plugin adds a prompt section teaching the model to emit structured JSON (inside a `schemaJson` code block) when an interface would help. The DSH web client picks up that JSON and hands it to a renderer. It's streaming, so the interface appears as the model writes it — no waiting for the full reply.
|
|
216
|
+
A bit more detail: when the composer toggle is enabled, the plugin adds a prompt section teaching the model to emit structured JSON (inside a `schemaJson` code block) when an interface would help. The DSH web client picks up that JSON and hands it to a renderer. It's streaming, so the interface appears as the model writes it — no waiting for the full reply.
|
|
213
217
|
|
|
214
218
|
The components come from [OpenTiny GenUI SDK](https://opentiny.design/genui-sdk) and the OpenTiny Vue library — OpenTiny's generative-UI stack, a spec plus rendering engine for letting LLMs produce interfaces. dsh-genui wires that into the DSH conversation flow.
|
|
215
219
|
|
|
@@ -219,6 +223,7 @@ On safety: the model can only use whitelisted components. It cannot inject HTML
|
|
|
219
223
|
|
|
220
224
|
## Links
|
|
221
225
|
|
|
226
|
+
- Changelog: <https://github.com/lhuans/dsh-genui/releases>
|
|
222
227
|
- npm package: <https://www.npmjs.com/package/dsh-genui>
|
|
223
228
|
- OpenTiny GenUI SDK: <https://github.com/opentiny/genui-sdk>
|
|
224
229
|
- DeepSeek Harness: <https://github.com/deepseek-ai/deepseek-harness>
|
package/README.zh-CN.md
CHANGED
|
@@ -153,7 +153,11 @@ DSH 在启动时就锁定了当前的插件集合,所以**装完插件不重
|
|
|
153
153
|
1. 回到跑着 DSH 的那个终端,按 `Ctrl+C` 停掉
|
|
154
154
|
2. 重新启动:`dsh web`
|
|
155
155
|
3. 浏览器刷新一下页面
|
|
156
|
-
4.
|
|
156
|
+
4. **开一个新会话**
|
|
157
|
+
|
|
158
|
+
### 打开 GenUI 提示词
|
|
159
|
+
|
|
160
|
+
点击输入框工具行里的星形按钮,开启 GenUI 授权提示词。DSH 重启或插件重新加载后,开关默认关闭;它只控制发给模型的提示词,卡片渲染能力始终保留。
|
|
157
161
|
|
|
158
162
|
### 验证装好了没
|
|
159
163
|
|
|
@@ -193,7 +197,7 @@ DSH 管理插件依赖 pnpm。执行 `corepack enable`(或 `npm i -g pnpm`)
|
|
|
193
197
|
|
|
194
198
|
**AI 不主动用界面回答?**
|
|
195
199
|
|
|
196
|
-
|
|
200
|
+
如果星形开关是关闭的,模型不会收到 GenUI 提示词。先打开开关,再说一句「用界面/表单/图表的方式给我」就行。
|
|
197
201
|
|
|
198
202
|
**会不会影响原来的用法?**
|
|
199
203
|
|
|
@@ -209,7 +213,7 @@ DSH 管理插件依赖 pnpm。执行 `corepack enable`(或 `npm i -g pnpm`)
|
|
|
209
213
|
|
|
210
214
|
一句话:AI 不再只写文字,它还会写一段「界面描述」,浏览器把这段描述渲染成真正的组件。
|
|
211
215
|
|
|
212
|
-
|
|
216
|
+
具体一点:输入框开关打开时,插件给模型加一段提示词,教它在需要的时候输出一段结构化的 JSON(写在 `schemaJson` 代码块里);DSH 网页端拿到这段 JSON 后,交给渲染器变成实际组件。整个过程是流式的,AI 写到哪、界面就渲染到哪,不用等它把话说完。
|
|
213
217
|
|
|
214
218
|
组件本身来自 [OpenTiny GenUI SDK](https://opentiny.design/genui-sdk) 和 OpenTiny Vue 组件库——这是 OpenTiny 团队做的生成式 UI 方案,一整套「让大模型输出界面」的规范和渲染引擎。dsh-genui 做的事情,是把它接到了 DSH 的对话流里。
|
|
215
219
|
|
|
@@ -219,6 +223,7 @@ DSH 管理插件依赖 pnpm。执行 `corepack enable`(或 `npm i -g pnpm`)
|
|
|
219
223
|
|
|
220
224
|
## 相关链接
|
|
221
225
|
|
|
226
|
+
- 更新记录:<https://github.com/lhuans/dsh-genui/releases>
|
|
222
227
|
- npm 包:<https://www.npmjs.com/package/dsh-genui>
|
|
223
228
|
- OpenTiny GenUI SDK:<https://github.com/opentiny/genui-sdk>
|
|
224
229
|
- DeepSeek Harness:<https://github.com/deepseek-ai/deepseek-harness>
|
package/lib/client.js
CHANGED
|
@@ -7,6 +7,7 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
let react_dom = require("react-dom");
|
|
10
11
|
//#region src/client/genui-card-tag.ts
|
|
11
12
|
/** Custom element tag for the Vue GenUI card (thin client + runtime share this string). */
|
|
12
13
|
const DSH_GENUI_CARD_TAG = "dsh-genui-card";
|
|
@@ -117,9 +118,11 @@ window.__ModuleLoader__.load({
|
|
|
117
118
|
* Split assistant text into markdown and schema-card segments.
|
|
118
119
|
* @param text - full assistant text block (streaming or final).
|
|
119
120
|
* @param streaming - whether the turn is still generating.
|
|
121
|
+
* @param interrupted - whether generation was stopped after partial content.
|
|
120
122
|
* @returns ordered segments for mixed markdown + GenUI rendering.
|
|
121
123
|
*/
|
|
122
|
-
function splitAssistantText(text, streaming) {
|
|
124
|
+
function splitAssistantText(text, streaming, interrupted = false) {
|
|
125
|
+
const live = streaming || interrupted;
|
|
123
126
|
const segments = [];
|
|
124
127
|
let last = 0;
|
|
125
128
|
COMPLETE_FENCE.lastIndex = 0;
|
|
@@ -138,7 +141,7 @@ window.__ModuleLoader__.load({
|
|
|
138
141
|
}
|
|
139
142
|
const rest = text.slice(last);
|
|
140
143
|
if (rest.length === 0) return segments;
|
|
141
|
-
if (
|
|
144
|
+
if (live) {
|
|
142
145
|
const open = OPEN_FENCE.exec(rest);
|
|
143
146
|
if (open !== null && open.index !== void 0) {
|
|
144
147
|
const before = rest.slice(0, open.index);
|
|
@@ -162,13 +165,13 @@ window.__ModuleLoader__.load({
|
|
|
162
165
|
}
|
|
163
166
|
//#endregion
|
|
164
167
|
//#region \0dsh-css:module:src/client/genui-assistant.module.css.mjs
|
|
165
|
-
const css = ".vHCAGq_root{flex-direction:column;gap:.5rem;min-width:0;display:flex}.vHCAGq_body{flex-direction:column;gap:.75rem;min-width:0;display:flex}.vHCAGq_card{isolation:isolate;border-radius:8px;width:100%;max-width:100%;display:block;overflow:auto}.vHCAGq_card[data-dsh-genui-card-pending],.vHCAGq_card[data-dsh-genui-card-error]{min-height:4.5rem}.vHCAGq_think{opacity:.85;font-size:.9em}.vHCAGq_stopped{opacity:.7;font-style:italic}.vHCAGq_images{flex-wrap:wrap;gap:.5rem;display:flex}.vHCAGq_image{object-fit:contain;border-radius:6px;max-width:100%;max-height:240px}";
|
|
166
|
-
const tagId = "dsh-genui/src/client/genui-assistant.module.css";
|
|
167
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
168
|
+
const css$1 = ".vHCAGq_root{flex-direction:column;gap:.5rem;min-width:0;display:flex}.vHCAGq_body{flex-direction:column;gap:.75rem;min-width:0;display:flex}.vHCAGq_card{isolation:isolate;border-radius:8px;width:100%;max-width:100%;display:block;overflow:auto}.vHCAGq_card[data-dsh-genui-card-pending],.vHCAGq_card[data-dsh-genui-card-error]{min-height:4.5rem}.vHCAGq_think{opacity:.85;font-size:.9em}.vHCAGq_stopped{opacity:.7;font-style:italic}.vHCAGq_images{flex-wrap:wrap;gap:.5rem;display:flex}.vHCAGq_image{object-fit:contain;border-radius:6px;max-width:100%;max-height:240px}";
|
|
169
|
+
const tagId$1 = "dsh-genui/src/client/genui-assistant.module.css";
|
|
170
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
168
171
|
const tag = document.createElement("style");
|
|
169
172
|
tag.dataset.plugin = "dsh-genui";
|
|
170
|
-
tag.dataset.pluginCss = tagId;
|
|
171
|
-
tag.textContent = css;
|
|
173
|
+
tag.dataset.pluginCss = tagId$1;
|
|
174
|
+
tag.textContent = css$1;
|
|
172
175
|
document.head.appendChild(tag);
|
|
173
176
|
}
|
|
174
177
|
var genui_assistant_module_css_default = {
|
|
@@ -186,8 +189,12 @@ window.__ModuleLoader__.load({
|
|
|
186
189
|
* Mixed markdown + Vue GenUI card body for one assistant text block.
|
|
187
190
|
*/
|
|
188
191
|
/** Render markdown segments and schemaJson fences as GenUI cards. */
|
|
189
|
-
const GenuiTextBody = (0, react.memo)(function GenuiTextBody({ text, streaming, labels, mentions, customActions }) {
|
|
190
|
-
const segments = (0, react.useMemo)(() => splitAssistantText(text, streaming), [
|
|
192
|
+
const GenuiTextBody = (0, react.memo)(function GenuiTextBody({ text, streaming, interrupted, labels, mentions, customActions }) {
|
|
193
|
+
const segments = (0, react.useMemo)(() => splitAssistantText(text, streaming, interrupted), [
|
|
194
|
+
text,
|
|
195
|
+
streaming,
|
|
196
|
+
interrupted
|
|
197
|
+
]);
|
|
191
198
|
const nodes = [];
|
|
192
199
|
for (let i = 0; i < segments.length; i++) {
|
|
193
200
|
const segment = segments[i];
|
|
@@ -284,6 +291,7 @@ window.__ModuleLoader__.load({
|
|
|
284
291
|
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GenuiTextBody, {
|
|
285
292
|
text: block.text,
|
|
286
293
|
streaming,
|
|
294
|
+
interrupted,
|
|
287
295
|
labels,
|
|
288
296
|
mentions,
|
|
289
297
|
customActions
|
|
@@ -338,6 +346,149 @@ window.__ModuleLoader__.load({
|
|
|
338
346
|
});
|
|
339
347
|
});
|
|
340
348
|
//#endregion
|
|
349
|
+
//#region src/prompt-control-url.ts
|
|
350
|
+
/** Same-origin host route used by the composer toggle button. */
|
|
351
|
+
const GENUI_PROMPT_CONTROL_URL = "/api/dsh-genui/prompt";
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region \0dsh-css:module:src/client/prompt-toggle-button.module.css.mjs
|
|
354
|
+
const css = ".aG4Rta_toggle{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:14px;justify-content:center;align-items:center;display:inline-flex}.aG4Rta_toggle:hover:not(:disabled),.aG4Rta_toggle:focus-visible{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);outline:none}.aG4Rta_toggle[aria-disabled=true]{cursor:not-allowed;opacity:.4}.aG4Rta_toggle[data-active]{background:var(--dsw-alias-button-ghost-active-fill);color:var(--dsw-alias-label-primary);box-shadow:inset 0 0 0 1px var(--dsw-alias-button-ghost-active-border)}.aG4Rta_tooltip{z-index:1000;background:var(--dsw-alias-tooltip-bg);width:max-content;max-width:50vw;color:var(--dsw-static-neutral-bluish-00);white-space:pre-line;overflow-wrap:break-word;pointer-events:none;border-radius:8px;padding:3px 7px;font-size:13px;line-height:20px;position:fixed;transform:translate(-50%,-100%)}";
|
|
355
|
+
const tagId = "dsh-genui/src/client/prompt-toggle-button.module.css";
|
|
356
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
357
|
+
const tag = document.createElement("style");
|
|
358
|
+
tag.dataset.plugin = "dsh-genui";
|
|
359
|
+
tag.dataset.pluginCss = tagId;
|
|
360
|
+
tag.textContent = css;
|
|
361
|
+
document.head.appendChild(tag);
|
|
362
|
+
}
|
|
363
|
+
var prompt_toggle_button_module_css_default = {
|
|
364
|
+
"toggle": "aG4Rta_toggle",
|
|
365
|
+
"tooltip": "aG4Rta_tooltip"
|
|
366
|
+
};
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/client/prompt-toggle-button.tsx
|
|
369
|
+
/**
|
|
370
|
+
* Turn the GenUI authoring prompt on or off from the composer tool row.
|
|
371
|
+
* Rendering stays installed either way; only the prompt section changes.
|
|
372
|
+
*/
|
|
373
|
+
function PromptToggleButton(_props) {
|
|
374
|
+
const [enabled, setEnabled] = (0, react.useState)(false);
|
|
375
|
+
const [available, setAvailable] = (0, react.useState)(false);
|
|
376
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
377
|
+
const [tooltipPoint, setTooltipPoint] = (0, react.useState)(null);
|
|
378
|
+
const buttonRef = (0, react.useRef)(null);
|
|
379
|
+
const tooltipTimer = (0, react.useRef)(null);
|
|
380
|
+
(0, react.useEffect)(() => {
|
|
381
|
+
let active = true;
|
|
382
|
+
const controller = new AbortController();
|
|
383
|
+
fetch(GENUI_PROMPT_CONTROL_URL, { signal: controller.signal }).then(async (response) => {
|
|
384
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
385
|
+
const state = await response.json();
|
|
386
|
+
if (active) {
|
|
387
|
+
setEnabled(state.enabled === true);
|
|
388
|
+
setAvailable(true);
|
|
389
|
+
}
|
|
390
|
+
}).catch(() => {
|
|
391
|
+
if (active) setAvailable(false);
|
|
392
|
+
});
|
|
393
|
+
return () => {
|
|
394
|
+
active = false;
|
|
395
|
+
controller.abort();
|
|
396
|
+
};
|
|
397
|
+
}, []);
|
|
398
|
+
const toggle = (0, react.useCallback)(async () => {
|
|
399
|
+
if (busy) return;
|
|
400
|
+
const next = !enabled;
|
|
401
|
+
setBusy(true);
|
|
402
|
+
setEnabled(next);
|
|
403
|
+
try {
|
|
404
|
+
const response = await fetch(GENUI_PROMPT_CONTROL_URL, {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: { "content-type": "application/json" },
|
|
407
|
+
body: JSON.stringify({ enabled: next })
|
|
408
|
+
});
|
|
409
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
410
|
+
const state = await response.json();
|
|
411
|
+
setEnabled(state.enabled === true);
|
|
412
|
+
setAvailable(true);
|
|
413
|
+
} catch {
|
|
414
|
+
setEnabled(!next);
|
|
415
|
+
setAvailable(false);
|
|
416
|
+
} finally {
|
|
417
|
+
setBusy(false);
|
|
418
|
+
}
|
|
419
|
+
}, [busy, enabled]);
|
|
420
|
+
const showTooltip = (0, react.useCallback)(() => {
|
|
421
|
+
if (tooltipTimer.current !== null) return;
|
|
422
|
+
tooltipTimer.current = setTimeout(() => {
|
|
423
|
+
tooltipTimer.current = null;
|
|
424
|
+
const button = buttonRef.current;
|
|
425
|
+
if (button === null) return;
|
|
426
|
+
const rect = button.getBoundingClientRect();
|
|
427
|
+
setTooltipPoint({
|
|
428
|
+
left: rect.left + rect.width / 2,
|
|
429
|
+
top: rect.top - 8
|
|
430
|
+
});
|
|
431
|
+
}, 120);
|
|
432
|
+
}, []);
|
|
433
|
+
const hideTooltip = (0, react.useCallback)(() => {
|
|
434
|
+
if (tooltipTimer.current !== null) {
|
|
435
|
+
clearTimeout(tooltipTimer.current);
|
|
436
|
+
tooltipTimer.current = null;
|
|
437
|
+
}
|
|
438
|
+
setTooltipPoint(null);
|
|
439
|
+
}, []);
|
|
440
|
+
const tooltipOpen = tooltipPoint !== null;
|
|
441
|
+
(0, react.useLayoutEffect)(() => {
|
|
442
|
+
if (!tooltipOpen) return;
|
|
443
|
+
const update = () => {
|
|
444
|
+
const button = buttonRef.current;
|
|
445
|
+
if (button === null) return;
|
|
446
|
+
const rect = button.getBoundingClientRect();
|
|
447
|
+
setTooltipPoint({
|
|
448
|
+
left: rect.left + rect.width / 2,
|
|
449
|
+
top: rect.top - 8
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
update();
|
|
453
|
+
window.addEventListener("resize", update);
|
|
454
|
+
window.addEventListener("scroll", update, true);
|
|
455
|
+
return () => {
|
|
456
|
+
window.removeEventListener("resize", update);
|
|
457
|
+
window.removeEventListener("scroll", update, true);
|
|
458
|
+
};
|
|
459
|
+
}, [tooltipOpen]);
|
|
460
|
+
(0, react.useEffect)(() => hideTooltip, [hideTooltip]);
|
|
461
|
+
const label = available ? enabled ? "GenUI prompt on" : "GenUI prompt off" : "GenUI prompt unavailable";
|
|
462
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
463
|
+
ref: buttonRef,
|
|
464
|
+
type: "button",
|
|
465
|
+
className: prompt_toggle_button_module_css_default.toggle,
|
|
466
|
+
"aria-label": "GenUI prompt",
|
|
467
|
+
"aria-pressed": enabled,
|
|
468
|
+
"aria-disabled": !available,
|
|
469
|
+
"data-active": enabled || void 0,
|
|
470
|
+
onClick: () => {
|
|
471
|
+
if (available && !busy) toggle();
|
|
472
|
+
},
|
|
473
|
+
onMouseDown: (event) => {
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
},
|
|
476
|
+
onMouseEnter: showTooltip,
|
|
477
|
+
onMouseLeave: hideTooltip,
|
|
478
|
+
onFocus: showTooltip,
|
|
479
|
+
onBlur: hideTooltip,
|
|
480
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
|
|
481
|
+
}), tooltipPoint !== null && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
482
|
+
className: prompt_toggle_button_module_css_default.tooltip,
|
|
483
|
+
role: "tooltip",
|
|
484
|
+
style: {
|
|
485
|
+
left: tooltipPoint.left,
|
|
486
|
+
top: tooltipPoint.top
|
|
487
|
+
},
|
|
488
|
+
children: label
|
|
489
|
+
}), document.body)] });
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
341
492
|
//#region src/client/index.ts
|
|
342
493
|
const inject = ["slots"];
|
|
343
494
|
/**
|
|
@@ -346,6 +497,11 @@ window.__ModuleLoader__.load({
|
|
|
346
497
|
*/
|
|
347
498
|
function apply(ctx) {
|
|
348
499
|
prefetchGenuiRuntime();
|
|
500
|
+
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
501
|
+
name: "conversation.input.left",
|
|
502
|
+
id: "genui-prompt-toggle",
|
|
503
|
+
order: 0
|
|
504
|
+
}, PromptToggleButton));
|
|
349
505
|
ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
|
|
350
506
|
name: "conversation.chat.node",
|
|
351
507
|
key: "assistant-step",
|