one-design-next 0.0.113 → 0.0.114
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/dist/_genui-types.d.ts +42 -3
- package/dist/_genui-types.js +19 -3
- package/dist/collapse/primitive.d.ts +1 -1
- package/dist/composer/caret.d.ts +74 -0
- package/dist/composer/caret.js +207 -1
- package/dist/composer/editor.d.ts +4 -3
- package/dist/composer/editor.js +292 -18
- package/dist/composer/hooks/useChipManager.d.ts +10 -2
- package/dist/composer/hooks/useChipManager.js +47 -14
- package/dist/composer/index.d.ts +6 -0
- package/dist/composer/index.js +35 -6
- package/dist/composer/inline-ref.d.ts +6 -1
- package/dist/composer/inline-ref.js +6 -1
- package/dist/composer/segments.d.ts +32 -4
- package/dist/composer/segments.js +86 -5
- package/dist/composer/style/index.css +22 -0
- package/dist/composer/utils.d.ts +21 -1
- package/dist/composer/utils.js +47 -3
- package/dist/icon/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/user-bubble/render-segments.d.ts +1 -1
- package/dist/user-bubble/render-segments.js +11 -2
- package/dist/user-bubble/style/index.css +8 -0
- package/package.json +1 -1
package/dist/composer/index.js
CHANGED
|
@@ -29,7 +29,10 @@ function createInvocationChipData(skill) {
|
|
|
29
29
|
kind: 'invocation',
|
|
30
30
|
state: skill.initialState,
|
|
31
31
|
refId: skill.id,
|
|
32
|
-
payload: {}
|
|
32
|
+
payload: {},
|
|
33
|
+
/* 提示文案随 chip 快照下来(而不是渲染时回查技能目录):
|
|
34
|
+
* 草稿用 defaultValue + defaultChips 还原时,目录可能已经变了。 */
|
|
35
|
+
hint: skill.hint
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
38
|
var TOOL_BTN_ACTIVE = 'color-mix(in srgb, var(--odn-color-brand-6) 10%, transparent)';
|
|
@@ -417,6 +420,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
417
420
|
* - segments:有序 IR(text / invocation / mention 交替)
|
|
418
421
|
* - meta:attachments / webSearch 等非正文数据;都为空时不传
|
|
419
422
|
*
|
|
423
|
+
* 派生 hint:最后一个 chip 是 skill 且其后没有内容时,`rawValueToSegments`
|
|
424
|
+
* 会在末尾追加一段 `hint`(与该 chip 后面渲染的灰字同源、同谓词)。
|
|
425
|
+
* 它不属于用户输入,也不是受控 value 的一部分。
|
|
426
|
+
*
|
|
420
427
|
* 需要单串文本的业务方在 onSend 内自行调 segmentsToReadableText。
|
|
421
428
|
* onSend 返回 false 或 Promise<false> 时视为发送失败,不清空;否则正常清空。
|
|
422
429
|
*
|
|
@@ -681,7 +688,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
681
688
|
|
|
682
689
|
/* editor 在 trigger 激活时把 ↑↓Enter Esc Tab 丢过来。返回 true = editor 别管了。
|
|
683
690
|
* - ↑↓ 改高亮 index(夹在 [0, max] 内)
|
|
684
|
-
* - Enter / Tab
|
|
691
|
+
* - Enter / Tab 选中当前高亮项(Tab 默认落在第一个可用技能上)
|
|
685
692
|
* - Escape 关 menu,焦点保留在 editor */
|
|
686
693
|
var handleTriggerKeyDown = useCallback(function (e) {
|
|
687
694
|
if (!triggerMenuVisible) return false;
|
|
@@ -700,7 +707,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
700
707
|
(_triggerSkillSlotRef$2 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$2 === void 0 || _triggerSkillSlotRef$2.moveActive('up');
|
|
701
708
|
return true;
|
|
702
709
|
}
|
|
703
|
-
if (e.key === 'Enter' && !e.shiftKey
|
|
710
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
704
711
|
var _triggerSkillSlotRef$3, _triggerSkillSlotRef$4;
|
|
705
712
|
e.preventDefault();
|
|
706
713
|
var _skill = (_triggerSkillSlotRef$3 = (_triggerSkillSlotRef$4 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$4 === void 0 ? void 0 : _triggerSkillSlotRef$4.getActiveSkill()) !== null && _triggerSkillSlotRef$3 !== void 0 ? _triggerSkillSlotRef$3 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills[triggerActiveIndex];
|
|
@@ -708,6 +715,20 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
708
715
|
handleSelectSkill(_skill, 'trigger');
|
|
709
716
|
return true;
|
|
710
717
|
}
|
|
718
|
+
/* Tab 与 Enter 同义:接受当前高亮项。query 变化时高亮已复位到第一个
|
|
719
|
+
* 可用项,所以「/ + 关键字有命中 → Tab」就是插入第一个匹配技能。
|
|
720
|
+
* 没有可选项时不 preventDefault,把 Tab 让回浏览器(editor 里还可能
|
|
721
|
+
* 落到 hint 采纳)。 */
|
|
722
|
+
if (e.key === 'Tab') {
|
|
723
|
+
var _ref4, _ref5, _triggerSkillSlotRef$5, _triggerSkillSlotRef$6;
|
|
724
|
+
var _skill2 = (_ref4 = (_ref5 = (_triggerSkillSlotRef$5 = (_triggerSkillSlotRef$6 = triggerSkillSlotRef.current) === null || _triggerSkillSlotRef$6 === void 0 ? void 0 : _triggerSkillSlotRef$6.getActiveSkill()) !== null && _triggerSkillSlotRef$5 !== void 0 ? _triggerSkillSlotRef$5 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills[triggerActiveIndex]) !== null && _ref5 !== void 0 ? _ref5 : filteredSkills === null || filteredSkills === void 0 ? void 0 : filteredSkills.find(function (s) {
|
|
725
|
+
return !s.disabled;
|
|
726
|
+
})) !== null && _ref4 !== void 0 ? _ref4 : null;
|
|
727
|
+
if (!_skill2) return false;
|
|
728
|
+
e.preventDefault();
|
|
729
|
+
handleSelectSkill(_skill2, 'trigger');
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
711
732
|
if (e.key === 'Escape') {
|
|
712
733
|
var _editorRef$current19;
|
|
713
734
|
e.preventDefault();
|
|
@@ -741,8 +762,8 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
741
762
|
useLayoutEffect(function () {
|
|
742
763
|
if (!toolMenuOpen && !triggerMenuVisible) return undefined;
|
|
743
764
|
var sync = function sync() {
|
|
744
|
-
var _skillTriggerRef$curr, _skillTriggerRef$curr2,
|
|
745
|
-
var triggerTop = toolMenuOpen ? (_skillTriggerRef$curr = (_skillTriggerRef$curr2 = skillTriggerRef.current) === null || _skillTriggerRef$curr2 === void 0 ? void 0 : _skillTriggerRef$curr2.getBoundingClientRect().top) !== null && _skillTriggerRef$curr !== void 0 ? _skillTriggerRef$curr : null : (
|
|
765
|
+
var _skillTriggerRef$curr, _skillTriggerRef$curr2, _ref6, _triggerInfo$rect$top, _triggerAnchorRef$cur;
|
|
766
|
+
var triggerTop = toolMenuOpen ? (_skillTriggerRef$curr = (_skillTriggerRef$curr2 = skillTriggerRef.current) === null || _skillTriggerRef$curr2 === void 0 ? void 0 : _skillTriggerRef$curr2.getBoundingClientRect().top) !== null && _skillTriggerRef$curr !== void 0 ? _skillTriggerRef$curr : null : (_ref6 = (_triggerInfo$rect$top = triggerInfo === null || triggerInfo === void 0 ? void 0 : triggerInfo.rect.top) !== null && _triggerInfo$rect$top !== void 0 ? _triggerInfo$rect$top : (_triggerAnchorRef$cur = triggerAnchorRef.current) === null || _triggerAnchorRef$cur === void 0 ? void 0 : _triggerAnchorRef$cur.getBoundingClientRect().top) !== null && _ref6 !== void 0 ? _ref6 : null;
|
|
746
767
|
if (triggerTop === null) return;
|
|
747
768
|
var next = resolveComposerSkillMenuMaxHeight({
|
|
748
769
|
triggerTop: triggerTop,
|
|
@@ -848,7 +869,10 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
848
869
|
activeColor: TOOL_BTN_ACTIVE,
|
|
849
870
|
disabled: btnDisabled
|
|
850
871
|
}, /*#__PURE__*/React.createElement("button", {
|
|
851
|
-
type: "button"
|
|
872
|
+
type: "button"
|
|
873
|
+
/* 不进入 Tab 焦点序列:composer 内的 Tab 只用于 hint 采纳,
|
|
874
|
+
* 不允许它逐个跳到工具栏按钮上(鼠标点击仍可正常使用)。 */,
|
|
875
|
+
tabIndex: -1,
|
|
852
876
|
onClick: function onClick() {
|
|
853
877
|
var _fileInputRef$current;
|
|
854
878
|
onToolClick === null || onToolClick === void 0 || onToolClick('attachment');
|
|
@@ -888,6 +912,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
888
912
|
disabled: disabled || webSearch
|
|
889
913
|
}, /*#__PURE__*/React.createElement("button", {
|
|
890
914
|
type: "button",
|
|
915
|
+
tabIndex: -1,
|
|
891
916
|
onClick: function onClick() {
|
|
892
917
|
onToolClick === null || onToolClick === void 0 || onToolClick('webSearch');
|
|
893
918
|
setWebSearch(function (v) {
|
|
@@ -965,6 +990,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
965
990
|
disabled: disabled || toolMenuOpen
|
|
966
991
|
}, /*#__PURE__*/React.createElement("button", {
|
|
967
992
|
type: "button",
|
|
993
|
+
tabIndex: -1,
|
|
968
994
|
disabled: disabled,
|
|
969
995
|
onMouseDown: function onMouseDown(e) {
|
|
970
996
|
return e.preventDefault();
|
|
@@ -1038,6 +1064,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
1038
1064
|
* 因为整体禁用语义包含"现在什么都不能做",包括中断。 */
|
|
1039
1065
|
var sendOrStopButton = !disabled && isGenerating ? /*#__PURE__*/React.createElement("button", {
|
|
1040
1066
|
type: "button",
|
|
1067
|
+
tabIndex: -1,
|
|
1041
1068
|
onClick: onStop,
|
|
1042
1069
|
"data-odn-composer-stop-btn": true,
|
|
1043
1070
|
title: "\u505C\u6B62\u751F\u6210"
|
|
@@ -1046,6 +1073,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
1046
1073
|
size: 18
|
|
1047
1074
|
})) : /*#__PURE__*/React.createElement("button", {
|
|
1048
1075
|
type: "button",
|
|
1076
|
+
tabIndex: -1,
|
|
1049
1077
|
onClick: handleSend,
|
|
1050
1078
|
disabled: disabled || !canSend,
|
|
1051
1079
|
"data-odn-composer-send-btn": true,
|
|
@@ -1222,6 +1250,7 @@ export var Composer = /*#__PURE__*/forwardRef(function Composer(_ref, ref) {
|
|
|
1222
1250
|
placement: "top"
|
|
1223
1251
|
}, /*#__PURE__*/React.createElement("button", {
|
|
1224
1252
|
type: "button",
|
|
1253
|
+
tabIndex: -1,
|
|
1225
1254
|
"data-odn-composer-expand-btn": true,
|
|
1226
1255
|
"aria-label": fullExpandLabel,
|
|
1227
1256
|
"aria-expanded": fullExpanded,
|
|
@@ -10,6 +10,11 @@ export interface ComposerInlineRefProps {
|
|
|
10
10
|
/** 有参数面板时由 Composer 注入;点击打开面板 */
|
|
11
11
|
onClick?: (anchor: HTMLElement) => void;
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Composer 内联引用视觉:按 kind 派发 Invocation(/)或 Mention(@)。
|
|
15
|
+
*
|
|
16
|
+
* 注:派生 hint 不在 host 内渲染——它是 host 的兄弟节点(见 `editor.tsx` 的
|
|
17
|
+
* `syncHintNode`),否则「chip 之后、hint 之前」这个 caret 位置会不存在。
|
|
18
|
+
*/
|
|
14
19
|
export declare function ComposerInlineRef({ data, active, onClick, }: ComposerInlineRefProps): React.JSX.Element;
|
|
15
20
|
export default ComposerInlineRef;
|
|
@@ -16,7 +16,12 @@ export function chipHasEditableParams(chip) {
|
|
|
16
16
|
return v !== null && v !== undefined && v !== '';
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Composer 内联引用视觉:按 kind 派发 Invocation(/)或 Mention(@)。
|
|
21
|
+
*
|
|
22
|
+
* 注:派生 hint 不在 host 内渲染——它是 host 的兄弟节点(见 `editor.tsx` 的
|
|
23
|
+
* `syncHintNode`),否则「chip 之后、hint 之前」这个 caret 位置会不存在。
|
|
24
|
+
*/
|
|
20
25
|
export function ComposerInlineRef(_ref) {
|
|
21
26
|
var data = _ref.data,
|
|
22
27
|
_ref$active = _ref.active,
|
|
@@ -5,18 +5,44 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
|
|
7
7
|
* 还原为 `ComposerSegment[]`。
|
|
8
|
+
* - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
|
|
9
|
+
* (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
|
|
8
10
|
* - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
|
|
9
11
|
* 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
|
|
10
12
|
* - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
|
|
11
13
|
*/
|
|
12
|
-
import type { ChipData, ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, TextData } from '../_genui-types';
|
|
14
|
+
import type { ChipData, ComposerHintSegment, ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, HintData, TextData } from '../_genui-types';
|
|
13
15
|
import { ComposerSegmentType } from '../_genui-types';
|
|
14
|
-
export type { ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, TextData, };
|
|
15
16
|
export { ComposerSegmentType };
|
|
17
|
+
export type { ComposerHintSegment, ComposerInvocationSegment, ComposerMentionSegment, ComposerSegment, ComposerTextSegment, HintData, TextData, };
|
|
16
18
|
export declare const isTextSegment: (s: ComposerSegment) => s is ComposerTextSegment;
|
|
17
19
|
export declare const isInvocationSegment: (s: ComposerSegment) => s is ComposerInvocationSegment;
|
|
18
20
|
export declare const isMentionSegment: (s: ComposerSegment) => s is ComposerMentionSegment;
|
|
19
|
-
|
|
21
|
+
export declare const isHintSegment: (s: ComposerSegment) => s is ComposerHintSegment;
|
|
22
|
+
/** hint 命中结果。 */
|
|
23
|
+
export interface ComposerHintTarget {
|
|
24
|
+
/** 命中 hint 的 chip id(渲染时用它定位到具体 chip) */
|
|
25
|
+
chipId: string;
|
|
26
|
+
/** 提示文本(skill 模板文案,发送时原样作为 hint 段的 data.text) */
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 判定当前内容是否命中 hint:
|
|
31
|
+
* 1. `value` 里存在 marker(即至少有一个 chip)
|
|
32
|
+
* 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
|
|
33
|
+
* 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveHintTarget(raw: string, chips: ChipData[]): ComposerHintTarget | null;
|
|
36
|
+
/**
|
|
37
|
+
* 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
|
|
38
|
+
*
|
|
39
|
+
* 这是「编辑器状态 → 发送内容」的**唯一映射**:
|
|
40
|
+
* 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
|
|
41
|
+
* 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
|
|
42
|
+
*
|
|
43
|
+
* 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
|
|
44
|
+
* 业务可以放心用它做预览 / 调试。
|
|
45
|
+
*/
|
|
20
46
|
export declare function rawValueToSegments(raw: string, chips: ChipData[]): ComposerSegment[];
|
|
21
47
|
export interface SegmentsToReadableTextOptions {
|
|
22
48
|
/** invocation 占位格式,默认 `[Skill: {label}]` */
|
|
@@ -27,7 +53,9 @@ export interface SegmentsToReadableTextOptions {
|
|
|
27
53
|
/**
|
|
28
54
|
* 把 segments 拼成一条字符串,供需要单串 string 的 LLM / 旧后端使用。
|
|
29
55
|
* 库**不内置调用**此函数;由业务在 onSend 内自行选择。
|
|
56
|
+
*
|
|
57
|
+
* `hint` 段直接输出原文(它本身就是给模型的模板文案,不加 `[Skill: …]` 包裹)。
|
|
30
58
|
*/
|
|
31
59
|
export declare function segmentsToReadableText(segments: ComposerSegment[], options?: SegmentsToReadableTextOptions): string;
|
|
32
|
-
/** segments
|
|
60
|
+
/** segments 是否含可见内容(文本非空、含内联引用,或含派生 hint)。 */
|
|
33
61
|
export declare function segmentsHasContent(segments: ComposerSegment[]): boolean;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - `rawValueToSegments`:库内部用,把含零宽 marker 的 raw value + chip 列表
|
|
7
7
|
* 还原为 `ComposerSegment[]`。
|
|
8
|
+
* - `resolveHintTarget`:结构谓词——「最后一个 chip 之后一个字符都没有
|
|
9
|
+
* (空格 / 换行都算内容)+ 该 chip 带 hint」,渲染与发送都靠它。
|
|
8
10
|
* - `segmentsToReadableText`:业务可选工具,按指定占位符格式把 segments 拼成
|
|
9
11
|
* 一句话(默认 `[Skill: label]` / `[@label]`,与 ComposerMention 兼容)。
|
|
10
12
|
* - `segmentsHasContent`:是否含可见内容;库内 send 校验用,业务可复用。
|
|
@@ -12,9 +14,8 @@
|
|
|
12
14
|
|
|
13
15
|
import { ComposerSegmentType } from "../_genui-types";
|
|
14
16
|
import { chipToInvocationData, chipToMentionData } from "./send-meta";
|
|
15
|
-
import { forEachMarkerSegment } from "./utils";
|
|
17
|
+
import { forEachMarkerSegment, parseMarkers } from "./utils";
|
|
16
18
|
export { ComposerSegmentType };
|
|
17
|
-
|
|
18
19
|
/* ────────────────────────────────────────────────────────────────────
|
|
19
20
|
* Type guards · 给业务方 filter / find 用,避免现场写 Extract<…>。
|
|
20
21
|
*
|
|
@@ -30,8 +31,75 @@ export var isInvocationSegment = function isInvocationSegment(s) {
|
|
|
30
31
|
export var isMentionSegment = function isMentionSegment(s) {
|
|
31
32
|
return s.type === ComposerSegmentType.Mention;
|
|
32
33
|
};
|
|
34
|
+
export var isHintSegment = function isHintSegment(s) {
|
|
35
|
+
return s.type === ComposerSegmentType.Hint;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/* ────────────────────────────────────────────────────────────────────
|
|
39
|
+
* Hint 结构谓词 · 「最后一个 chip + 其后一个字符都没有」
|
|
40
|
+
*
|
|
41
|
+
* hint 不是内容,而是内容结构的投影:只要最后一个 chip 之后的 value 是
|
|
42
|
+
* **空串**,且该 chip 带 hint,就命中。
|
|
43
|
+
*
|
|
44
|
+
* 空格、换行、文字**都算内容**:用户在 chip 后面敲下第一个字符 / 按下
|
|
45
|
+
* Enter(含 Shift+Enter)都说明 skill 块之后已经有东西了,hint 必须让位。
|
|
46
|
+
* 反过来,把这些字符(包括换行)删干净、chip 后面重新变成空串,hint 才回来。
|
|
47
|
+
* 插入 chip 本身不会写空格(见 useChipManager.insertChip):行尾插入后尾串
|
|
48
|
+
* 就是空串,hint 立刻命中,不需要为「自动补的空格」做特殊判断。
|
|
49
|
+
*
|
|
50
|
+
* 谓词与光标位置无关——用户在前面打字不影响。**渲染(Editor)与发送
|
|
51
|
+
* (Composer)都只依赖这一个谓词**,每次内容变化重算一次,两处永远一致。
|
|
52
|
+
* ──────────────────────────────────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
/** hint 命中结果。 */
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 最后一个 marker 的 chip id 与它之后的尾串;没有 chip 时返回 null。
|
|
58
|
+
*
|
|
59
|
+
* 「尾串」是 hint 判定的唯一输入:谓词要求它是**空串**。
|
|
60
|
+
*/
|
|
61
|
+
function getLastMarkerTail(raw) {
|
|
62
|
+
var markers = parseMarkers(raw);
|
|
63
|
+
var last = markers[markers.length - 1];
|
|
64
|
+
if (!last) return null;
|
|
65
|
+
return {
|
|
66
|
+
chipId: last.id,
|
|
67
|
+
tail: raw.slice(last.rawEnd)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
33
70
|
|
|
34
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* 判定当前内容是否命中 hint:
|
|
73
|
+
* 1. `value` 里存在 marker(即至少有一个 chip)
|
|
74
|
+
* 2. 最后一个 marker 之后**一个字符都没有**(空格 / 换行都算内容)
|
|
75
|
+
* 3. 该 marker 对应的 chip 带 hint 文本(`@` 触发的 mention chip 不参与)
|
|
76
|
+
*/
|
|
77
|
+
export function resolveHintTarget(raw, chips) {
|
|
78
|
+
var _chip$hint;
|
|
79
|
+
var info = getLastMarkerTail(raw);
|
|
80
|
+
if (!info || info.tail.length > 0) return null;
|
|
81
|
+
var chip = chips.find(function (c) {
|
|
82
|
+
return c.id === info.chipId;
|
|
83
|
+
});
|
|
84
|
+
if (!chip || chip.kind === 'mention') return null;
|
|
85
|
+
var text = (_chip$hint = chip.hint) === null || _chip$hint === void 0 ? void 0 : _chip$hint.trim();
|
|
86
|
+
if (!text) return null;
|
|
87
|
+
return {
|
|
88
|
+
chipId: chip.id,
|
|
89
|
+
text: text
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 从含零宽 marker 的 raw value + chip 列表构建有序 segments。
|
|
95
|
+
*
|
|
96
|
+
* 这是「编辑器状态 → 发送内容」的**唯一映射**:
|
|
97
|
+
* 1. marker 依次还原为 invocation / mention,与文本段按出现顺序交替
|
|
98
|
+
* 2. 末尾按结构谓词补一段派生的 `hint`(skill 在最后且其后一个字符都没有时)
|
|
99
|
+
*
|
|
100
|
+
* 因此 `rawValueToSegments(value, chips)` 与 `onSend` 收到的 segments 一致,
|
|
101
|
+
* 业务可以放心用它做预览 / 调试。
|
|
102
|
+
*/
|
|
35
103
|
export function rawValueToSegments(raw, chips) {
|
|
36
104
|
var chipById = new Map(chips.map(function (c) {
|
|
37
105
|
return [c.id, c];
|
|
@@ -63,6 +131,15 @@ export function rawValueToSegments(raw, chips) {
|
|
|
63
131
|
});
|
|
64
132
|
}
|
|
65
133
|
});
|
|
134
|
+
var hintTarget = resolveHintTarget(raw, chips);
|
|
135
|
+
if (hintTarget) {
|
|
136
|
+
segments.push({
|
|
137
|
+
type: 'hint',
|
|
138
|
+
data: {
|
|
139
|
+
text: hintTarget.text
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
66
143
|
return segments;
|
|
67
144
|
}
|
|
68
145
|
var defaultFormatInvocation = function defaultFormatInvocation(label) {
|
|
@@ -75,6 +152,8 @@ var defaultFormatMention = function defaultFormatMention(label) {
|
|
|
75
152
|
/**
|
|
76
153
|
* 把 segments 拼成一条字符串,供需要单串 string 的 LLM / 旧后端使用。
|
|
77
154
|
* 库**不内置调用**此函数;由业务在 onSend 内自行选择。
|
|
155
|
+
*
|
|
156
|
+
* `hint` 段直接输出原文(它本身就是给模型的模板文案,不加 `[Skill: …]` 包裹)。
|
|
78
157
|
*/
|
|
79
158
|
export function segmentsToReadableText(segments, options) {
|
|
80
159
|
var _options$formatInvoca, _options$formatMentio;
|
|
@@ -88,15 +167,17 @@ export function segmentsToReadableText(segments, options) {
|
|
|
88
167
|
return fmtInv(seg.data.label);
|
|
89
168
|
case 'mention':
|
|
90
169
|
return fmtMen(seg.data.label);
|
|
170
|
+
case 'hint':
|
|
171
|
+
return seg.data.text;
|
|
91
172
|
default:
|
|
92
173
|
return '';
|
|
93
174
|
}
|
|
94
175
|
}).join('');
|
|
95
176
|
}
|
|
96
177
|
|
|
97
|
-
/** segments
|
|
178
|
+
/** segments 是否含可见内容(文本非空、含内联引用,或含派生 hint)。 */
|
|
98
179
|
export function segmentsHasContent(segments) {
|
|
99
180
|
return segments.some(function (s) {
|
|
100
|
-
return s.type === 'invocation' || s.type === 'mention' || s.type === 'text' && s.data.text.trim().length > 0;
|
|
181
|
+
return s.type === 'invocation' || s.type === 'mention' || s.type === 'hint' || s.type === 'text' && s.data.text.trim().length > 0;
|
|
101
182
|
});
|
|
102
183
|
}
|
|
@@ -169,7 +169,12 @@
|
|
|
169
169
|
width: 100%;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/* 升格态:box 必须回到正常流。
|
|
173
|
+
* 单行态是 absolute + bottom: 0(贴 inner 底部,inner 只留 52px)。内容一旦
|
|
174
|
+
* 变高(多行文本 / 派生 hint),绝对定位的 box 会向上溢出 inner 的占位、盖住
|
|
175
|
+
* 上方所有内容(demo 里表现为输入框飘到外面的文案上)。 */
|
|
172
176
|
[data-odn-composer][data-odn-composer-variant=lite][data-odn-composer-expanded] [data-odn-composer-box] {
|
|
177
|
+
position: relative;
|
|
173
178
|
padding-bottom: 42px;
|
|
174
179
|
}
|
|
175
180
|
|
|
@@ -287,6 +292,23 @@
|
|
|
287
292
|
line-height: inherit;
|
|
288
293
|
}
|
|
289
294
|
|
|
295
|
+
/* ------------------------------------------------------------------
|
|
296
|
+
* 派生 hint(结构谓词:chip 是最后一个内容且其后无可见内容)
|
|
297
|
+
* 它是 chip host 的**兄弟节点**(不是 host 内部):
|
|
298
|
+
* - 只有兄弟关系才存在「chip 之后、hint 之前」这个 caret 落点;
|
|
299
|
+
* host 是 contentEditable=false 原子块,塞进 host 就只能落在整块之后
|
|
300
|
+
* - hint 之后不允许落 caret(caret.ts · normalizeCollapsedCaretBeforeHint)
|
|
301
|
+
* - 非内容:getPlainText 整棵子树跳过,不进 value / 剪贴板 / segments
|
|
302
|
+
* - pre-wrap:skills 的模板文案可含换行
|
|
303
|
+
* ----------------------------------------------------------------- */
|
|
304
|
+
[data-odn-composer] [data-odn-composer-skill-hint] {
|
|
305
|
+
margin-left: 4px;
|
|
306
|
+
color: var(--odn-color-black-8);
|
|
307
|
+
pointer-events: none;
|
|
308
|
+
user-select: none;
|
|
309
|
+
white-space: pre-wrap;
|
|
310
|
+
}
|
|
311
|
+
|
|
290
312
|
/* ------------------------------------------------------------------
|
|
291
313
|
* Block 行模型(caret-rewrite step 1/5)
|
|
292
314
|
* editor 直接子节点都是 [data-composer-line] div,每行渲染为独立 block。
|
package/dist/composer/utils.d.ts
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
export declare const LINE_DATASET_KEY = "composerLine";
|
|
14
14
|
/** 行容器 DOM 属性名。 */
|
|
15
15
|
export declare const LINE_ATTR_NAME = "data-composer-line";
|
|
16
|
+
/**
|
|
17
|
+
* 派生 hint 的 DOM 钩子(skill 在最后且其后无内容时,渲染在 chip 之后的只读文本)。
|
|
18
|
+
*
|
|
19
|
+
* **不是正文**:它是 chip host 的兄弟节点,`getPlainText` 整棵子树跳过,
|
|
20
|
+
* 因此不进 value / 剪贴板;caret 也不允许停在它之后(见 `caret.ts`)。
|
|
21
|
+
* 注意别和底部快捷键提示的 `data-odn-composer-hint` 混用。
|
|
22
|
+
*/
|
|
23
|
+
export declare const COMPOSER_HINT_ATTR = "data-odn-composer-skill-hint";
|
|
24
|
+
export declare const COMPOSER_HINT_SELECTOR = "[data-odn-composer-skill-hint]";
|
|
16
25
|
/** 是否是行容器。 */
|
|
17
26
|
export declare function isLineRoot(node: Node | null | undefined): node is HTMLDivElement;
|
|
18
27
|
/** 创建一个新的空行容器。 */
|
|
@@ -70,6 +79,16 @@ export declare function forEachMarkerSegment(raw: string, visit: (segment: {
|
|
|
70
79
|
type: 'marker';
|
|
71
80
|
id: string;
|
|
72
81
|
}) => void): void;
|
|
82
|
+
/**
|
|
83
|
+
* 行容器内的 `<br>` 是否只是浏览器给「空行」补的占位。
|
|
84
|
+
*
|
|
85
|
+
* 行模型下换行由 line div 表达,行内的 `<br>` 不承载用户换行;Chrome 在
|
|
86
|
+
* 用户删空一行后会把该行变成 `<div><br></div>`(占位,撑住行高)。若把它
|
|
87
|
+
* 当成 '\n',`value` 就会凭空多出一行(`marker + '\n'` 变成 `marker + '\n\n'`)。
|
|
88
|
+
*
|
|
89
|
+
* 判定:该 `<br>` 之后只剩不可见文本(ZWSP 等)或更多 `<br>`。
|
|
90
|
+
*/
|
|
91
|
+
export declare const isPlaceholderBr: (node: Node | null | undefined) => boolean;
|
|
73
92
|
/**
|
|
74
93
|
* 从 contenteditable 里把所有 chip 替换成 marker,得到上层 onChange / onSend
|
|
75
94
|
* 关心的 raw string。
|
|
@@ -77,7 +96,8 @@ export declare function forEachMarkerSegment(raw: string, visit: (segment: {
|
|
|
77
96
|
* 原则:
|
|
78
97
|
* - chip host 元素 → 替换为 `createMarker(id)`
|
|
79
98
|
* - 编辑器自塞的 ZWSP 锚点 → 全部剔除(不进入 value)
|
|
80
|
-
* -
|
|
99
|
+
* - 派生 hint 子树 → 整棵跳过(它不是正文)
|
|
100
|
+
* - `<br>` → `\n`;但行容器内的「占位 `<br>`」(空行撑高用)不算换行
|
|
81
101
|
* - 行容器 `<div data-composer-line>` → 行间输出 `\n`(首行除外)
|
|
82
102
|
*
|
|
83
103
|
* @param editor 编辑区根节点(contentEditable=true)
|
package/dist/composer/utils.js
CHANGED
|
@@ -29,6 +29,16 @@ export var LINE_DATASET_KEY = 'composerLine';
|
|
|
29
29
|
/** 行容器 DOM 属性名。 */
|
|
30
30
|
export var LINE_ATTR_NAME = 'data-composer-line';
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 派生 hint 的 DOM 钩子(skill 在最后且其后无内容时,渲染在 chip 之后的只读文本)。
|
|
34
|
+
*
|
|
35
|
+
* **不是正文**:它是 chip host 的兄弟节点,`getPlainText` 整棵子树跳过,
|
|
36
|
+
* 因此不进 value / 剪贴板;caret 也不允许停在它之后(见 `caret.ts`)。
|
|
37
|
+
* 注意别和底部快捷键提示的 `data-odn-composer-hint` 混用。
|
|
38
|
+
*/
|
|
39
|
+
export var COMPOSER_HINT_ATTR = 'data-odn-composer-skill-hint';
|
|
40
|
+
export var COMPOSER_HINT_SELECTOR = "[".concat(COMPOSER_HINT_ATTR, "]");
|
|
41
|
+
|
|
32
42
|
/** 是否是行容器。 */
|
|
33
43
|
export function isLineRoot(node) {
|
|
34
44
|
var _hasAttribute, _ref;
|
|
@@ -233,6 +243,35 @@ export function forEachMarkerSegment(raw, visit) {
|
|
|
233
243
|
* getPlainText · 从 DOM 提取 marker 化的字符串
|
|
234
244
|
* ──────────────────────────────────────────────────────────────────── */
|
|
235
245
|
|
|
246
|
+
/**
|
|
247
|
+
* 行容器内的 `<br>` 是否只是浏览器给「空行」补的占位。
|
|
248
|
+
*
|
|
249
|
+
* 行模型下换行由 line div 表达,行内的 `<br>` 不承载用户换行;Chrome 在
|
|
250
|
+
* 用户删空一行后会把该行变成 `<div><br></div>`(占位,撑住行高)。若把它
|
|
251
|
+
* 当成 '\n',`value` 就会凭空多出一行(`marker + '\n'` 变成 `marker + '\n\n'`)。
|
|
252
|
+
*
|
|
253
|
+
* 判定:该 `<br>` 之后只剩不可见文本(ZWSP 等)或更多 `<br>`。
|
|
254
|
+
*/
|
|
255
|
+
export var isPlaceholderBr = function isPlaceholderBr(node) {
|
|
256
|
+
if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
257
|
+
if (node.tagName !== 'BR') return false;
|
|
258
|
+
var cur = node.nextSibling;
|
|
259
|
+
while (cur) {
|
|
260
|
+
if (cur.nodeType === Node.TEXT_NODE) {
|
|
261
|
+
var _cur$textContent;
|
|
262
|
+
if (stripInvisibleChars((_cur$textContent = cur.textContent) !== null && _cur$textContent !== void 0 ? _cur$textContent : '') !== '') return false;
|
|
263
|
+
cur = cur.nextSibling;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (cur.nodeType === Node.ELEMENT_NODE && cur.tagName === 'BR') {
|
|
267
|
+
cur = cur.nextSibling;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return true;
|
|
273
|
+
};
|
|
274
|
+
|
|
236
275
|
/**
|
|
237
276
|
* 从 contenteditable 里把所有 chip 替换成 marker,得到上层 onChange / onSend
|
|
238
277
|
* 关心的 raw string。
|
|
@@ -240,7 +279,8 @@ export function forEachMarkerSegment(raw, visit) {
|
|
|
240
279
|
* 原则:
|
|
241
280
|
* - chip host 元素 → 替换为 `createMarker(id)`
|
|
242
281
|
* - 编辑器自塞的 ZWSP 锚点 → 全部剔除(不进入 value)
|
|
243
|
-
* -
|
|
282
|
+
* - 派生 hint 子树 → 整棵跳过(它不是正文)
|
|
283
|
+
* - `<br>` → `\n`;但行容器内的「占位 `<br>`」(空行撑高用)不算换行
|
|
244
284
|
* - 行容器 `<div data-composer-line>` → 行间输出 `\n`(首行除外)
|
|
245
285
|
*
|
|
246
286
|
* @param editor 编辑区根节点(contentEditable=true)
|
|
@@ -252,6 +292,8 @@ export function getPlainText(editor) {
|
|
|
252
292
|
var out = '';
|
|
253
293
|
var visit = function visit(node) {
|
|
254
294
|
if (node instanceof HTMLElement) {
|
|
295
|
+
// 派生 hint 整棵子树都不是正文,直接跳过(含其内部文本节点)
|
|
296
|
+
if (node.hasAttribute(COMPOSER_HINT_ATTR)) return;
|
|
255
297
|
var id = node.dataset[chipIdAttr];
|
|
256
298
|
if (id) {
|
|
257
299
|
out += createMarker(id);
|
|
@@ -286,6 +328,8 @@ export function getPlainText(editor) {
|
|
|
286
328
|
firstLineSeen = true;
|
|
287
329
|
for (var _i3 = 0, _Array$from3 = Array.from(c.childNodes); _i3 < _Array$from3.length; _i3++) {
|
|
288
330
|
var cc = _Array$from3[_i3];
|
|
331
|
+
/* 行容器内的占位 <br> 不算换行(换行已由行边界表达)。 */
|
|
332
|
+
if (isPlaceholderBr(cc)) continue;
|
|
289
333
|
visit(cc);
|
|
290
334
|
}
|
|
291
335
|
continue;
|
|
@@ -412,8 +456,8 @@ var isInvisibleChar = function isInvisibleChar(ch) {
|
|
|
412
456
|
* 影响触发判定。 */
|
|
413
457
|
var prevSignificantSibling = function prevSignificantSibling(node) {
|
|
414
458
|
var cur = node.previousSibling;
|
|
415
|
-
while (cur && cur.nodeType === Node.TEXT_NODE && ((_cur$
|
|
416
|
-
var _cur$
|
|
459
|
+
while (cur && cur.nodeType === Node.TEXT_NODE && ((_cur$textContent2 = cur.textContent) !== null && _cur$textContent2 !== void 0 ? _cur$textContent2 : '') === '') {
|
|
460
|
+
var _cur$textContent2;
|
|
417
461
|
cur = cur.previousSibling;
|
|
418
462
|
}
|
|
419
463
|
return cur;
|