one-design-next 0.0.114 → 0.0.115

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.
@@ -122,6 +122,19 @@ export interface ComposerEditorRef {
122
122
  * 配合浮层"用户选了某个 skill"的回调使用。
123
123
  */
124
124
  replaceTriggerWithChip: (data: ChipData) => void;
125
+ /**
126
+ * 在内容末尾插入 raw value + chips(`ComposerRef.insertContent` 的编辑器实现)。
127
+ *
128
+ * 与粘贴 / 草稿还原共用 `insertSerializedAtRange`:一次 mutation 只抛一次
129
+ * onChange,派生 hint / 高度 / trigger 也只重算一次。
130
+ *
131
+ * - `focus: false`:不聚焦、不滚动,也不主动改写 selection(后台预填草稿)
132
+ */
133
+ insertSerializedAtEnd: (value: string, chips: ChipData[], options?: {
134
+ focus?: boolean;
135
+ }) => void;
136
+ /** 当前 raw value(含 marker,DOM 实时值;不依赖 React state 是否已 flush) */
137
+ getValue: () => string;
125
138
  /** 主动取消当前触发态(不修改 DOM,只清状态 + 抛 onTriggerChange(null))。 */
126
139
  cancelTrigger: () => void;
127
140
  /** 取最新的 trigger rect,便于浮层在滚动 / resize 时刷新位置。 */
@@ -23,13 +23,13 @@ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
23
23
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
24
24
  import { createPortal } from 'react-dom';
25
25
  import ScrollArea from "../scroll-area";
26
- import { caretBeforeHintIn, caretRangeFromPoint, findChipHostBeforeCaret, getCurrentLine, isCaretBeforeHint, isCaretOnHintLine, isPointOnHint, isPositionAfterHint, mergeLineIntoPreviousAtCaretStart, normalizeCollapsedCaretAtChipBoundary, normalizeCollapsedCaretBeforeHint, resolveCaretMove, splitLineAt, wouldArrowCrossHint } from "./caret";
26
+ import { caretBeforeHintIn, caretRangeFromPoint, findChipHostBeforeCaret, getCaretBeforeHint, getCurrentLine, isCaretBeforeHint, isCaretOnHintLine, isPointOnHint, isPositionAfterHint, mergeLineIntoPreviousAtCaretStart, normalizeCollapsedCaretAtChipBoundary, normalizeCollapsedCaretBeforeHint, resolveCaretMove, splitLineAt, wouldArrowCrossHint } from "./caret";
27
27
  import { buildClipboardPayload, COMPOSER_CLIPBOARD_MIME, parseClipboardPayload, plainTextFromClipboardPayload, remapClipboardPayload } from "./clipboard";
28
28
  import { useChipManager } from "./hooks/useChipManager";
29
29
  import { useChipSelectionMarker } from "./hooks/useChipSelectionMarker";
30
30
  import { chipHasEditableParams, ComposerInlineRef } from "./inline-ref";
31
31
  import { resolveHintTarget } from "./segments";
32
- import { COMPOSER_HINT_ATTR, createLineRoot, createLineRootWithText, ensureLineRoots, getPlainText, isEditorEmpty, LINE_ATTR_NAME, normalizeEditorDom, probeTrigger, stripInvisibleChars, ZWSP } from "./utils";
32
+ import { COMPOSER_HINT_ATTR, createLineRoot, createLineRootWithText, ensureCaretAnchorAfter, ensureLineRoots, getPlainText, isEditorEmpty, isLineRoot, LINE_ATTR_NAME, normalizeEditorDom, probeTrigger, stripInvisibleChars, ZWSP } from "./utils";
33
33
 
34
34
  /** Composer editor 触发态对外信号——告诉父组件该弹浮层选 skill 了。 */
35
35
 
@@ -82,6 +82,109 @@ var scrollCaretIntoView = function scrollCaretIntoView(viewport) {
82
82
  viewport.scrollTop -= viewportRect.top - rect.top + 4;
83
83
  }
84
84
  };
