one-design-next 0.0.121 → 0.0.123

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.
@@ -142,11 +142,25 @@ export interface ChipData {
142
142
  *
143
143
  * **不是正文内容**:既不进 `value`(无 marker),也不进剪贴板;只在
144
144
  * 「结构谓词成立」时作为派生的只读视觉渲染在 chip 之后——即该 chip 是
145
- * 最后一个内容、且其后**一个字符都没有**(空格 / 换行都算内容;详见
146
- * `resolveHintTarget`)。
145
+ * 最后一个内容、其后**一个字符都没有**(空格 / 换行都算内容),且它的
146
+ * hint 还没关闭(见 `hintClosed`;详见 `resolveHintTarget`)。
147
147
  * 发送时由 `rawValueToSegments` 按同一谓词追加为 `ComposerHintSegment`。
148
148
  */
149
149
  hint?: string;
150
+ /**
151
+ * 该 chip 的 hint 是否已经关闭(一次性,只关不开)。
152
+ *
153
+ * hint 的机会只有一次:只要这颗 chip **后面出现过东西**——插入时后面就有
154
+ * 文字、用户后面敲了字符 / 空格 / 换行,或者又插了一块 skill——就置为
155
+ * `true`。此后即使把后面的内容删回空串,灰字也不会再出现,发送时也不再补
156
+ * `hint` 段。
157
+ *
158
+ * 由编辑器在内容变化时写入(`resolveHintClosedChipIds`),业务不需要读写;
159
+ * 它随 `chips` 快照透传,业务若把 chips 一起存进草稿,恢复后 hint 也保持
160
+ * 关闭。skill 本身没有 `hint` 时不写这个字段——那是「没有这个能力」,不是
161
+ * 状态。
162
+ */
163
+ hintClosed?: boolean;
150
164
  }
