@deepseek-ai/dsh-client-ui-message-feedback 0.1.0-rc.7 → 0.1.0-rc.8

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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-message-feedback/README.md
5
- README.md: 461e87589567eb95b075839d5893cc551e67a035
6
- README.zh.md: 31f722021ff65f476a6ba1ab0211fd4e091671d2
5
+ README.md: d3bb4e28b95da2fde0d26b7ef83ebca377b4939f
6
+ README.zh.md: d823bb9de2b7d39d0bc5d00164b8ce8dffec4a34
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
- Per-message feedback plugin, browser half: a Like/Dislike pair plus an optional note, contributed as the `feedback` entry (order 10) of the `conversation.chat.assistant-actions` strip. The strip is declared by `ui-conversation` and rendered inside the finalized assistant message's IconActions row, between copy and branch, so the controls inherit that row's chrome and hover behavior. Only finalized messages reach the slot — an interruption-frozen partial carries no `messageId` and therefore no feedback controls. The strip renders once per turn, on the closing assistant message that owns the turn's IconActions row: earlier steps of a multi-step turn produce tool rows rather than a rateable body, so they present no controls even though the Host would accept them as targets.
5
+ Per-message feedback plugin, browser half: a Like/Dislike pair plus an optional note, contributed as the `feedback` entry (order 10) of the `conversation.chat.assistant-actions` strip. The strip is declared by `ui-conversation` and rendered inside the finalized assistant message's IconActions row, between copy and branch, so the controls inherit that row's chrome and hover behavior. The note editor itself does not sit in that row: it is a `role="dialog"` popover portaled to `document.body` and anchored under its trigger, so the row keeps its single line whether the editor is open or closed and the panel is not clipped by the conversation column. A rating or list-load failure shows inline in the row; a note-save failure shows inside the popover, which stays open so the draft can be corrected. Only finalized messages reach the slot — an interruption-frozen partial carries no `messageId` and therefore no feedback controls. The strip renders once per turn, on the closing assistant message that owns the turn's IconActions row: earlier steps of a multi-step turn produce tool rows rather than a rateable body, so they present no controls even though the Host would accept them as targets.
6
6
 
7
7
  One `MessageFeedbackController` per Session backs every message control in that Session, so a single `messageFeedback.list` read seeds the whole transcript. The read is deferred to the first hover or focus rather than fired on mount, because the controls mount once per settled message in the visible history.
8
8
 
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- 单条消息反馈插件的浏览器侧:一对 Like/Dislike 按钮加一个可选备注,作为 `conversation.chat.assistant-actions` 条带的 `feedback` 条目(order 10)贡献。该条带由 `ui-conversation` 声明,渲染在已定稿助手消息的 IconActions 行内、复制与分支之间,因此控件沿用该行的样式与 hover 行为。只有已定稿的消息能到达这个 slot——被中断冻结的部分输出不带 `messageId`,因此也没有反馈控件。该操作栏每个 Turn 渲染一次,位于持有该 Turn IconActions 行的收尾助手消息上:多步骤 Turn 中较早的步骤产出的是工具行而非可评分正文,因此即使 Host 会接受它们作为目标,界面上也不出现控件。
5
+ 单条消息反馈插件的浏览器侧:一对 Like/Dislike 按钮加一个可选备注,作为 `conversation.chat.assistant-actions` 条带的 `feedback` 条目(order 10)贡献。该条带由 `ui-conversation` 声明,渲染在已定稿助手消息的 IconActions 行内、复制与分支之间,因此控件沿用该行的样式与 hover 行为。备注编辑器本身不在这一行里:它是一个 `role="dialog"` 的浮层,portal 到 `document.body` 并锚定在其触发按钮下方,因此无论编辑器是否打开该行都保持单行,面板也不会被会话列裁掉。评分或列表加载失败在行内展示;备注保存失败在浮层内展示,且面板保持打开以便修正草稿。只有已定稿的消息能到达这个 slot——被中断冻结的部分输出不带 `messageId`,因此也没有反馈控件。该操作栏每个 Turn 渲染一次,位于持有该 Turn IconActions 行的收尾助手消息上:多步骤 Turn 中较早的步骤产出的是工具行而非可评分正文,因此即使 Host 会接受它们作为目标,界面上也不出现控件。
6
6
 
7
7
  每个 Session 一个 `MessageFeedbackController`,支撑该 Session 内所有消息的控件,因此一次 `messageFeedback.list` 读取即可填充整段对话。该读取延迟到首次 hover 或 focus 才发起,而不是在挂载时触发,因为可见历史中每条已结束的消息都会挂载一次控件。