85
+
86
+ /** insertContent 的目标行:最后一个行容器;没有行模型时退回 editor 本身。 */
87
+ var resolveEndLine = function resolveEndLine(editor) {
88
+ var last = editor.lastElementChild;
89
+ return isLineRoot(last) ? last : editor;
90
+ };
91
+
92
+ /** 行内直接子节点的派生 hint(不是正文,插入必须落在它之前)。 */
93
+ var findDirectHint = function findDirectHint(line) {
94
+ for (var _i = 0, _Array$from = Array.from(line.children); _i < _Array$from.length; _i++) {
95
+ var child = _Array$from[_i];
96
+ if (child.hasAttribute(COMPOSER_HINT_ATTR)) return child;
97
+ }
98
+ return null;
99
+ };
100
+
101
+ /**
102
+ * insertContent 的插入点:最后一个行容器的内容末尾。
103
+ *
104
+ * 不能直接 `selectNodeContents(editor).collapse(false)`:
105
+ * - 行尾的派生 hint 不是内容:落在 hint 之前,避免内容插到灰字后面
106
+ * - 行尾的占位 `<br>` 不是换行:插到它前面。插到后面会让它从「占位」变成
107
+ * 真实换行,`getPlainText` 就会凭空多输出一个 '\n'
108
+ */
109
+ var resolveEndRange = function resolveEndRange(editor) {
110
+ var line = resolveEndLine(editor);
111
+ var range = document.createRange();
112
+ var hint = findDirectHint(line);
113
+ if (hint) {
114
+ var before = getCaretBeforeHint(hint);
115
+ if (before) {
116
+ range.setStart(before.node, before.offset);
117
+ range.collapse(true);
118
+ return range;
119
+ }
120
+ }
121
+ var offset = line.childNodes.length;
122
+ while (offset > 0) {
123
+ var node = line.childNodes[offset - 1];
124
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
125
+ offset -= 1;
126
+ continue;
127
+ }
128
+ break;
129
+ }
130
+ range.setStart(line, offset);
131
+ range.collapse(true);
132
+ return range;
133
+ };
134
+
135
+ /**
136
+ * insertContent 之后把 caret 落到新内容末尾。
137
+ *
138
+ * 行尾是 chip 时补一颗不进 value 的 ZWSP 锚点(与 insertChip 一致):
139
+ * 只靠元素偏移落位时,部分浏览器会把 caret 带到结构块之后。
140
+ * 行尾有派生 hint 时仍落在 hint 之前(hint 之后不是合法插入点)。
141
+ */
142
+ var placeCaretAtEnd = function placeCaretAtEnd(editor) {
143
+ var sel = window.getSelection();
144
+ if (!sel) return;
145
+ var line = resolveEndLine(editor);
146
+ var target;
147
+ var hint = findDirectHint(line);
148
+ var beforeHint = hint ? getCaretBeforeHint(hint) : null;
149
+ if (beforeHint) {
150
+ target = beforeHint;
151
+ } else {
152
+ var offset = line.childNodes.length;
153
+ while (offset > 0) {
154
+ var node = line.childNodes[offset - 1];
155
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
156
+ offset -= 1;
157
+ continue;
158
+ }
159
+ break;
160
+ }
161
+ var tail = offset > 0 ? line.childNodes[offset - 1] : null;
162
+ if ((tail === null || tail === void 0 ? void 0 : tail.nodeType) === Node.TEXT_NODE) {
163
+ var _tail$textContent;
164
+ target = {
165
+ node: tail,
166
+ offset: ((_tail$textContent = tail.textContent) !== null && _tail$textContent !== void 0 ? _tail$textContent : '').length
167
+ };
168
+ } else if (tail instanceof HTMLElement && tail.hasAttribute('data-odn-composer-chip-host')) {
169
+ var _anchor$textContent;
170
+ var _anchor = ensureCaretAnchorAfter(tail);
171
+ target = {
172
+ node: _anchor,
173
+ offset: ((_anchor$textContent = _anchor.textContent) !== null && _anchor$textContent !== void 0 ? _anchor$textContent : '').length
174
+ };
175
+ } else {
176
+ target = {
177
+ node: line,
178
+ offset: offset
179
+ };
180
+ }
181
+ }
182
+ var range = document.createRange();
183
+ range.setStart(target.node, target.offset);
184
+ range.collapse(true);
185
+ sel.removeAllRanges();
186
+ sel.addRange(range);
187
+ };
85
188
  var LINE_HEIGHT = 24;
86
189
  /** 与 SCSS `padding: 2px … 0` 对齐。10 行 = 10 * 24 + 2 = 242px。 */
87
190
  var EDITOR_PADDING_BLOCK = 2;
@@ -536,6 +639,33 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
536
639
  onTriggerChange === null || onTriggerChange === void 0 || onTriggerChange(null);
537
640
  _insertChip(data, el);
538
641
  },
642
+ insertSerializedAtEnd: function insertSerializedAtEnd(value, nextChips, options) {
643
+ var el = editorRef.current;
644
+ if (!el || !value) return;
645
+ ensureLineRoots(el);
646
+ var range = resolveEndRange(el);
647
+ var shouldFocus = (options === null || options === void 0 ? void 0 : options.focus) !== false;
648
+ /* 拿不到 Selection 时仍要把内容插进去,只是不主动落 caret。 */
649
+ var sel = shouldFocus ? window.getSelection() : null;
650
+ if (sel) {
651
+ sel.removeAllRanges();
652
+ sel.addRange(range);
653
+ }
654
+ insertSerializedAtRange(value, nextChips, range, el, {
655
+ focus: shouldFocus,
656
+ caret: !!sel
657
+ });
658
+ if (!shouldFocus) return;
659
+ placeCaretAtEnd(el);
660
+ if (sel && sel.rangeCount > 0) {
661
+ lastEditorRangeRef.current = sel.getRangeAt(0).cloneRange();
662
+ }
663
+ requestScrollCaretIntoView();
664
+ },
665
+ getValue: function getValue() {
666
+ var el = editorRef.current;
667
+ return el ? getPlainText(el) : '';
668
+ },
539
669
  cancelTrigger: function cancelTrigger() {
540
670
  if (!triggerRef.current) return;
541
671
  triggerRef.current = null;
@@ -558,7 +688,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
558
688
  return editorRef.current;
559
689
  }