151
165
  export interface SkillItem {
152
166
  id: string;
@@ -159,6 +173,8 @@ export interface SkillItem {
159
173
  * 选中插入后,只要该 skill chip 是输入框里最后一个内容且其后没有其它内容
160
174
  * (空格 / 换行都算内容),就会以只读灰字形式渲染在 chip 后面;用户在其后
161
175
  * 输入任何字符或换行即自动隐藏。与光标位置无关。
176
+ * 灰字是一次性的:一旦收起(或者插入时后面本来就有内容),再把后面的内容
177
+ * 删掉也不会重新出现。
162
178
  */
163
179
  hint?: string;
164
180
  /**
@@ -224,7 +240,8 @@ export interface ComposerMentionSegment {
224
240
  }
225
241
  /**
226
242
  * 派生的 skill 提示段:**不是用户输入的内容**,而是「最后一个 skill chip + 其后
227
- * 无内容」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板文案。
243
+ * 无内容 + hint 未关闭」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板
244
+ * 文案。
228
245
  *
229
246
  * 只在 `onSend` 的 segments 里出现;不出现在 `value` / `chips` / 剪贴板里。
230
247
  * 需要忽略它的业务可以直接 filter,或用 `isHintSegment` 收窄。
@@ -56,7 +56,8 @@ export var ComposerSegmentType = {
56
56
 
57
57
  /**
58
58
  * 派生的 skill 提示段:**不是用户输入的内容**,而是「最后一个 skill chip + 其后
59
- * 无内容」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板文案。
59
+ * 无内容 + hint 未关闭」这一结构谓词成立时,由 Composer 在发送瞬间追加的模板
60
+ * 文案。
60
61
  *
61
62
  * 只在 `onSend` 的 segments 里出现;不出现在 `value` / `chips` / 剪贴板里。
62
63
  * 需要忽略它的业务可以直接 filter,或用 `isHintSegment` 收窄。
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import React from 'react';
20
20
  import type { ChipData } from '../_genui-types';
21
+ import { type ComposerSkillHint } from './segments';
21
22
  /** Composer editor 触发态对外信号——告诉父组件该弹浮层选 skill 了。 */
22
23
  export interface ComposerEditorTriggerInfo {
23
24
  /** 触发字符(@ 或 /) */
@@ -72,6 +73,16 @@ export interface ComposerEditorProps {
72
73
  defaultValue?: string;
73
74
  /** 值变化回调;参数是 raw string,含 marker。 */
74
75
  onChange?: (value: string) => void;
76
+ /**
77
+ * 灰字(skill hint)命中状态变化回调:
78
+ * - 灰字出现(最后一个 chip 命中结构谓词)→ 传入命中的 chip / 文案
79
+ * - 灰字消失(后面有内容 / chip 被删 / hint 已关闭)→ 传入 null
80
+ *
81
+ * 只在状态**真的变化**时抛一次;与灰字渲染共用同一份判定(先落 DOM 再抛,
82
+ * 不会各自算一套)。判定成立但灰字被临时摘掉(IME 合成期)时不跟着抖。
83
+ * 挂载时会先同步一次当前状态(空内容即 null),业务侧不必自己猜初始值。
84
+ */
85
+ onSkillHintChange?: (hint: ComposerSkillHint | null) => void;
75
86
  /** 与 defaultValue / 首次受控 value 配套,把 marker 还原成 chip。 */
76
87
  defaultChips?: ChipData[];
77
88
  placeholder?: string;
@@ -29,7 +29,7 @@ import { buildClipboardPayload, COMPOSER_CLIPBOARD_MIME, parseClipboardPayload,
29
29
  import { useChipManager } from "./hooks/useChipManager";
30
30
  import { useChipSelectionMarker } from "./hooks/useChipSelectionMarker";
31
31
  import { chipHasEditableParams, ComposerInlineRef } from "./inline-ref";
32
- import { resolveHintTarget } from "./segments";
32
+ import { resolveHintClosedChipIds, resolveHintTarget } from "./segments";
33
33
  import { COMPOSER_HINT_ACCEPT_ATTR, COMPOSER_HINT_ACCEPT_SELECTOR, COMPOSER_HINT_ATTR, createLineRoot, createLineRootWithText, ensureCaretAnchorAfter, ensureLineRoots, getPlainText, isEditorEmpty, isLineRoot, LINE_ATTR_NAME, normalizeEditorDom, probeTrigger, stripInvisibleChars, ZWSP } from "./utils";
34
34
 
35
35
  /** Composer editor 触发态对外信号——告诉父组件该弹浮层选 skill 了。 */
@@ -326,6 +326,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
326
326
  defaultValue = _ref$defaultValue === void 0 ? '' : _ref$defaultValue,
327
327
  defaultChips = _ref.defaultChips,
328
328
  onChange = _ref.onChange,
329
+ onSkillHintChange = _ref.onSkillHintChange,
329
330
  placeholder = _ref.placeholder,
330
331
  autoSize = _ref.autoSize,
331
332
  onPressEnter = _ref.onPressEnter,
@@ -353,6 +354,9 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
353
354
  var isComposingRef = useRef(false);
354
355
  var onTriggerChangeRef = useRef(onTriggerChange);
355
356
  onTriggerChangeRef.current = onTriggerChange;
357
+ /** 灰字状态回调走 ref:调用方常传内联函数,不能让 fireChange / 挂载同步因此反复重建 */
358
+ var onSkillHintChangeRef = useRef(onSkillHintChange);
359
+ onSkillHintChangeRef.current = onSkillHintChange;
356
360
  var isControlled = valueProp !== undefined;
357
361
  var minRows = (_autoSize$minRows = autoSize === null || autoSize === void 0 ? void 0 : autoSize.minRows) !== null && _autoSize$minRows !== void 0 ? _autoSize$minRows : 1;
358
362
  var maxRows = autoSize === null || autoSize === void 0 ? void 0 : autoSize.maxRows;
@@ -490,6 +494,9 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
490
494
  var getChipsRef = useRef(function () {
491
495
  return [];
492
496
  });
497
+ /* hint 关闭器:与 getChipsRef 同一套「ref 打破声明顺序」做法——fireChange 声明
498
+ * 在 useChipManager 之前,而关闭方法由后者提供。 */
499
+ var closeChipHintsRef = useRef(function () {});
493
500
 
494
501
  /* ---- 派生 hint:imperative DOM 同步 ----
495
502
  * hint 必须是 chip host 的**兄弟节点**,不能塞在 host 内部:host 是
@@ -584,6 +591,33 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
584
591
  }
585
592
  }, [removeHintNode]);
586
593
 
594
+ /* 灰字状态的**上一帧快照**:回调只在真的变化时抛一次。
595
+ * fireChange 每次输入都会跑,不去重的话业务侧会被同一条状态反复冲刷。
596
+ * `undefined` = 还没抛过(与「抛过 null」区分),保证挂载时一定会同步一次。 */
597
+ var lastSkillHintRef = useRef(undefined);
598
+
599
+ /**
600
+ * 把「灰字当前状态」通知给业务方(与 syncHintNode 同一份 target,保证同源)。
601
+ *
602
+ * 只在 chipId / refId / 文案任一变化时抛:同一条状态在连续输入里会算很多次。
603
+ */
604
+ var notifySkillHint = useCallback(function (target, chips) {
605
+ var _chips$find, _onSkillHintChangeRef;
606
+ var next = target ? {
607
+ chipId: target.chipId,
608
+ text: target.text,
609
+ refId: (_chips$find = chips.find(function (chip) {
610
+ return chip.id === target.chipId;
611
+ })) === null || _chips$find === void 0 ? void 0 : _chips$find.refId
612
+ } : null;
613
+ var prev = lastSkillHintRef.current;
614
+ if (prev !== undefined && (prev === null || prev === void 0 ? void 0 : prev.chipId) === (next === null || next === void 0 ? void 0 : next.chipId) && (prev === null || prev === void 0 ? void 0 : prev.text) === (next === null || next === void 0 ? void 0 : next.text) && (prev === null || prev === void 0 ? void 0 : prev.refId) === (next === null || next === void 0 ? void 0 : next.refId)) {
615
+ return;
616
+ }
617
+ lastSkillHintRef.current = next;
618
+ (_onSkillHintChangeRef = onSkillHintChangeRef.current) === null || _onSkillHintChangeRef === void 0 || _onSkillHintChangeRef.call(onSkillHintChangeRef, next);
619
+ }, []);
620
+
587
621
  /** 把 caret 直接落到 hint 之前(= chip 右侧);没有 hint 时不动。 */
588
622
  var placeCaretBeforeHint = useCallback(function () {
589
623
  var el = editorRef.current;
@@ -615,11 +649,21 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
615
649
  normalizeEditorDom(el);
616
650
  var chips = getChipsRef.current();
617
651
  var raw = getPlainText(el);
618
- /* 派生 hint 与 value 同源同刻重算:结构谓词只依赖 raw + chips(换行也算
652
+ /* hint 是一次性的:先把「后面出现过东西」的 chip 永久关闭,再算谓词。
653
+ * 顺序不能反——反了的话,删到空的那一帧会先渲染出灰字再被移除(闪一下)。
654
+ * 关闭会改写 chips 快照,所以谓词必须用关闭后的快照重新取。
655
+ *
656
+ * 派生 hint 与 value 同源同刻重算:结构谓词只依赖 raw + chips(换行也算
619
657
  * 内容,所以按 Enter 后 hint 自动消失)。必须先落 DOM 再算 empty / 高度——
620
658
  * 否则「chip 被删」时残留的 hint 文本会让 placeholder 判定与 lite 升格
621
659
  * 判定晚一拍。 */
622
- syncHintNode(resolveHintTarget(raw, chips));
660
+ closeChipHintsRef.current(resolveHintClosedChipIds(raw, chips));
661
+ /* 关闭会改写 chips 快照,谓词必须用关闭后的快照重新取;
662
+ * 渲染(syncHintNode)与对外回调(notifySkillHint)共用这一份 target,
663
+ * 业务拿到的是「这一刻灰字该不该在」的同一份判定,不会各算一套。 */
664
+ var hintChips = getChipsRef.current();
665
+ var hintTarget = resolveHintTarget(raw, hintChips);
666
+ syncHintNode(hintTarget);
623
667
  var showPlaceholder = shouldShowPlaceholder(el);
624
668
  if (forceNonEmptyOnceRef.current && showPlaceholder) {
625
669
  setEmpty(false);
@@ -632,7 +676,23 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
632
676
  scheduleControlledSync();
633
677
  // editor.height 锁定时 RO 不触发,主动同步一次,确保升格判定能拿到 scrollHeight
634
678
  syncHeight();
635
- }, [onChange, pullCaretBeforeHint, scheduleControlledSync, shouldShowPlaceholder, syncHeight, syncHintNode]);
679
+ /* 对外回调放在所有内部状态更新之后:业务在里面同步 setState 甚至
680
+ * updateChipData 都不会插进「DOM 已落、caret / 高度还没收尾」的中间态。 */
681
+ notifySkillHint(hintTarget, hintChips);
682
+ }, [notifySkillHint, onChange, pullCaretBeforeHint, scheduleControlledSync, shouldShowPlaceholder, syncHeight, syncHintNode]);
683
+
684
+ /* 挂载时同步一次灰字状态。
685
+ *
686
+ * 带 chips 的草稿恢复已经在挂载的 layout effect 里走 fireChange 抛过一次,
687
+ * 这里兜的是「空内容 / 只有纯文本挂载」——否则业务侧上一轮留下的状态没人复位。
688
+ * 放在 passive effect:晚于 chips 还原,且 notifySkillHint 自带去重,
689
+ * 与 fireChange 那一次重复时不会再抛。 */
690
+ useEffect(function () {
691
+ var el = editorRef.current;
692
+ if (!el) return;
693
+ var hintChips = getChipsRef.current();
694
+ notifySkillHint(resolveHintTarget(getPlainText(el), hintChips), hintChips);
695
+ }, [notifySkillHint]);
636
696
 
637
697
  /* ─────────────────────────────────────
638
698
  * chip 管理
@@ -648,10 +708,12 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
648
708
  syncChipsAfterDomMutation = _useChipManager.syncChipsAfterDomMutation,
649
709
  removeChip = _useChipManager.removeChip,
650
710
  updateChipData = _useChipManager.updateChipData,
711
+ closeChipHints = _useChipManager.closeChipHints,
651
712
  getChipHost = _useChipManager.getChipHost,
652
713
  getChips = _useChipManager.getChips,
653
714
  resetChips = _useChipManager.resetChips;
654
715
  getChipsRef.current = getChips;
716
+ closeChipHintsRef.current = closeChipHints;
655
717
  useChipSelectionMarker(editorRef, chips);
656
718
  var insertSerializedAtRangeRef = useRef(insertSerializedAtRange);
657
719
  insertSerializedAtRangeRef.current = insertSerializedAtRange;
@@ -775,6 +837,10 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
775
837
  onTriggerChange === null || onTriggerChange === void 0 || onTriggerChange(null);
776
838
  }
777
839
  onChange === null || onChange === void 0 || onChange('');
840
+ /* 这条路不走 fireChange(清空不是「内容变化」链路的一环),
841
+ * 灰字状态要在这里手动同步,否则业务侧会一直以为灰字还显示着。
842
+ * 位置放在 onChange 之后,与 fireChange 的「先 onChange 再灰字」一致。 */
843
+ notifySkillHint(null, []);
778
844
  },
779
845
  insertChip: function insertChip(data) {
780
846
  var el = editorRef.current;
@@ -871,7 +937,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
871
937
  return editorRef.current;
872
938
  }
873
939
  };
874
- }, [onChange, _insertChip, insertSerializedAtRange, requestScrollCaretIntoView, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
940
+ }, [onChange, _insertChip, insertSerializedAtRange, notifySkillHint, requestScrollCaretIntoView, removeChip, resetChips, onTriggerChange, getChips, updateChipData, getChipHost]);
875
941
 
