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 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** — the plugin's prompt is injected when a session begins, so older sessions won't pick it up
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
- That's expected; it decides case by case. When you want one, just say "give me a form / chart / interface."
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
- 具体一点:插件给模型加了一段提示词,教它在需要的时候输出一段结构化的 JSON(写在 `schemaJson` 代码块里);DSH 网页端拿到这段 JSON 后,交给渲染器变成实际组件。整个过程是流式的,AI 写到哪、界面就渲染到哪,不用等它把话说完。
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 (streaming) {
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), [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",