560
690
  };
561
- }, [onChange, _insertChip, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
691
+ }, [onChange, _insertChip, insertSerializedAtRange, requestScrollCaretIntoView, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
562
692
 
563
693
  /* ─────────────────────────────────────
564
694
  * 监听 selection 变化:方向键 / 鼠标点击移位都要重算 trigger
@@ -35,9 +35,13 @@ export interface UseChipManagerReturn {
35
35
  * `focus: false` 时插入后不把焦点交给编辑器。
36
36
  * 首屏还原 defaultValue / defaultChips 必须传 false:默认 focus() 会把
37
37
  * 页面滚动到编辑器(浏览器「聚焦元素进入视口」),导致进页面就跳位置。
38
+ *
39
+ * `caret: false` 时不主动改写 selection(后台预填用,避免抢走用户
40
+ * 在别处的光标)。单行路径由调用方传自己的 range,不受影响。
38
41
  */
39
42
  options?: {
40
43
  focus?: boolean;
44
+ caret?: boolean;
41
45
  }) => void;
42
46
  /** 移除已从 DOM 摘下的 chip 状态(cut / 选区删除后)。 */
43
47
  syncChipsAfterDomMutation: (editor: HTMLElement) => void;
@@ -16,7 +16,7 @@ import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
16
16
  */
17
17
 
18
18
  import { useCallback, useRef, useState } from 'react';
19
- import { createLineRoot, ensureLineRoots, forEachMarkerSegment, getCurrentLineRoot, LINE_ATTR_NAME, stripInvisibleChars, ZWSP } from "../utils";
19
+ import { createLineRoot, ensureCaretAnchorAfter, ensureLineRoots, forEachMarkerSegment, getCurrentLineRoot, LINE_ATTR_NAME, stripInvisibleChars } from "../utils";
20
20
 
21
21
  /** chip 在 React 状态中的形态:data + 对应的 DOM host。 */
22
22
 
@@ -27,24 +27,6 @@ function createChipHost(data) {
27
27
  host.setAttribute('data-odn-composer-chip-host', '');
28
28
  return host;
29
29
  }
30
-
31
- /**
32
- * 在 host 后面垫一个「不进 value」的 ZWSP 文本锚点并返回它。
33
- *
34
- * chip 后面没有正文时(行尾 / 后面只剩派生 hint / 后面紧跟着另一个 chip),
35
- * caret 若只靠「元素偏移」落位,部分浏览器会把它带到结构块之后;垫一个只含
36
- * ZWSP 的文本节点,caret 始终有真实文本落点,而 `getPlainText` 会把整段剔掉,
37
- * 因此不会进 value / segments / 剪贴板。已有锚点直接复用,避免越积越多。
38
- */
39
- function ensureCaretAnchorAfter(host) {
40
- var _tail$textContent, _tail$textContent2;
41
- var parent = host.parentNode;
42
- var tail = host.nextSibling;
43
- var reusable = (tail === null || tail === void 0 ? void 0 : tail.nodeType) === Node.TEXT_NODE && ((_tail$textContent = tail.textContent) !== null && _tail$textContent !== void 0 ? _tail$textContent : '') !== '' && stripInvisibleChars((_tail$textContent2 = tail.textContent) !== null && _tail$textContent2 !== void 0 ? _tail$textContent2 : '') === '' ? tail : null;
44
- var anchor = reusable !== null && reusable !== void 0 ? reusable : document.createTextNode(ZWSP);
45
- if (!reusable) parent.insertBefore(anchor, tail);
46
- return anchor;
47
- }
48
30
  function createCaretAnchorAtBoundary(parent, offset) {
49
31
  var _parent$childNodes, _parent$childNodes$sa;
50
32
  var len = parent.childNodes.length;
@@ -255,7 +237,7 @@ onAfterMutate) {
255
237
  }
256
238
 
257
239
  // caret 落「拼回点」——粘贴段末尾、原 caret 后内容的起点。
258
- var sel = window.getSelection();
240
+ var sel = (options === null || options === void 0 ? void 0 : options.caret) === false ? null : window.getSelection();
259
241
  if (sel && lastLine) {
260
242
  var r = document.createRange();
261
243
  r.setStart(lastLine, caretBoundaryChildCount);
@@ -271,7 +253,7 @@ onAfterMutate) {
271
253
  _line.appendChild(lineFrags[_i]);
272
254
  editor.appendChild(_line);
273
255
  }