876
942
  /* ─────────────────────────────────────
877
943
  * 监听 selection 变化:方向键 / 鼠标点击移位都要重算 trigger
@@ -1027,6 +1093,8 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
1027
1093
  }
1028
1094
  removeHintNode();
1029
1095
  resetChips();
1096
+ /* 同 clear():外部改写 value 不走 fireChange,灰字状态在这里手动同步 */
1097
+ notifySkillHint(null, []);
1030
1098
  setEmpty(shouldShowPlaceholder(el));
1031
1099
  if (triggerRef.current) {
1032
1100
  var _onTriggerChangeRef$c;
@@ -1035,7 +1103,7 @@ export var ComposerEditor = /*#__PURE__*/forwardRef(function ComposerEditor(_ref
1035
1103
  }
1036
1104
  syncHeight();
1037
1105
  }
1038
- }, [controlledSyncTick, valueProp, isControlled, resetChips, shouldShowPlaceholder, syncHeight]);
1106
+ }, [controlledSyncTick, valueProp, isControlled, notifySkillHint, resetChips, shouldShowPlaceholder, syncHeight]);
1039
1107
 
1040
1108
  /* ─────────────────────────────────────
1041
1109
  * 输入事件
@@ -49,6 +49,14 @@ export interface UseChipManagerReturn {
49
49
  removeChip: (id: string) => void;
50
50
  /** 更新 chip 数据(参数面板关闭保存等)。 */