8
8
 
package/lib/client.js CHANGED
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
+ let react_dom = require("react-dom");
9
10
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
11
  //#region lib/types/client/controller.js
11
12
  const INITIAL_VIEW = Object.freeze({
@@ -325,7 +326,7 @@ window.__ModuleLoader__.load({
325
326
  };
326
327
  //#endregion
327
328
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css.mjs
328
- const css = "._8_XoUG_action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}._8_XoUG_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_action:disabled{cursor:default;opacity:.4}._8_XoUG_action[data-active]{color:var(--dsw-alias-label-primary)}._8_XoUG_noteOpen{max-width:220px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;background:0 0;border:none;border-radius:14px;padding:0 8px;font-size:13px;line-height:28px;overflow:hidden}._8_XoUG_noteOpen:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_noteEditor{align-items:flex-start;gap:6px;display:inline-flex}._8_XoUG_noteInput{border:1px solid var(--dsw-alias-border-secondary);background:var(--dsw-alias-bg-primary);width:260px;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;border-radius:8px;padding:6px 8px;font-size:13px}._8_XoUG_noteSave,._8_XoUG_noteCancel{cursor:pointer;border:none;border-radius:14px;height:28px;padding:0 10px;font-size:13px}._8_XoUG_noteSave{background:var(--dsw-alias-interactive-bg-primary);color:var(--dsw-alias-label-inverse)}._8_XoUG_noteSave:disabled{cursor:default;opacity:.4}._8_XoUG_noteCancel{color:var(--dsw-alias-label-tertiary);background:0 0}._8_XoUG_noteCancel:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_failure{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:28px}";
329
+ const css = "._8_XoUG_action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}._8_XoUG_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_action:disabled{cursor:default;opacity:.4}._8_XoUG_action[data-active]{color:var(--dsw-alias-label-primary)}._8_XoUG_noteOpen{max-width:220px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;cursor:pointer;background:0 0;border:none;border-radius:14px;padding:0 8px;font-size:13px;line-height:28px;overflow:hidden}._8_XoUG_noteOpen:hover,._8_XoUG_noteOpen[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_notePanel{z-index:1100;box-sizing:border-box;border:1px solid var(--dsw-alias-border-inverted);background:var(--dsw-specific-menu);width:320px;max-width:min(360px,100vw - 24px);max-height:calc(100vh - 24px);box-shadow:var(--dsw-shadow-lv3);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;gap:8px;padding:8px;display:flex;position:fixed;overflow-y:auto}._8_XoUG_noteInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;border-radius:8px;padding:6px 8px;font-size:13px}._8_XoUG_noteActions{justify-content:flex-end;gap:6px;display:flex}._8_XoUG_noteSave,._8_XoUG_noteCancel{cursor:pointer;border:none;border-radius:14px;height:28px;padding:0 10px;font-size:13px}._8_XoUG_noteSave{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}._8_XoUG_noteSave:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}._8_XoUG_noteSave:disabled{cursor:default;opacity:.4}._8_XoUG_noteCancel{color:var(--dsw-alias-label-tertiary);background:0 0}._8_XoUG_noteCancel:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_failure{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:20px}";
329
330
  const tagId = "@deepseek-ai/dsh-client-ui-message-feedback/MessageFeedbackActions.module.css";
330
331
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
331
332
  const tag = document.createElement("style");
@@ -335,27 +336,49 @@ window.__ModuleLoader__.load({
335
336
  document.head.appendChild(tag);
336
337
  }
337
338
  var MessageFeedbackActions_module_css_default = {
339
+ "action": "_8_XoUG_action",
340
+ "failure": "_8_XoUG_failure",
341
+ "noteActions": "_8_XoUG_noteActions",
338
342
  "noteCancel": "_8_XoUG_noteCancel",
343
+ "noteInput": "_8_XoUG_noteInput",
339
344
  "noteOpen": "_8_XoUG_noteOpen",
340
- "failure": "_8_XoUG_failure",
341
- "noteSave": "_8_XoUG_noteSave",
342
- "noteEditor": "_8_XoUG_noteEditor",
343
- "action": "_8_XoUG_action",
344
- "noteInput": "_8_XoUG_noteInput"
345
+ "notePanel": "_8_XoUG_notePanel",
346
+ "noteSave": "_8_XoUG_noteSave"
345
347
  };
346
348
  //#endregion
347
349
  //#region lib/types/client/MessageFeedbackActions.js
348
350
  /**
349
351
  * Per-message feedback controls: a Like/Dislike pair plus an optional note.
350
- * Rendered inside the assistant message's IconActions row, so the buttons
351
- * reuse that row's chrome and sit between copy and branch.
352
+ * The buttons render inside the assistant message's IconActions row, so they
353
+ * reuse that row's chrome and sit between copy and branch. The note editor is
354
+ * a popover (portaled to `document.body`) anchored to the note trigger, not an
355
+ * inline expansion: a 260px textarea plus buttons cannot fit the row at any
356
+ * viewport, and an inline element pushed the branch action and clock out of the
357
+ * conversation column. Portaling out of the column also escapes its `overflow`
358
+ * clip, so the panel cannot be cropped or detached from the message it annotates.
352
359
  * @module @deepseek-ai/dsh-client-ui-message-feedback/client/MessageFeedbackActions
353
360
  */
361
+ /** Safe distance kept between the panel and the viewport edges (the Menu portal margin). */
362
+ const PANEL_MARGIN = 12;
363
+ /** Distance between the trigger's bottom edge and the panel's top. */
364
+ const PANEL_GAP = 4;
365
+ /**
366
+ * Unplaced portal panel: hidden but laid out so `offsetWidth` is real for the
367
+ * clamp. The explicit insets match `Menu`'s measure style — a `position: fixed`
368
+ * element with auto insets otherwise sits at its static position, a different
369
+ * origin than the one the first placement measures from.
370
+ */
371
+ const MEASURE_STYLE = {
372
+ visibility: "hidden",
373
+ left: 0,
374
+ top: 0
375
+ };
354
376
  /**
355
377
  * One message's feedback controls.
356
378
  * @param props - the owner's message identity, the injected verbs, and the
357
379
  * shared feedback hook.
358
- * @returns the rating buttons, plus the note editor while it is open.
380
+ * @returns the rating buttons and the note trigger, with the note editor
381
+ * portal-open beneath the trigger while it is open.
359
382
  */
360
383
  function MessageFeedbackActions({ messageId, ensure, rate, toggle, clearNote, useFeedback, t }) {
361
384
  const item = useFeedback((view) => view.items.get(messageId));
@@ -364,7 +387,11 @@ window.__ModuleLoader__.load({
364
387
  const [noteOpen, setNoteOpen] = (0, react.useState)(false);
365
388
  const [draft, setDraft] = (0, react.useState)("");
366
389
  const [pending, setPending] = (0, react.useState)(false);
367
- const [failure, setFailure] = (0, react.useState)(null);
390
+ const [rowFailure, setRowFailure] = (0, react.useState)(null);
391
+ const [noteFailure, setNoteFailure] = (0, react.useState)(null);
392
+ const triggerRef = (0, react.useRef)(null);
393
+ const panelRef = (0, react.useRef)(null);
394
+ const inputRef = (0, react.useRef)(null);
368
395
  const seeded = (0, react.useRef)(false);
369
396
  const seed = (0, react.useCallback)(() => {
370
397
  if (seeded.current) return;
@@ -375,44 +402,113 @@ window.__ModuleLoader__.load({
375
402
  (0, react.useEffect)(() => () => {
376
403
  alive.current = false;
377
404
  }, []);
378
- const settle = (0, react.useCallback)((result) => {
405
+ /** Bumped whenever an editing session ends, so a late save can tell it is stale. */
406
+ const noteGeneration = (0, react.useRef)(0);
407
+ /** Current panel open-state, readable from a stale closure via a ref. */
408
+ const noteOpenRef = (0, react.useRef)(false);
409
+ (0, react.useEffect)(() => {
410
+ noteOpenRef.current = noteOpen;
411
+ }, [noteOpen]);
412
+ const errorCopy = (0, react.useCallback)((result) => {
413
+ return result.error?.code === "version-conflict" ? t("error.conflict") : t("error.generic");
414
+ }, [t]);
415
+ const settleRating = (0, react.useCallback)((result) => {
379
416
  if (!alive.current) return;
380
417
  setPending(false);
381
- if (result.ok) {
382
- setFailure(null);
383
- return;
384
- }
385
- setFailure(result.error?.code === "version-conflict" ? t("error.conflict") : t("error.generic"));
386
- }, [t]);
418
+ setRowFailure(result.ok ? null : errorCopy(result));
419
+ }, [errorCopy]);
420
+ const closeNote = (0, react.useCallback)(() => {
421
+ noteGeneration.current += 1;
422
+ setNoteOpen(false);
423
+ }, []);
387
424
  const onRate = (0, react.useCallback)((next) => {
388
425
  setPending(true);
389
- setFailure(null);
390
- setNoteOpen(false);
391
- toggle(messageId, next).then(settle);
426
+ setRowFailure(null);
427
+ closeNote();
428
+ toggle(messageId, next).then(settleRating);
392
429
  }, [
430
+ closeNote,
393
431
  messageId,
394
- settle,
432
+ settleRating,
395
433
  toggle
396
434
  ]);
397
435
  const onSaveNote = (0, react.useCallback)((current) => {
398
436
  const trimmed = draft.trim();
399
437
  setPending(true);
400
- setFailure(null);
438
+ setNoteFailure(null);
439
+ const generation = noteGeneration.current;
440
+ const staleSeed = item?.note ?? "";
401
441
  (trimmed.length === 0 ? clearNote(messageId) : rate(messageId, current, trimmed)).then((result) => {
402
- settle(result);
403
- if (result.ok && alive.current) setNoteOpen(false);
442
+ if (!alive.current) return;
443
+ setPending(false);
444
+ if (result.ok) {
445
+ if (generation === noteGeneration.current) {
446
+ setNoteFailure(null);
447
+ setNoteOpen(false);
448
+ return;
449
+ }
450
+ setDraft((draftNow) => draftNow === staleSeed ? trimmed : draftNow);
451
+ return;
452
+ }
453
+ if (generation === noteGeneration.current || !noteOpenRef.current) setNoteFailure(errorCopy(result));
404
454
  });
405
455
  }, [
406
456
  clearNote,
407
457
  draft,
458
+ errorCopy,
459
+ item?.note,
408
460
  messageId,
409
- rate,
410
- settle
461
+ noteOpenRef,
462
+ rate
411
463
  ]);
412
- const openNote = (0, react.useCallback)(() => {
464
+ const toggleNote = (0, react.useCallback)(() => {
465
+ if (noteOpen) {
466
+ closeNote();
467
+ return;
468
+ }
413
469
  setDraft(item?.note ?? "");
470
+ setNoteFailure(null);
414
471
  setNoteOpen(true);
415
- }, [item?.note]);
472
+ }, [
473
+ noteOpen,
474
+ closeNote,
475
+ item?.note
476
+ ]);
477
+ const pos = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
478
+ open: noteOpen,
479
+ anchorRef: triggerRef,
480
+ panelRef,
481
+ gap: PANEL_GAP,
482
+ margin: PANEL_MARGIN
483
+ });
484
+ (0, react.useEffect)(() => {
485
+ if (!noteOpen) return;
486
+ inputRef.current?.focus();
487
+ const onPointerDown = (e) => {
488
+ if (!(e.target instanceof Node)) return;
489
+ if (triggerRef.current?.contains(e.target) === true) return;
490
+ if (panelRef.current?.contains(e.target) === true) return;
491
+ closeNote();
492
+ };
493
+ const onKeyDown = (e) => {
494
+ if (e.key === "Escape") closeNote();
495
+ };
496
+ document.addEventListener("pointerdown", onPointerDown);
497
+ document.addEventListener("keydown", onKeyDown);
498
+ return () => {
499
+ document.removeEventListener("pointerdown", onPointerDown);
500
+ document.removeEventListener("keydown", onKeyDown);
501
+ };
502
+ }, [noteOpen, closeNote]);
503
+ const wasOpen = (0, react.useRef)(false);
504
+ (0, react.useEffect)(() => {
505
+ if (noteOpen) {
506
+ wasOpen.current = true;
507
+ return;
508
+ }
509
+ if (wasOpen.current) triggerRef.current?.focus();
510
+ wasOpen.current = false;
511
+ }, [noteOpen]);
416
512
  const likeLabel = rating === "positive" ? t("action.likeActive") : t("action.like");
417
513
  const dislikeLabel = rating === "negative" ? t("action.dislikeActive") : t("action.dislike");
418
514
  return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
@@ -452,54 +548,72 @@ window.__ModuleLoader__.load({
452
548
  children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDislikeOutline16, {})
453
549
  })
454
550
  }),
455
- rating !== void 0 && !noteOpen && (0, react_jsx_runtime.jsx)("button", {
551
+ rating !== void 0 && (0, react_jsx_runtime.jsx)("button", {
552
+ ref: triggerRef,
456
553
  type: "button",
457
554
  className: MessageFeedbackActions_module_css_default.noteOpen,
458
- onClick: openNote,
555
+ "aria-haspopup": "dialog",
556
+ "aria-expanded": noteOpen,
557
+ onClick: toggleNote,
459
558
  children: item?.note === void 0 ? t("note.open") : item.note
460
559
  }),
461
- rating !== void 0 && noteOpen && (0, react_jsx_runtime.jsxs)("span", {
462
- className: MessageFeedbackActions_module_css_default.noteEditor,
560
+ rowFailure === null && loadFailed && (0, react_jsx_runtime.jsx)("span", {
561
+ className: MessageFeedbackActions_module_css_default.failure,
562
+ role: "status",
563
+ children: t("error.load")
564
+ }),
565
+ rowFailure !== null && (0, react_jsx_runtime.jsx)("span", {
566
+ className: MessageFeedbackActions_module_css_default.failure,
567
+ role: "status",
568
+ children: rowFailure
569
+ }),
570
+ !(rating !== void 0 && noteOpen) && noteFailure !== null && (0, react_jsx_runtime.jsx)("span", {
571
+ className: MessageFeedbackActions_module_css_default.failure,
572
+ role: "status",
573
+ children: noteFailure
574
+ }),
575
+ rating !== void 0 && noteOpen && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)("div", {
576
+ ref: panelRef,
577
+ className: MessageFeedbackActions_module_css_default.notePanel,
578
+ role: "dialog",
579
+ "aria-label": t("note.dialog"),
580
+ style: pos ?? MEASURE_STYLE,
463
581
  children: [
464
582
  (0, react_jsx_runtime.jsx)("textarea", {
583
+ ref: inputRef,
465
584
  className: MessageFeedbackActions_module_css_default.noteInput,
466
585
  "aria-label": t("note.aria"),
467
586
  placeholder: t("note.placeholder"),
468
587
  value: draft,
469
- rows: 2,
588
+ rows: 3,
470
589
  onChange: (event) => {
471
590
  setDraft(event.target.value);
472
591
  }
473
592
  }),
474
- (0, react_jsx_runtime.jsx)("button", {
475
- type: "button",
476
- className: MessageFeedbackActions_module_css_default.noteSave,
477
- disabled: pending,
478
- onClick: () => {
479
- onSaveNote(rating);
480
- },
481
- children: t("note.save")
593
+ (0, react_jsx_runtime.jsxs)("div", {
594
+ className: MessageFeedbackActions_module_css_default.noteActions,
595
+ children: [(0, react_jsx_runtime.jsx)("button", {
596
+ type: "button",
597
+ className: MessageFeedbackActions_module_css_default.noteSave,
598
+ disabled: pending,
599
+ onClick: () => {
600
+ onSaveNote(rating);
601
+ },
602
+ children: t("note.save")
603
+ }), (0, react_jsx_runtime.jsx)("button", {
604
+ type: "button",
605
+ className: MessageFeedbackActions_module_css_default.noteCancel,
606
+ onClick: closeNote,
607
+ children: t("note.cancel")
608
+ })]
482
609
  }),
483
- (0, react_jsx_runtime.jsx)("button", {
484
- type: "button",
485
- className: MessageFeedbackActions_module_css_default.noteCancel,
486
- onClick: () => {
487
- setNoteOpen(false);
488
- },
489
- children: t("note.cancel")
610
+ noteFailure !== null && (0, react_jsx_runtime.jsx)("span", {
611
+ className: MessageFeedbackActions_module_css_default.failure,
612
+ role: "status",
613
+ children: noteFailure
490
614
  })
491
615
  ]
492
- }),
493
- failure === null && loadFailed && (0, react_jsx_runtime.jsx)("span", {
494
- className: MessageFeedbackActions_module_css_default.failure,
495
- role: "status",
496
- children: t("error.load")
497
- }),
498
- failure !== null && (0, react_jsx_runtime.jsx)("span", {
499
- className: MessageFeedbackActions_module_css_default.failure,
500
- role: "status",
501
- children: failure
502
- })
616
+ }), document.body)
503
617
  ] });
504
618
  }
505
619
  //#endregion
@@ -512,6 +626,7 @@ window.__ModuleLoader__.load({
512
626
  "action.dislike": "有问题的回答",
513
627
  "action.dislikeActive": "取消标记",
514
628
  "note.open": "补充说明",
629
+ "note.dialog": "反馈",
515
630
  "note.placeholder": "这条回答哪里好,或哪里有问题?(可选)",
516
631
  "note.save": "保存",
517
632
  "note.cancel": "取消",
@@ -527,6 +642,7 @@ window.__ModuleLoader__.load({
527
642
  "action.dislike": "Bad response",
528
643
  "action.dislikeActive": "Remove rating",
529
644
  "note.open": "Add a note",
645
+ "note.dialog": "Feedback",
530
646
  "note.placeholder": "What was good, or what went wrong? (optional)",
531
647
  "note.save": "Save",
532
648
  "note.cancel": "Cancel",
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Per-message feedback controls: a Like/Dislike pair plus an optional note.
3
- * Rendered inside the assistant message's IconActions row, so the buttons
4
- * reuse that row's chrome and sit between copy and branch.
3
+ * The buttons render inside the assistant message's IconActions row, so they
4
+ * reuse that row's chrome and sit between copy and branch. The note editor is
5
+ * a popover (portaled to `document.body`) anchored to the note trigger, not an
6
+ * inline expansion: a 260px textarea plus buttons cannot fit the row at any
7
+ * viewport, and an inline element pushed the branch action and clock out of the
8
+ * conversation column. Portaling out of the column also escapes its `overflow`
9
+ * clip, so the panel cannot be cropped or detached from the message it annotates.
5
10
  * @module @deepseek-ai/dsh-client-ui-message-feedback/client/MessageFeedbackActions
6
11
  */
7
12
  import type { MessageFeedbackActionProps } from './slots.ts';
@@ -9,7 +14,8 @@ import type { MessageFeedbackActionProps } from './slots.ts';
9
14
  * One message's feedback controls.
10
15
  * @param props - the owner's message identity, the injected verbs, and the
11
16
  * shared feedback hook.
12
- * @returns the rating buttons, plus the note editor while it is open.
17
+ * @returns the rating buttons and the note trigger, with the note editor
18
+ * portal-open beneath the trigger while it is open.
13
19
  */
14
20
  export declare function MessageFeedbackActions({ messageId, ensure, rate, toggle, clearNote, useFeedback, t }: MessageFeedbackActionProps): import("react").JSX.Element;
15
21
  //# sourceMappingURL=MessageFeedbackActions.d.ts.map
@@ -6,6 +6,7 @@ export declare const zh: {
6
6
  'action.dislike': string;
7
7
  'action.dislikeActive': string;
8
8
  'note.open': string;
9
+ 'note.dialog': string;
9
10
  'note.placeholder': string;
10
11
  'note.save': string;
11
12
  'note.cancel': string;
@@ -29,6 +30,7 @@ export declare const en: {
29
30
  'action.dislike': string;
30
31
  'action.dislikeActive': string;
31
32
  'note.open': string;
33
+ 'note.dialog': string;
32
34
  'note.placeholder': string;
33
35
  'note.save': string;
34
36
  'note.cancel': string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-message-feedback",
3
3
  "description": "Per-message feedback controls contributed to the assistant-message action strip, backed by the messageFeedback Host Remote",
4
- "version": "0.1.0-rc.7",
4
+ "version": "0.1.0-rc.8",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -42,35 +42,33 @@
42
42
  },
43
43
  "license": "MIT",
44
44
  "peerDependencies": {
45
- "react": "^18.2.0",
46
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
47
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
48
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
49
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
50
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
51
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
52
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
53
- "@deepseek-ai/dsh-message-feedback": "^0.1.0-rc.7",
54
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
45
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8",
46
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8",
47
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
48
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
49
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8",
50
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
51
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8",
55
52
  "@deepseek-ai/cordis": "^4.0.1",
56
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7"
53
+ "@deepseek-ai/dsh-message-feedback": "^0.1.0-rc.8"
57
54
  },
58
55
  "devDependencies": {
59
56
  "@testing-library/react": "^16.1.0",
60
57
  "@types/react": "~18.3.1",
58
+ "@types/react-dom": "~18.3.0",
61
59
  "react": "^18.2.0",
62
60
  "react-dom": "^18.2.0",
63
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
64
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
65
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
66
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.0-rc.7",
67
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
68
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
69
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
70
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
71
- "@deepseek-ai/dsh-message-feedback": "^0.1.0-rc.7",
72
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7",
73
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
61
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8",
62
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
63
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
64
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8",
65
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.0-rc.8",
66
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8",
67
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.8",
68
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
69
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
70
+ "@deepseek-ai/dsh-message-feedback": "^0.1.0-rc.8",
71
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8",
74
72
  "@deepseek-ai/cordis": "^4.0.1"
75
73
  },
76
74
  "files": [