274
- var _sel = window.getSelection();
256
+ var _sel = (options === null || options === void 0 ? void 0 : options.caret) === false ? null : window.getSelection();
275
257
  var _lastLine = editor.querySelector(":scope > [".concat(LINE_ATTR_NAME, "]:last-of-type"));
276
258
  if (_sel && _lastLine) {
277
259
  var _r = document.createRange();
@@ -2,8 +2,10 @@ import React from 'react';
2
2
  import type { Attachment, ChipData, ComposerSegment, SendMeta, SkillItem } from '../_genui-types';
3
3
  import type { IconName } from '../icon';
4
4
  import { type SkillSlotProps } from '../skill-slot';
5
+ import { type ComposerInsertOptions, type ComposerInsertPart, type ComposerInsertResult } from './insert';
5
6
  import type { ComposerParamPanelContext } from './param-panel';
6
7
  import './style';
8
+ export type { ComposerInsertOptions, ComposerInsertPart, ComposerInsertResult, } from './insert';
7
9
  export type { ComposerParamPanelContext } from './param-panel';
8
10
  export type ComposerTool = 'attachment' | 'webSearch' | 'skill';
9
11
  export type ComposerVariant = 'full' | 'lite';
@@ -48,6 +50,38 @@ export interface ComposerRef {
48
50
  * 返回插入的 chip 实例 id,可用于后续 `updateChipData` 更新状态。
49
51
  */
50
52
  insertSkill: (skill: SkillItem) => string;
53
+ /**
54
+ * 命令式:插入一段内容,支持文本 / skill / 自定义 chip 混合,且只触发一次 onChange。
55
+ *
56
+ * 插入位置由 `options.position` 显式声明(当前仅支持 `'end'`):忽略 caret,
57
+ * 始终落在已有内容末尾——适合 Welcome Card / 建议项 / 外部面板「把东西塞进
58
+ * 输入框」的场景。与 `insertSkill`(依赖当前 caret)互补。
59
+ *
60
+ * `parts` 也接受单个 part 或一个字符串(等价于单个 text part)。
61
+ *
62
+ * 约定:
63
+ * - **不自动补空格**:文本与 chip 的间隔由业务在 text part 里显式写。
64
+ * - `skill` 走与菜单插入相同的 chip 结构(含 `hint` / `initialState`);
65
+ * `initialState: 'pending'` 且提供 `renderParamPanel` 时自动打开参数面板
66
+ * (`focus: false` 的后台预填不打开)。
67
+ * - `chip` 是逃生舱:mention / 自定义 kind / 已存草稿都可以直接传。
68
+ * - chip id 与编辑器已有 chip 冲突时自动换新 id,以返回的 `chipIds` 为准。
69
+ * - 受控 `value` 场景请在 onChange 内回写,与手动输入一致(否则一帧后会被
70
+ * controlled sync 拉回旧值)。
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * composerRef.current?.insertContent(
75
+ * [
76
+ * { type: 'text', text: '请基于这份数据' },
77
+ * { type: 'skill', skill: reportSkill },
78
+ * { type: 'text', text: '生成复盘报告' },
79
+ * ],
80
+ * { position: 'end' },
81
+ * );
82
+ * ```
83
+ */
84
+ insertContent: (parts: ComposerInsertPart | ComposerInsertPart[] | string, options: ComposerInsertOptions) => ComposerInsertResult;
51
85
  /**
52
86
  * 更新编辑器内指定 chip 的数据(partial merge)。
53
87
  * 典型用法:`updateChipData(chipId, { state: 'pending' })` 将 chip 置为待补充态。
@@ -1,9 +1,9 @@
1
1
  import _extends from "@babel/runtime/helpers/esm/extends";
2
2
  import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
3
- import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
4
3
  import _createForOfIteratorHelper from "@babel/runtime/helpers/esm/createForOfIteratorHelper";
5
4
  import _regeneratorRuntime from "@babel/runtime/helpers/esm/regeneratorRuntime";
6
5
  import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";
6
+ import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
7
7
  import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
8
8
  import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
9
9
  import { SKILL_CATEGORY_ICON } from "../_genui-inline-ref";
@@ -18,23 +18,10 @@ import SkillSlot from "../skill-slot";
18
18
  import Tooltip from "../tooltip";
19
19
  import { COMPOSER_EDITOR_MAX_ROWS_PX, ComposerEditor } from "./editor";
20
20
  import { chipHasEditableParams, isMentionChip } from "./inline-ref";
21
+ import { buildInsertPayload, createSkillChipData } from "./insert";
21
22
  import { rawValueToSegments, segmentsHasContent } from "./segments";
22
23
  import "./style";
23
24
  import { COMPOSER_SKILL_MENU_DENSITY_CAP_PX, filterSkillsByQuery, resolveComposerSkillMenuMaxHeight, resolveComposerSkillMenuWidth, shouldOpenSlashTriggerMenu } from "./utils";
24
- function createInvocationChipData(skill) {
25
- return {
26
- id: "skill-".concat(skill.id, "-").concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 6)),
27
- label: skill.label,
28
- icon: skill.icon,
29
- kind: 'invocation',
30
- state: skill.initialState,
31
- refId: skill.id,
32
- payload: {},
33
- /* 提示文案随 chip 快照下来(而不是渲染时回查技能目录):
34
- * 草稿用 defaultValue + defaultChips 还原时,目录可能已经变了。 */
35
- hint: skill.hint
36
- };
37
- }
38
25
  var TOOL_BTN_ACTIVE = 'color-mix(in srgb, var(--odn-color-brand-6) 10%, transparent)';