51
51
  updateChipData: (id: string, patch: Partial<ChipData>) => void;
52
+ /**
53
+ * 把这些 chip 的 hint 标记为已关闭(`ChipData.hintClosed`)。
54
+ *
55
+ * 只改 chips 快照里的数据,**不抛 onChange / 不走 onAfterMutate**:调用点在
56
+ * fireChange 内部,走 onAfterMutate 会立刻再进一次 fireChange(递归)。
57
+ * 幂等:已经关闭的 / 不存在的 id 直接忽略。
58
+ */
59
+ closeChipHints: (ids: readonly string[]) => void;
52
60
  /** 按 id 取 chip host(参数面板 anchor)。 */
53
61
  getChipHost: (id: string) => HTMLSpanElement | null;
54
62
  /** 当前 chip 快照(读 ref,插入后同步 onChange 时可用)。 */
@@ -397,6 +397,27 @@ onAfterMutate) {
397
397
  setChips(next);
398
398
  onAfterMutate === null || onAfterMutate === void 0 || onAfterMutate();
399
399
  }, [onAfterMutate]);
400
+ var closeChipHints = useCallback(function (ids) {
401
+ if (ids.length === 0) return;
402
+ var targets = new Set(ids);
403
+ var mutated = false;
404
+ /* 不就地写 `data`:insertChip / insertSerializedAtRange 拿到的对象可能属于
405
+ * 调用方(defaultChips / insertContent 的 chips),原地改会把内部状态写到
406
+ * 业务数据上。 */
407
+ var next = chipsRef.current.map(function (chip) {
408
+ if (!targets.has(chip.data.id) || chip.data.hintClosed) return chip;
409
+ mutated = true;
410
+ return _objectSpread(_objectSpread({}, chip), {}, {
411
+ data: _objectSpread(_objectSpread({}, chip.data), {}, {
412
+ hintClosed: true
413
+ })
414
+ });
415
+ });
416
+ if (!mutated) return;
417
+ chipsRef.current = next;
418
+ // 不调 onAfterMutate:调用方就在 fireChange 里,否则会递归
419
+ setChips(next);
420
+ }, []);
400
421
  var getChipHost = useCallback(function (id) {
401
422
  var _chipsRef$current$fin, _chipsRef$current$fin2;
402
423
  return (_chipsRef$current$fin = (_chipsRef$current$fin2 = chipsRef.current.find(function (c) {
@@ -422,6 +443,7 @@ onAfterMutate) {
422
443
  syncChipsAfterDomMutation: syncChipsAfterDomMutation,
423
444
  removeChip: removeChip,
424
445
  updateChipData: updateChipData,
446
+ closeChipHints: closeChipHints,
425
447
  getChipHost: getChipHost,
426
448
  getChips: getChips,
427
449
  resetChips: resetChips
@@ -4,9 +4,11 @@ import type { IconName } from '../icon';
4
4
  import { type SkillSlotProps } from '../skill-slot';
5
5
  import { type ComposerInsertOptions, type ComposerInsertPart, type ComposerInsertResult } from './insert';
6
6
  import type { ComposerParamPanelContext } from './param-panel';
7
+ import { type ComposerSkillHint } from './segments';
7
8
  import './style';
8
9
  export type { ComposerInsertOptions, ComposerInsertPart, ComposerInsertResult, } from './insert';
9
10
  export type { ComposerParamPanelContext } from './param-panel';
11
+ export type { ComposerHintTarget, ComposerSkillHint } from './segments';
10
12
  export type ComposerTool = 'attachment' | 'webSearch' | 'skill';
11
13
  export type ComposerVariant = 'full' | 'lite';
12
14
  export type SubmitType = 'enter' | 'shiftEnter';
@@ -105,17 +107,33 @@ export interface ComposerProps {
105
107
  defaultChips?: ChipData[];
106
108
  /** 输入框值变化回调。第二参是当前 chip 快照,旧调用方忽略即可。 */
107
109
  onChange?: (value: string, chips?: ChipData[]) => void;
110
+ /**
111
+ * 技能灰字(`SkillItem.hint`)的出现 / 消失回调。
112
+ *
113
+ * 入参与输入框里那行灰字共用**同一份判定**(`resolveHintTarget`):命中时给
114
+ * `{ chipId, text, refId }`,消失时给 `null`。因此业务不需要自己解析
115
+ * value / chips,也不会出现「库里觉得有、业务觉得没有」的分歧。
116
+ *
117
+ * 语义要点:
118
+ * - 只在状态真的变化时抛一次,同一条状态不会因为连续输入重复抛;
119
+ * - 判定成立但灰字被临时摘掉(IME 合成期避开候选串)时不跟着抖;
120
+ * - 挂载时先同步一次当前状态(首屏没有灰字即 `null`),便于业务复位上一轮状态;
121
+ * - 灰字一次性:收起后即使把内容删回空串也不会再抛命中;
122
+ * - 卸载不会再补一次 `null`:需要清理时由业务在自己的卸载路径处理;
123
+ * - 与 `hint`(底部快捷键提示开关)无关。
124
+ */
125
+ onSkillHintChange?: (hint: ComposerSkillHint | null) => void;
108
126
  /**
109
127
  * 发送回调。
110
128
  *
111
129
  * `segments`:有序内容 IR,业务按数组顺序自定义拼后端。
112
130
  * `meta`:附件 / 联网搜索等非正文数据;为空时不传。
113
131
  *
114
- * **派生 `hint` 段**:当最后一个 chip 是带 `SkillItem.hint` 的 skill、且其后
115
- * 一个字符都没有(空格 / 换行都算内容)时,该提示文案会作为
116
- * `{ type: 'hint', data: { text } }` 出现在 segments 末尾——与输入框里那段
117
- * 灰字同源同谓词,不会漂移。不需要它的业务直接 filter / `isHintSegment`
118
- * 判一下即可。
132
+ * **派生 `hint` 段**:当最后一个 chip 是带 `SkillItem.hint` 的 skill、其后
133
+ * 一个字符都没有(空格 / 换行都算内容)、且这颗 chip 的 hint 还没关闭时,
134
+ * 该提示文案会作为 `{ type: 'hint', data: { text } }` 出现在 segments 末尾
135
+ * ——与输入框里那段灰字同源同谓词,不会漂移。不需要它的业务直接 filter /
136
+ * `isHintSegment` 判一下即可。
119
137
  *
120
138
  * 兼容旧链路(需要单串文本):业务可调 `segmentsToReadableText(segments)`。
121
139
  *
@@ -94,6 +94,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
94
94
  defaultValue = _ref$defaultValue === void 0 ? '' : _ref$defaultValue,
95
95
  defaultChips = _ref.defaultChips,
96
96
  onChangeProp = _ref.onChange,
97
+ onSkillHintChange = _ref.onSkillHintChange,
97
98
  onSend = _ref.onSend,
98
99
  onStop = _ref.onStop,
99
100
  _ref$isGenerating = _ref.isGenerating,
@@ -514,9 +515,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
514
515
  * - segments:有序 IR(text / invocation / mention 交替)
515
516
  * - meta:attachments / webSearch 等非正文数据;都为空时不传
516
517
  *
517
- * 派生 hint:最后一个 chip 是 skill 且其后没有内容时,`rawValueToSegments`
518
- * 会在末尾追加一段 `hint`(与该 chip 后面渲染的灰字同源、同谓词)。
519
- * 它不属于用户输入,也不是受控 value 的一部分。
518
+ * 派生 hint:最后一个 chip 是 skill、其后没有内容、且它的 hint 还没关闭时,
519
+ * `rawValueToSegments` 会在末尾追加一段 `hint`(与该 chip 后面渲染的灰字
520
+ * 同源、同谓词)。它不属于用户输入,也不是受控 value 的一部分;hint 一旦
521
+ * 关闭(`ChipData.hintClosed`)就不再追加——没显示给用户的模板不会被发出去。
520
522
  *
521
523
  * 需要单串文本的业务方在 onSend 内自行调 segmentsToReadableText。
522
524
  * onSend 返回 false 或 Promise<false> 时视为发送失败,不清空;否则正常清空。
@@ -1234,6 +1236,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
1234
1236
  value: text,
1235
1237
  defaultChips: defaultChips,
1236
1238
  onChange: updateText,
1239
+ onSkillHintChange: onSkillHintChange,
1237
1240
  disabled: disabled,
1238
1241
  paramEditChipId: (_paramEdit$chipId = paramEdit === null || paramEdit === void 0 ? void 0 : paramEdit.chipId) !== null && _paramEdit$chipId !== void 0 ? _paramEdit$chipId : null,
1239
1242
  onInlineRefClick: renderParamPanel ? handleInlineRefClick : undefined
@@ -6,7 +6,10 @@
6
6
  * - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
7
7
  * 还原为 `ComposerSegment[]`。
8
8
  * - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
9
- * (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
9
+ * (空格 / 换行都算内容)+ 该 chip 带 hint + hint 未关闭」,渲染与发送
10
+ * 都靠它。
11
+ * - `resolveHintClosedChipIds`:找出「一次性机会已作废」的 chip(自己的尾串
12
+ * 出现过内容),由编辑器写回 `ChipData.hintClosed`。
10
13
  * - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
11
14
  * 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
12
15
  * - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
@@ -26,19 +29,46 @@ export interface ComposerHintTarget {
26
29
  /** 提示文本(skill 模板文案,发送时原样作为 hint 段的 data.text) */
27
30
  text: string;
28
31
  }
32
+ /**
33
+ * 灰字当前状态的对外快照(`ComposerProps.onSkillHintChange` 的入参)。
34
+ *
35
+ * 在 `ComposerHintTarget` 基础上补 chip 的业务标识:业务拿到回调时通常只想
36
+ * 知道「哪一块技能正在展示灰字」,不必再去 onChange 的 chips 快照里反查——
37
+ * 尤其 chip 插入与注册是两条异步链路,反查可能还没就绪。
38
+ */
39
+ export interface ComposerSkillHint extends ComposerHintTarget {
40
+ /** 命中 chip 的业务侧资源 id(`ChipData.refId`,skill chip 即 `SkillItem.id`);自定义 chip 可能没有 */
41
+ refId?: string;
42
+ }
29
43
  /**
30
44
  * 判定当前内容是否命中 hint:
31
45
  * 1. `value` 里存在 marker(即至少有一个 chip)
32
46
  * 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
33
47
  * 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
48
+ * 4. 该 chip 的 hint 还没关闭(`hintClosed`,一次性)
34
49
  */
35
50
  export declare function resolveHintTarget(raw: string, chips: ChipData[]): ComposerHintTarget | null;
51
+ /**
52
+ * 找出「hint 的一次性机会已经作废」的 chip id(编辑器据此写回 `hintClosed`)。
53
+ *
54
+ * 判据只看每颗 chip **后面出现过什么**:本 marker 与下个 marker 之间有字符
55
+ * (文字 / 空格 / 换行),或者后面还有别的 marker(又插了一块),hint 的
56
+ * 机会就作废。因此它同时覆盖两类情况:
57
+ * - 插入时就落在正文中间(或后面紧跟着另一块):hint 从未出现,之后把后面
58
+ * 删空也不该再冒出来
59
+ * - 展示过又被内容顶掉:删回空串也不该回来
60
+ *
61
+ * 纯函数,幂等(已关闭的 chip 直接跳过,重复调用结果一致);没有 hint 文本的
62
+ * chip 不参与——没能力 ≠ 状态。
63
+ */
64
+ export declare function resolveHintClosedChipIds(raw: string, chips: ChipData[]): string[];
36
65
  /**
37
66
  * 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
38
67
  *
39
68
  * 这是「编辑器状态 → 发送内容」的**唯一映射**:
40
69
  * 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
41
- * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
70
+ * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后、其后一个字符都没有、
71
+ * 且 hint 还没关闭时)
42
72
  *
43
73
  * 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
44
74
  * 业务可以放心用它做预览 / 调试。
@@ -6,7 +6,10 @@
6
6
  * - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
7
7
  * 还原为 `ComposerSegment[]`。
8
8
  * - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
9
- * (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
9
+ * (空格 / 换行都算内容)+ 该 chip 带 hint + hint 未关闭」,渲染与发送
10
+ * 都靠它。
11
+ * - `resolveHintClosedChipIds`:找出「一次性机会已作废」的 chip(自己的尾串
12
+ * 出现过内容),由编辑器写回 `ChipData.hintClosed`。
10
13
  * - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
11
14
  * 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
12
15
  * - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
@@ -36,27 +39,40 @@ export var isHintSegment = function isHintSegment(s) {
36
39
  };
37
40
 
38
41
  /* ────────────────────────────────────────────────────────────────────
39
- * Hint 结构谓词 · 「最后一个 chip + 其后一个字符都没有」
42
+ * Hint 结构谓词 · 「最后一个 chip + 其后一个字符都没有 + 没关闭」
40
43
  *
41
44
  * hint 不是内容,而是内容结构的投影:只要最后一个 chip 之后的 value 是
42
- * **空串**,且该 chip 带 hint,就命中。
45
+ * **空串**,且该 chip 带 hint、且它的 hint 还没关闭,就命中。
43
46
  *
44
47
  * 空格、换行、文字**都算内容**:用户在 chip 后面敲下第一个字符 / 按下
45
48
  * Enter(含 Shift+Enter)都说明 skill 块之后已经有东西了,hint 必须让位。
46
- * 反过来,把这些字符(包括换行)删干净、chip 后面重新变成空串,hint 才回来。
47
49
  * 插入 chip 本身不会写空格(见 useChipManager.insertChip):行尾插入后尾串
48
50
  * 就是空串,hint 立刻命中,不需要为「自动补的空格」做特殊判断。
49
51
  *
52
+ * hint 是**一次性**的:只要这颗 chip 后面出现过东西——文字 / 空格 / 换行,
53
+ * 或者又插了一块——hint 就永久关闭(`ChipData.hintClosed`),之后把内容删回
54
+ * 空串也不会再出现,发送时也不再补 `hint` 段。因此「插入时后面本来就有内容」
55
+ * 的 chip 从一开始就是关闭态,不会因为后面被删空而凭空冒出灰字。关闭由
56
+ * `resolveHintClosedChipIds` 判定、编辑器在同一次内容变化里写回。
57
+ *
50
58
  * 谓词与光标位置无关——用户在前面打字不影响。**渲染(Editor)与发送
51
59
  * (Composer)都只依赖这一个谓词**,每次内容变化重算一次,两处永远一致。
52
60
  * ──────────────────────────────────────────────────────────────────── */
53
61
 
54
62
  /** hint 命中结果。 */
55
63
 
64
+ /**
65
+ * 灰字当前状态的对外快照(`ComposerProps.onSkillHintChange` 的入参)。
66
+ *
67
+ * 在 `ComposerHintTarget` 基础上补 chip 的业务标识:业务拿到回调时通常只想
68
+ * 知道「哪一块技能正在展示灰字」,不必再去 onChange 的 chips 快照里反查——
69
+ * 尤其 chip 插入与注册是两条异步链路,反查可能还没就绪。
70
+ */
71
+
56
72
  /**
57
73
  * 最后一个 marker 的 chip id 与它之后的尾串;没有 chip 时返回 null。
58
74
  *
59
- * 「尾串」是 hint 判定的唯一输入:谓词要求它是**空串**。
75
+ * 「尾串」是结构判定的一部分:谓词要求它是**空串**(另外还要求 hint 未关闭)。
60
76
  */
61
77
  function getLastMarkerTail(raw) {
62
78
  var markers = parseMarkers(raw);
@@ -73,6 +89,7 @@ function getLastMarkerTail(raw) {
73
89
  * 1. `value` 里存在 marker(即至少有一个 chip)
74
90
  * 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
75
91
  * 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
92
+ * 4. 该 chip 的 hint 还没关闭(`hintClosed`,一次性)
76
93
  */
77
94
  export function resolveHintTarget(raw, chips) {
78
95
  var _chip$hint;
@@ -83,19 +100,64 @@ export function resolveHintTarget(raw, chips) {
83
100
  });
84
101
  if (!chip || chip.kind === 'mention') return null;
85
102
  var text = (_chip$hint = chip.hint) === null || _chip$hint === void 0 ? void 0 : _chip$hint.trim();
86
- if (!text) return null;
103
+ if (!text || chip.hintClosed) return null;
87
104
  return {
88
105
  chipId: chip.id,
89
106
  text: text
90
107
  };
91
108
  }
92
109
 
110
+ /**
111
+ * 找出「hint 的一次性机会已经作废」的 chip id(编辑器据此写回 `hintClosed`)。
112
+ *
113
+ * 判据只看每颗 chip **后面出现过什么**:本 marker 与下个 marker 之间有字符
114
+ * (文字 / 空格 / 换行),或者后面还有别的 marker(又插了一块),hint 的
115
+ * 机会就作废。因此它同时覆盖两类情况:
116
+ * - 插入时就落在正文中间(或后面紧跟着另一块):hint 从未出现,之后把后面
117
+ * 删空也不该再冒出来
118
+ * - 展示过又被内容顶掉:删回空串也不该回来
119
+ *
120
+ * 纯函数,幂等(已关闭的 chip 直接跳过,重复调用结果一致);没有 hint 文本的
121
+ * chip 不参与——没能力 ≠ 状态。
122
+ */
123
+ export function resolveHintClosedChipIds(raw, chips) {
124
+ /* 先看有没有「还开着 hint 的 chip」:没有就不必解析 marker(无 hint 的
125
+ * composer 每次输入都会走到这里)。 */
126
+ var hasCandidate = chips.some(function (chip) {
127
+ var _chip$hint2;
128
+ return chip.kind !== 'mention' && !chip.hintClosed && !!((_chip$hint2 = chip.hint) !== null && _chip$hint2 !== void 0 && _chip$hint2.trim());
129
+ });
130
+ if (!hasCandidate || !raw) return [];
131
+ var markers = parseMarkers(raw);
132
+ if (markers.length === 0) return [];
133
+ var chipById = new Map(chips.map(function (chip) {
134
+ return [chip.id, chip];
135
+ }));
136
+ var closed = [];
137
+ var seen = new Set();
138
+ markers.forEach(function (marker, index) {
139
+ var _chip$hint3, _next$rawStart;
140
+ var chip = chipById.get(marker.id);
141
+ if (!chip || chip.kind === 'mention' || chip.hintClosed) return;
142
+ if (!((_chip$hint3 = chip.hint) !== null && _chip$hint3 !== void 0 && _chip$hint3.trim()) || seen.has(chip.id)) return;
143
+ var next = markers[index + 1];
144
+ /* 尾串 = 本 marker 结束 → 下个 marker 开始 */
145
+ var hasTextAfter = raw.slice(marker.rawEnd, (_next$rawStart = next === null || next === void 0 ? void 0 : next.rawStart) !== null && _next$rawStart !== void 0 ? _next$rawStart : raw.length).length > 0;
146
+ if (next || hasTextAfter) {
147
+ seen.add(chip.id);
148
+ closed.push(chip.id);
149
+ }
150
+ });
151
+ return closed;
152
+ }
153
+
93
154
  /**
94
155
  * 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
95
156
  *
96
157
  * 这是「编辑器状态 → 发送内容」的**唯一映射**:
97
158
  * 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
98
- * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
159
+ * 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后、其后一个字符都没有、
160
+ * 且 hint 还没关闭时)
99
161
  *
100
162
  * 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
101
163
  * 业务可以放心用它做预览 / 调试。
package/dist/index.d.ts CHANGED
@@ -83,4 +83,4 @@ export { default as Welcome, type WelcomeProps } from './welcome';
83
83
  export { ComposerSegmentType } from './_genui-types';
84
84
  export type { AgentEvent, Attachment, ChatMessage, ChipData, ComposerHintSegment, ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, Conversation, HintData, InlineRefState, InvocationData, MentionData, MessageSnapshot, PreviewTab, PreviewTarget, SendMeta, SkillItem, Source, TextData, ToolCall, ToolStatus, } from './_genui-types';
85
85
  export type { InterruptBase, InterruptIntent, InterruptStatus, } from './_interrupt-types';
86
- export { isHintSegment, isInvocationSegment, isMentionSegment, isTextSegment, rawValueToSegments, resolveHintTarget, segmentsHasContent, segmentsToReadableText, type ComposerHintTarget, type SegmentsToReadableTextOptions, } from './composer/segments';
86
+ export { isHintSegment, isInvocationSegment, isMentionSegment, isTextSegment, rawValueToSegments, resolveHintTarget, segmentsHasContent, segmentsToReadableText, type ComposerHintTarget, type ComposerSkillHint, type SegmentsToReadableTextOptions, } from './composer/segments';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "one-design-next",
3
- "version": "0.0.121",
3
+ "version": "0.0.123",
4
4
  "description": "One Design Next from TAD@tencent.com",
5
5
  "license": "MIT",
6
6
  "type": "module",