39
26
  var DEFAULT_ACCEPT = 'image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md,.csv,.json,.xml,.html,.css,.js,.ts,.jsx,.tsx,.py,.java,.go,.rs,.c,.cpp,.zip,.rar,.7z';
40
27
  function formatSizeMb(bytes) {
@@ -322,12 +309,49 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
322
309
  },
323
310
  insertSkill: function insertSkill(skill) {
324
311
  var _editorRef$current8, _editorRef$current9;
325
- var data = createInvocationChipData(skill);
312
+ var data = createSkillChipData(skill);
326
313
  (_editorRef$current8 = editorRef.current) === null || _editorRef$current8 === void 0 || _editorRef$current8.insertChip(data);
327
314
  (_editorRef$current9 = editorRef.current) === null || _editorRef$current9 === void 0 || _editorRef$current9.focus();
328
315
  if (skill.initialState === 'pending') openParamPanel(data.id);
329
316
  return data.id;
330
317
  },
318
+ insertContent: function insertContent(parts, options) {
319
+ var editor = editorRef.current;
320
+ if (!editor) return {
321
+ chipIds: [],
322
+ value: ''
323
+ };
324
+
325
+ /* 类型层 position 目前只开 'end';这里兜住 JS / any 调用方,
326
+ * 避免未来扩 'caret' 前被静默当成末尾处理。 */
327
+ var position = options === null || options === void 0 ? void 0 : options.position;
328
+ if (process.env.NODE_ENV !== 'production' && position && position !== 'end') {
329
+ // eslint-disable-next-line no-console
330
+ console.warn("[Composer] insertContent \u5F53\u524D\u4EC5\u652F\u6301 position: \"end\"\uFF0C\u6536\u5230\u300C".concat(position, "\u300D\uFF0C\u5DF2\u6309\u672B\u5C3E\u63D2\u5165\u5904\u7406\u3002"));
331
+ }
332
+ var payload = buildInsertPayload(parts, editor.getChips());
333
+ if (!payload) {
334
+ return {
335
+ chipIds: [],
336
+ value: editor.getValue()
337
+ };
338
+ }
339
+ editor.insertSerializedAtEnd(payload.value, payload.chips, options);
340
+
341
+ /* 与 insertSkill 对齐:pending skill 插入后自动打开参数面板。
342
+ * 一次 insertContent 可能带多个 pending,只开最后一个(用户最后看到的那块)。
343
+ * `focus: false` 是后台预填语义,不主动弹面板。 */
344
+ var pendingChip = _toConsumableArray(payload.chips).reverse().find(function (chip) {
345
+ return chip.state === 'pending' && !isMentionChip(chip);
346
+ });
347
+ if (pendingChip && (options === null || options === void 0 ? void 0 : options.focus) !== false) {
348
+ openParamPanel(pendingChip.id);
349
+ }
350
+ return {
351
+ chipIds: payload.chipIds,
352
+ value: editor.getValue()
353
+ };
354
+ },
331
355
  updateChipData: function updateChipData(id, patch) {
332
356
  var _editorRef$current10;
333
357
  return (_editorRef$current10 = editorRef.current) === null || _editorRef$current10 === void 0 ? void 0 : _editorRef$current10.updateChipData(id, patch);
@@ -644,7 +668,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
644
668
  * source = 'trigger' → editor.replaceTriggerWithChip 替换 `/` + query 整段
645
669
  * 任一入口选完后都关闭对应 menu。chip data 用 skill.id + 一次性实例 id。 */
646
670
  var handleSelectSkill = useCallback(function (skill, source) {
647
- var data = createInvocationChipData(skill);
671
+ var data = createSkillChipData(skill);
648
672
  if (source === 'trigger') {
649
673
  var _editorRef$current16;
650
674
  (_editorRef$current16 = editorRef.current) === null || _editorRef$current16 === void 0 || _editorRef$current16.replaceTriggerWithChip(data);
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Composer · 命令式插入内容(insertContent)
3
+ *
4
+ * 业务场景:Welcome Card / 建议项 / 外部面板希望在「已有输入内容的末尾」一次性
5
+ * 塞入文本、skill chip 等,而不是依赖当前 caret(用户可能停在任意位置)。
6
+ *
7
+ * 职责边界:
8
+ * - 本模块只把 parts 编成 `{ value, chips }`(含 marker 的 raw value + 新 chip
9
+ * 快照),真正的 DOM 插入由 editor 的 `insertSerializedAtEnd` →
10
+ * `insertSerializedAtRange` 完成——与粘贴 / 草稿还原共用同一条链路,
11
+ * 因此 onChange / 派生 hint / 高度 / trigger 只会重算一次。
12
+ * - **不自动补空格**:文本与 chip 之间的间隔由业务自己在 text part 里写,
13
+ * 避免 value / segments 里出现用户没输入过的字符(与插入 skill 的既有约定一致)。
14
+ */
15
+ import type { ChipData, SkillItem } from '../_genui-types';
16
+ /**
17
+ * insertContent 支持的内容片段:
18
+ * - `text`:普通文本(支持 `\n`;`\r\n` 会归一成 `\n`)
19
+ * - `skill`:按技能目录的条目生成一个 invocation chip(含 hint / initialState)
20
+ * - `chip`:逃生舱,业务自带完整 chip 数据(mention / 自定义 kind / 已存草稿)
21
+ */
22
+ export type ComposerInsertPart = {
23
+ type: 'text';
24
+ text: string;
25
+ } | {
26
+ type: 'skill';
27
+ skill: SkillItem;
28
+ } | {
29
+ type: 'chip';
30
+ chip: ChipData;
31
+ };
32
+ export interface ComposerInsertOptions {
33
+ /**
34
+ * 插入位置。
35
+ *
36
+ * 当前仅支持 `'end'`(已有内容末尾);`'caret'` 预留在同一入口,
37
+ * 未来扩展时**不会改变现有调用**,`'end'` 的语义也保持不变。
38
+ */
39
+ position: 'end';
40
+ /**
41
+ * 插入后是否聚焦编辑器,并把 caret 落到新内容末尾(同时滚到可见区)。
42
+ * 传 `false` 时只改内容,不动焦点、不滚动(如后台预填草稿)。
43
+ * @default true
44
+ */
45
+ focus?: boolean;
46
+ }
47
+ export interface ComposerInsertResult {
48
+ /** 实际插入的 chip id,顺序与 parts 中 chip 出现顺序一致;id 冲突时已换新 */
49
+ chipIds: string[];
50
+ /** 插入后的 raw value(含 marker),等价于本次 onChange 第一参 */
51
+ value: string;
52
+ }
53
+ /** buildInsertPayload 的产物:可直接交给 editor 插入。 */
54
+ export interface ComposerInsertPayload {
55
+ /** 待插入的 raw value(唯一数据源:marker 与 text 交替) */
56
+ value: string;
57
+ /** 待插入 chip 的完整数据(只含本次新增,不含编辑器里已有的) */
58
+ chips: ChipData[];
59
+ /** 本次实际使用的 chip id(与 chips 一一对应) */
60
+ chipIds: string[];
61
+ }
62
+ /**
63
+ * 按 SkillItem 生成 chip 实例数据。
64
+ *
65
+ * hint 随 chip 快照下来(而不是渲染时回查技能目录):草稿用
66
+ * `defaultValue + defaultChips` 还原时,目录可能已经变了。
67
+ */
68
+ export declare function createSkillChipData(skill: SkillItem): ChipData;
69
+ /**
70
+ * 把 parts 编成一次插入所需的 raw value + chips。
71
+ *
72
+ * 返回 `null` 表示没有可插入内容(空数组 / 全空文本),调用方应直接 no-op,
73
+ * 不要触发 onChange。
74
+ */
75
+ export declare function buildInsertPayload(parts: ComposerInsertPart | ComposerInsertPart[] | string, existingChips?: readonly ChipData[]): ComposerInsertPayload | null;
@@ -0,0 +1,130 @@
1
+ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
+ import _typeof from "@babel/runtime/helpers/esm/typeof";
3
+ import _createForOfIteratorHelper from "@babel/runtime/helpers/esm/createForOfIteratorHelper";
4
+ /**
5
+ * Composer · 命令式插入内容(insertContent)
6
+ *
7
+ * 业务场景:Welcome Card / 建议项 / 外部面板希望在「已有输入内容的末尾」一次性
8
+ * 塞入文本、skill chip 等,而不是依赖当前 caret(用户可能停在任意位置)。
9
+ *
10
+ * 职责边界:
11
+ * - 本模块只把 parts 编成 `{ value, chips }`(含 marker 的 raw value + 新 chip
12
+ * 快照),真正的 DOM 插入由 editor 的 `insertSerializedAtEnd` →
13
+ * `insertSerializedAtRange` 完成——与粘贴 / 草稿还原共用同一条链路,
14
+ * 因此 onChange / 派生 hint / 高度 / trigger 只会重算一次。
15
+ * - **不自动补空格**:文本与 chip 之间的间隔由业务自己在 text part 里写,
16
+ * 避免 value / segments 里出现用户没输入过的字符(与插入 skill 的既有约定一致)。
17
+ */
18
+
19
+ import { createMarker, stripInvisibleChars } from "./utils";
20
+
21
+ /**
22
+ * insertContent 支持的内容片段:
23
+ * - `text`:普通文本(支持 `\n`;`\r\n` 会归一成 `\n`)
24
+ * - `skill`:按技能目录的条目生成一个 invocation chip(含 hint / initialState)
25
+ * - `chip`:逃生舱,业务自带完整 chip 数据(mention / 自定义 kind / 已存草稿)
26
+ */
27
+
28
+ /** buildInsertPayload 的产物:可直接交给 editor 插入。 */
29
+
30
+ var randomSuffix = function randomSuffix() {
31
+ return "".concat(Date.now().toString(36), "-").concat(Math.random().toString(36).slice(2, 6));
32
+ };
33
+
34
+ /**
35
+ * 按 SkillItem 生成 chip 实例数据。
36
+ *
37
+ * hint 随 chip 快照下来(而不是渲染时回查技能目录):草稿用
38
+ * `defaultValue + defaultChips` 还原时,目录可能已经变了。
39
+ */
40
+ export function createSkillChipData(skill) {
41
+ return {
42
+ id: "skill-".concat(skill.id, "-").concat(randomSuffix()),
43
+ label: skill.label,
44
+ icon: skill.icon,
45
+ kind: 'invocation',
46
+ state: skill.initialState,
47
+ refId: skill.id,
48
+ payload: {},
49
+ hint: skill.hint
50
+ };
51
+ }
52
+
53
+ /**
54
+ * 文本预处理:
55
+ * - `\r\n` / `\r` → `\n`,交给行模型拆行
56
+ * - 剔除零宽 marker 字符,避免业务文案里混入 `\u200b\u2063` 后被解析成 chip
57
+ * - 不 trim:首尾空格是业务显式传入的内容,库不替它做决定
58
+ */
59
+ function normalizeText(text) {
60
+ return stripInvisibleChars(text.replace(/\r\n?/g, '\n'));
61
+ }
62
+
63
+ /**
64
+ * chip id 必须唯一:marker、Portal key、removeChip / updateChipData 都靠它定位。
65
+ * 冲突时换一个新 id,并在开发期提示——返回值里的 `chipIds` 才是最终生效的 id。
66
+ */
67
+ function resolveUniqueChipId(chip, usedIds) {
68
+ if (chip.id && !usedIds.has(chip.id)) return chip.id;
69
+ var nextId = "".concat(chip.id || 'chip', "-").concat(randomSuffix());
70
+ while (usedIds.has(nextId)) nextId = "chip-".concat(randomSuffix());
71
+ if (process.env.NODE_ENV !== 'production') {
72
+ // eslint-disable-next-line no-console
73
+ console.warn("[Composer] insertContent \u6536\u5230\u91CD\u590D\u6216\u4E3A\u7A7A\u7684 chip id\u300C".concat(chip.id, "\u300D\uFF0C") + "\u5DF2\u6539\u7528\u300C".concat(nextId, "\u300D\u3002\u8BF7\u4EE5\u8FD4\u56DE\u7684 chipIds \u4E3A\u51C6\u3002"));
74
+ }
75
+ return nextId;
76
+ }
77
+
78
+ /**
79
+ * 把 parts 编成一次插入所需的 raw value + chips。
80
+ *
81
+ * 返回 `null` 表示没有可插入内容(空数组 / 全空文本),调用方应直接 no-op,
82
+ * 不要触发 onChange。
83
+ */
84
+ export function buildInsertPayload(parts) {
85
+ var existingChips = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
86
+ var list = typeof parts === 'string' ? [{
87
+ type: 'text',
88
+ text: parts
89
+ }] : Array.isArray(parts) ? parts : [parts];
90
+ var usedIds = new Set(existingChips.map(function (chip) {
91
+ return chip.id;
92
+ }));
93
+ var chips = [];
94
+ var chipIds = [];
95
+ var value = '';
96
+ var _iterator = _createForOfIteratorHelper(list),
97
+ _step;
98
+ try {
99
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
100
+ var part = _step.value;
101
+ if (!part || _typeof(part) !== 'object') continue;
102
+ if (part.type === 'text') {
103
+ if (typeof part.text === 'string') value += normalizeText(part.text);
104
+ continue;
105
+ }
106
+ var chip = null;
107
+ if (part.type === 'skill') {
108
+ chip = part.skill ? createSkillChipData(part.skill) : null;
109
+ } else if (part.chip) {
110
+ chip = _objectSpread({}, part.chip);
111
+ }
112
+ if (!chip) continue;
113
+ chip.id = resolveUniqueChipId(chip, usedIds);
114
+ usedIds.add(chip.id);
115
+ chips.push(chip);
116
+ chipIds.push(chip.id);
117
+ value += createMarker(chip.id);
118
+ }
119
+ } catch (err) {
120
+ _iterator.e(err);
121
+ } finally {
122
+ _iterator.f();
123
+ }
124
+ if (!value) return null;
125
+ return {
126
+ value: value,
127
+ chips: chips,
128
+ chipIds: chipIds
129
+ };
130
+ }
@@ -113,6 +113,15 @@ export declare function getPlainText(editor: HTMLElement, chipIdAttr?: string):
113
113
  * 空格 / 全角空格也算"已输入",只要文本非空就该收起 placeholder。
114
114
  */
115
115
  export declare function isEditorEmpty(editor: HTMLElement, chipIdAttr?: string): boolean;
116
+ /**
117
+ * 在 host 后面垫一个「不进 value」的 ZWSP 文本锚点并返回它。
118
+ *
119
+ * chip 后面没有正文时(行尾 / 后面只剩派生 hint / 后面紧跟着另一个 chip),
120
+ * caret 若只靠「元素偏移」落位,部分浏览器会把它带到结构块之后;垫一个只含
121
+ * ZWSP 的文本节点,caret 始终有真实文本落点,而 `getPlainText` 会把整段剔掉,
122
+ * 因此不进 value / segments / 剪贴板。已有锚点直接复用,避免越积越多。
123
+ */
124
+ export declare function ensureCaretAnchorAfter(host: Node): Text;
116
125
  /**
117
126
  * 清理 contenteditable 删字后的幽灵节点:空 text、仅 chip 时的占位 <br>。
118
127
  * 避免「文字 + invocation」删光文字后出现多余换行,以及 caret 卡在 chip 后误删 invocation。
@@ -368,6 +368,24 @@ export function isEditorEmpty(editor) {
368
368
  * chip 前的 leading <br> / 空 text node。
369
369
  * ──────────────────────────────────────────────────────────────────── */
370
370
 
371
+ /**
372
+ * 在 host 后面垫一个「不进 value」的 ZWSP 文本锚点并返回它。
373
+ *
374
+ * chip 后面没有正文时(行尾 / 后面只剩派生 hint / 后面紧跟着另一个 chip),
375
+ * caret 若只靠「元素偏移」落位,部分浏览器会把它带到结构块之后;垫一个只含
376
+ * ZWSP 的文本节点,caret 始终有真实文本落点,而 `getPlainText` 会把整段剔掉,
377
+ * 因此不进 value / segments / 剪贴板。已有锚点直接复用,避免越积越多。
378
+ */
379
+ export function ensureCaretAnchorAfter(host) {
380
+ var _tail$textContent, _tail$textContent2;
381
+ var parent = host.parentNode;
382
+ var tail = host.nextSibling;
383
+ var reusable = (tail === null || tail === void 0 ? void 0 : tail.nodeType) === Node.TEXT_NODE && ((_tail$textContent = tail.textContent) !== null && _tail$textContent !== void 0 ? _tail$textContent : '') !== '' && stripInvisibleChars((_tail$textContent2 = tail.textContent) !== null && _tail$textContent2 !== void 0 ? _tail$textContent2 : '') === '' ? tail : null;
384
+ var anchor = reusable !== null && reusable !== void 0 ? reusable : document.createTextNode(ZWSP);
385
+ if (!reusable) parent.insertBefore(anchor, tail);
386
+ return anchor;
387
+ }
388
+
371
389
  /**
372
390
  * 清理 contenteditable 删字后的幽灵节点:空 text、仅 chip 时的占位 <br>。
373
391
  * 避免「文字 + invocation」删光文字后出现多余换行,以及 caret 卡在 chip 后误删 invocation。
package/dist/index.d.ts CHANGED
@@ -63,6 +63,7 @@ export { CHAT_LIST_PINNED_ID, CHAT_LIST_UNGROUPED_ID, default as ChatList, match
63
63
  export { CLARIFY_DOCK_MAX_OPTIONS, CLARIFY_DOCK_SKIPPED_LABEL, CLARIFY_DOCK_SKIPPED_VALUE, ClarifyDockPanel, ClarifyDockTrigger, clarifyDockLayoutId, parseClarifyBoundDate, toISODate, useClarifyDockState, type ClarifyDockAnswerItem, type ClarifyDockHostMessage, type ClarifyDockMessageState, type ClarifyDockPanelProps, type ClarifyDockQuestion, type ClarifyDockResolvePayload, type ClarifyDockSpec, type ClarifyDockState, type ClarifyDockTriggerProps, type ClarifyInput, type ClarifyOption, } from './clarify-dock';
64
64
  export { default as CodeBlock, type CodeBlockProps } from './code-block';
65
65
  export { default as Composer, type ComposerProps, type ComposerRef, type ComposerSkillSlotProps, type ComposerTool, type ComposerToolConfig, } from './composer';
66
+ export type { ComposerInsertOptions, ComposerInsertPart, ComposerInsertResult, } from './composer/insert';
66
67
  export { type ComposerParamPanelContext } from './composer/param-panel';
67
68
  export { default as ErrorBlock, type ErrorBlockProps } from './error-block';
68
69
  export { default as Fab, type FabProps } from './fab';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.114",
3
+ "version": "0.0.115",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "license": "MIT",
6
6
  "type": "module",