@intentface/chat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/ask-user/ask-user.d.ts +104 -0
- package/dist/ask-user/ask-user.js +226 -0
- package/dist/ask-user/index.d.ts +3 -0
- package/dist/ask-user/index.js +3 -0
- package/dist/ask-user/index.parts.d.ts +1 -0
- package/dist/ask-user/index.parts.js +5 -0
- package/dist/attachments/attachments.d.ts +41 -0
- package/dist/attachments/attachments.js +91 -0
- package/dist/attachments/index.d.ts +3 -0
- package/dist/attachments/index.js +3 -0
- package/dist/attachments/index.parts.d.ts +1 -0
- package/dist/attachments/index.parts.js +5 -0
- package/dist/chip/chip.d.ts +25 -0
- package/dist/chip/chip.js +25 -0
- package/dist/chip/index.d.ts +2 -0
- package/dist/chip/index.js +2 -0
- package/dist/chip/index.parts.d.ts +1 -0
- package/dist/chip/index.parts.js +5 -0
- package/dist/chip-markdown.d.ts +25 -0
- package/dist/chip-markdown.js +52 -0
- package/dist/composer/actions.d.ts +35 -0
- package/dist/composer/actions.js +75 -0
- package/dist/composer/ask-user-machine.d.ts +52 -0
- package/dist/composer/ask-user-machine.js +164 -0
- package/dist/composer/attachments-machine.d.ts +34 -0
- package/dist/composer/attachments-machine.js +91 -0
- package/dist/composer/attachments.d.ts +16 -0
- package/dist/composer/attachments.js +32 -0
- package/dist/composer/command-list.d.ts +49 -0
- package/dist/composer/command-list.js +357 -0
- package/dist/composer/commands.d.ts +28 -0
- package/dist/composer/commands.js +56 -0
- package/dist/composer/container.d.ts +3 -0
- package/dist/composer/container.js +32 -0
- package/dist/composer/controller.d.ts +3 -0
- package/dist/composer/controller.js +6 -0
- package/dist/composer/editor-dom.d.ts +55 -0
- package/dist/composer/editor-dom.js +431 -0
- package/dist/composer/fuzzy.d.ts +4 -0
- package/dist/composer/fuzzy.js +49 -0
- package/dist/composer/index.d.ts +25 -0
- package/dist/composer/index.js +13 -0
- package/dist/composer/index.parts.d.ts +11 -0
- package/dist/composer/index.parts.js +15 -0
- package/dist/composer/internals.d.ts +29 -0
- package/dist/composer/internals.js +102 -0
- package/dist/composer/keyboard.d.ts +61 -0
- package/dist/composer/keyboard.js +75 -0
- package/dist/composer/panel.d.ts +28 -0
- package/dist/composer/panel.js +80 -0
- package/dist/composer/placeholder.d.ts +10 -0
- package/dist/composer/placeholder.js +8 -0
- package/dist/composer/popover.d.ts +12 -0
- package/dist/composer/popover.js +63 -0
- package/dist/composer/prefix-detection.d.ts +23 -0
- package/dist/composer/prefix-detection.js +58 -0
- package/dist/composer/root.d.ts +21 -0
- package/dist/composer/root.js +107 -0
- package/dist/composer/segments.d.ts +108 -0
- package/dist/composer/segments.js +366 -0
- package/dist/composer/store.d.ts +97 -0
- package/dist/composer/store.js +451 -0
- package/dist/composer/textarea.d.ts +39 -0
- package/dist/composer/textarea.js +64 -0
- package/dist/composer/trigger-tracker.d.ts +12 -0
- package/dist/composer/trigger-tracker.js +90 -0
- package/dist/composer/types.d.ts +126 -0
- package/dist/composer/types.js +4 -0
- package/dist/composer/use-composer-editor.d.ts +43 -0
- package/dist/composer/use-composer-editor.js +649 -0
- package/dist/internal/collapsible.d.ts +22 -0
- package/dist/internal/collapsible.js +90 -0
- package/dist/internal/primitive-props.d.ts +4 -0
- package/dist/internal/primitive-props.js +4 -0
- package/dist/internal/render/baseTypes.d.ts +18 -0
- package/dist/internal/render/baseTypes.js +4 -0
- package/dist/internal/render/empty.d.ts +3 -0
- package/dist/internal/render/empty.js +5 -0
- package/dist/internal/render/getReactElementRef.d.ts +5 -0
- package/dist/internal/render/getReactElementRef.js +15 -0
- package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
- package/dist/internal/render/getStateAttributesProps.js +23 -0
- package/dist/internal/render/mergeObjects.d.ts +1 -0
- package/dist/internal/render/mergeObjects.js +14 -0
- package/dist/internal/render/mergeProps.d.ts +55 -0
- package/dist/internal/render/mergeProps.js +183 -0
- package/dist/internal/render/positioning.d.ts +22 -0
- package/dist/internal/render/positioning.js +78 -0
- package/dist/internal/render/reactVersion.d.ts +3 -0
- package/dist/internal/render/reactVersion.js +7 -0
- package/dist/internal/render/resolveClassName.d.ts +8 -0
- package/dist/internal/render/resolveClassName.js +12 -0
- package/dist/internal/render/resolveStyle.d.ts +8 -0
- package/dist/internal/render/resolveStyle.js +12 -0
- package/dist/internal/render/transition.d.ts +48 -0
- package/dist/internal/render/transition.js +203 -0
- package/dist/internal/render/types.d.ts +74 -0
- package/dist/internal/render/types.js +3 -0
- package/dist/internal/render/useMergedRefs.d.ts +21 -0
- package/dist/internal/render/useMergedRefs.js +102 -0
- package/dist/internal/render/useRefWithInit.d.ts +10 -0
- package/dist/internal/render/useRefWithInit.js +12 -0
- package/dist/internal/render/useRender.d.ts +71 -0
- package/dist/internal/render/useRender.js +12 -0
- package/dist/internal/render/useRenderElement.d.ts +62 -0
- package/dist/internal/render/useRenderElement.js +157 -0
- package/dist/internal/render/warn.d.ts +1 -0
- package/dist/internal/render/warn.js +15 -0
- package/dist/internal/state-mappings.d.ts +8 -0
- package/dist/internal/state-mappings.js +17 -0
- package/dist/message/index.d.ts +3 -0
- package/dist/message/index.js +3 -0
- package/dist/message/index.parts.d.ts +1 -0
- package/dist/message/index.parts.js +5 -0
- package/dist/message/message.d.ts +43 -0
- package/dist/message/message.js +78 -0
- package/dist/message-utils.d.ts +55 -0
- package/dist/message-utils.js +79 -0
- package/dist/reasoning/index.d.ts +3 -0
- package/dist/reasoning/index.js +3 -0
- package/dist/reasoning/index.parts.d.ts +1 -0
- package/dist/reasoning/index.parts.js +5 -0
- package/dist/reasoning/reasoning.d.ts +20 -0
- package/dist/reasoning/reasoning.js +49 -0
- package/dist/steps/index.d.ts +2 -0
- package/dist/steps/index.js +2 -0
- package/dist/steps/index.parts.d.ts +1 -0
- package/dist/steps/index.parts.js +5 -0
- package/dist/steps/steps.d.ts +53 -0
- package/dist/steps/steps.js +79 -0
- package/dist/thread/geometry.d.ts +18 -0
- package/dist/thread/geometry.js +59 -0
- package/dist/thread/index.d.ts +4 -0
- package/dist/thread/index.js +3 -0
- package/dist/thread/index.parts.d.ts +1 -0
- package/dist/thread/index.parts.js +5 -0
- package/dist/thread/stores.d.ts +21 -0
- package/dist/thread/stores.js +63 -0
- package/dist/thread/thread.d.ts +66 -0
- package/dist/thread/thread.js +639 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +17 -0
- package/package.json +106 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type ChipData, type ChipSegment } from "../chip-markdown.js";
|
|
2
|
+
export type Segment = {
|
|
3
|
+
type: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "chip";
|
|
7
|
+
id: string;
|
|
8
|
+
chip: ChipData;
|
|
9
|
+
};
|
|
10
|
+
export type SegmentDoc = Segment[];
|
|
11
|
+
export type TextChange = {
|
|
12
|
+
rangeStart: number;
|
|
13
|
+
rangeEnd: number;
|
|
14
|
+
insertedLength: number;
|
|
15
|
+
};
|
|
16
|
+
export declare const nextChipId: () => string;
|
|
17
|
+
export declare const documentLength: (doc: SegmentDoc) => number;
|
|
18
|
+
/** Chips contribute nothing — the getText()/onValueChange contract. */
|
|
19
|
+
export declare const getPlainText: (doc: SegmentDoc) => string;
|
|
20
|
+
/** Chips as the sentinel char — positions stay 1:1 for diffing. */
|
|
21
|
+
export declare const toFlatText: (doc: SegmentDoc) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Chips as a single space — positions stay 1:1 for prefix detection, and the
|
|
24
|
+
* whitespace guarantees an active token can never span a chip.
|
|
25
|
+
*/
|
|
26
|
+
export declare const toScanText: (doc: SegmentDoc) => string;
|
|
27
|
+
export declare const chipRangesOf: (doc: SegmentDoc) => Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
start: number;
|
|
30
|
+
end: number;
|
|
31
|
+
}>;
|
|
32
|
+
/** The "\n"-delimited line around a position in scan text. */
|
|
33
|
+
export declare const lineBoundsAt: (scanText: string, position: number) => {
|
|
34
|
+
lineStart: number;
|
|
35
|
+
lineEnd: number;
|
|
36
|
+
};
|
|
37
|
+
export declare const segmentsFromText: (text: string) => SegmentDoc;
|
|
38
|
+
export declare const segmentsFromChipSegments: (segments: readonly ChipSegment[], makeId?: () => string) => SegmentDoc;
|
|
39
|
+
/**
|
|
40
|
+
* The {text} submit payload — text as-is, chips as chip-markdown tokens.
|
|
41
|
+
* Byte-identical to the legacy serializeDocument for equivalent content.
|
|
42
|
+
*/
|
|
43
|
+
export declare const serializeSegments: (doc: SegmentDoc) => {
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Replace the logical range [from, to) with the inserted segments. A chip is
|
|
48
|
+
* atomic: it is removed iff its position falls inside the range (range edges
|
|
49
|
+
* never split it).
|
|
50
|
+
*/
|
|
51
|
+
export declare const spliceSegments: (doc: SegmentDoc, from: number, to: number, insert: SegmentDoc) => SegmentDoc;
|
|
52
|
+
/** Extract [from, to) as a sub-document. Chips are included iff fully inside. */
|
|
53
|
+
export declare const sliceSegments: (doc: SegmentDoc, from: number, to: number) => SegmentDoc;
|
|
54
|
+
/**
|
|
55
|
+
* Fit segments into `room` logical units for maxLength: text is sliced at the
|
|
56
|
+
* boundary; a chip that doesn't fit whole is dropped entirely.
|
|
57
|
+
*/
|
|
58
|
+
export declare const truncateToFit: (insert: SegmentDoc, room: number) => SegmentDoc;
|
|
59
|
+
export declare const mapPositionResult: (position: number, change: TextChange, bias: -1 | 1) => {
|
|
60
|
+
position: number;
|
|
61
|
+
deleted: boolean;
|
|
62
|
+
};
|
|
63
|
+
export declare const mapPosition: (position: number, change: TextChange, bias: -1 | 1) => number;
|
|
64
|
+
export declare const diffFlatText: (oldFlat: string, newFlat: string, caretHint?: number) => TextChange | null;
|
|
65
|
+
export type BeforeInputPlan = {
|
|
66
|
+
kind: "native";
|
|
67
|
+
} | {
|
|
68
|
+
kind: "block";
|
|
69
|
+
} | {
|
|
70
|
+
kind: "splice";
|
|
71
|
+
from: number;
|
|
72
|
+
to: number;
|
|
73
|
+
insert: SegmentDoc;
|
|
74
|
+
caret: number;
|
|
75
|
+
};
|
|
76
|
+
export declare const planBeforeInput: (args: {
|
|
77
|
+
inputType: string;
|
|
78
|
+
data: string | null;
|
|
79
|
+
/** The event's first target range in logical positions; null when unavailable. */
|
|
80
|
+
targetRange: {
|
|
81
|
+
start: number;
|
|
82
|
+
end: number;
|
|
83
|
+
} | null;
|
|
84
|
+
doc: SegmentDoc;
|
|
85
|
+
caret: {
|
|
86
|
+
start: number;
|
|
87
|
+
end: number;
|
|
88
|
+
};
|
|
89
|
+
maxLength?: number;
|
|
90
|
+
}) => BeforeInputPlan;
|
|
91
|
+
export type SnapshotInlineNode = {
|
|
92
|
+
type: "text";
|
|
93
|
+
text: string;
|
|
94
|
+
} | {
|
|
95
|
+
type: "mentionChip";
|
|
96
|
+
attrs: {
|
|
97
|
+
prefix: string;
|
|
98
|
+
value: string;
|
|
99
|
+
label: string;
|
|
100
|
+
icon?: string;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
export type SnapshotParagraphNode = {
|
|
104
|
+
type: "paragraph";
|
|
105
|
+
content?: SnapshotInlineNode[];
|
|
106
|
+
};
|
|
107
|
+
export declare const segmentsToParagraphJSON: (doc: SegmentDoc) => SnapshotParagraphNode[];
|
|
108
|
+
export declare const segmentsFromParagraphJSON: (paragraphs: readonly SnapshotParagraphNode[], makeId?: () => string) => SegmentDoc;
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// Segments — the editor engine's pure document kernel. The document is a flat
|
|
2
|
+
// inline sequence: text segments (soft breaks ride inline as "\n") and atomic
|
|
3
|
+
// chips of logical length 1. Everything here is pure and position-based —
|
|
4
|
+
// logical positions count one per text character and one per chip — so the
|
|
5
|
+
// DOM layer, the trigger tracker, and the tests all speak the same integers.
|
|
6
|
+
//
|
|
7
|
+
// Projections: the same document reads three ways depending on the consumer —
|
|
8
|
+
// getPlainText (chips vanish; the getText/onValueChange contract),
|
|
9
|
+
// toFlatText (chips as ""; input diffing needs 1:1 positions),
|
|
10
|
+
// toScanText (chips as " "; prefix detection needs word boundaries and the
|
|
11
|
+
// guarantee that a token can never span a chip).
|
|
12
|
+
import { encodeChipMarkdown } from "../chip-markdown.js";
|
|
13
|
+
// The character standing in for a chip in the diff projection. Object
|
|
14
|
+
// Replacement Character — never produced by real typing.
|
|
15
|
+
const CHIP_SENTINEL = "";
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Chip ids — monotonically increasing per module load. Deterministic (no
|
|
18
|
+
// randomness), unique within a session, and test-friendly.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
let chipIdCounter = 0;
|
|
21
|
+
export const nextChipId = () => `chip-${++chipIdCounter}`;
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Projections & measurements
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export const documentLength = (doc) => doc.reduce((length, segment) => length + (segment.type === "text" ? segment.text.length : 1), 0);
|
|
26
|
+
/** Chips contribute nothing — the getText()/onValueChange contract. */
|
|
27
|
+
export const getPlainText = (doc) => doc.reduce((text, segment) => (segment.type === "text" ? text + segment.text : text), "");
|
|
28
|
+
/** Chips as the sentinel char — positions stay 1:1 for diffing. */
|
|
29
|
+
export const toFlatText = (doc) => doc.reduce((text, segment) => text + (segment.type === "text" ? segment.text : CHIP_SENTINEL), "");
|
|
30
|
+
/**
|
|
31
|
+
* Chips as a single space — positions stay 1:1 for prefix detection, and the
|
|
32
|
+
* whitespace guarantees an active token can never span a chip.
|
|
33
|
+
*/
|
|
34
|
+
export const toScanText = (doc) => doc.reduce((text, segment) => text + (segment.type === "text" ? segment.text : " "), "");
|
|
35
|
+
export const chipRangesOf = (doc) => {
|
|
36
|
+
const ranges = [];
|
|
37
|
+
let offset = 0;
|
|
38
|
+
for (const segment of doc) {
|
|
39
|
+
if (segment.type === "chip") {
|
|
40
|
+
ranges.push({ id: segment.id, start: offset, end: offset + 1 });
|
|
41
|
+
offset += 1;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
offset += segment.text.length;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return ranges;
|
|
48
|
+
};
|
|
49
|
+
/** The "\n"-delimited line around a position in scan text. */
|
|
50
|
+
export const lineBoundsAt = (scanText, position) => {
|
|
51
|
+
const lineStart = scanText.lastIndexOf("\n", Math.max(0, position - 1)) + 1;
|
|
52
|
+
const newlineAfter = scanText.indexOf("\n", position);
|
|
53
|
+
return { lineStart, lineEnd: newlineAfter === -1 ? scanText.length : newlineAfter };
|
|
54
|
+
};
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Construction & serialization
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
export const segmentsFromText = (text) => text.length === 0 ? [] : [{ type: "text", text }];
|
|
59
|
+
export const segmentsFromChipSegments = (segments, makeId = nextChipId) => normalizeSegments(segments.map((segment) => segment.type === "text"
|
|
60
|
+
? { type: "text", text: segment.text }
|
|
61
|
+
: {
|
|
62
|
+
type: "chip",
|
|
63
|
+
id: makeId(),
|
|
64
|
+
chip: {
|
|
65
|
+
prefix: segment.prefix,
|
|
66
|
+
value: segment.value,
|
|
67
|
+
label: segment.label,
|
|
68
|
+
...(segment.icon ? { icon: segment.icon } : {}),
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
71
|
+
/**
|
|
72
|
+
* The {text} submit payload — text as-is, chips as chip-markdown tokens.
|
|
73
|
+
* Byte-identical to the legacy serializeDocument for equivalent content.
|
|
74
|
+
*/
|
|
75
|
+
export const serializeSegments = (doc) => ({
|
|
76
|
+
text: doc.reduce((text, segment) => text + (segment.type === "text" ? segment.text : encodeChipMarkdown(segment.chip)), ""),
|
|
77
|
+
});
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Splice & slice — all document surgery goes through these, so text merging
|
|
80
|
+
// and empty-segment dropping happen in exactly one place.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/** Merge adjacent text segments and drop empty ones. */
|
|
83
|
+
const normalizeSegments = (doc) => {
|
|
84
|
+
const normalized = [];
|
|
85
|
+
for (const segment of doc) {
|
|
86
|
+
if (segment.type === "text" && segment.text.length === 0)
|
|
87
|
+
continue;
|
|
88
|
+
const previous = normalized.at(-1);
|
|
89
|
+
if (segment.type === "text" && previous?.type === "text") {
|
|
90
|
+
normalized[normalized.length - 1] = { type: "text", text: previous.text + segment.text };
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
normalized.push(segment);
|
|
94
|
+
}
|
|
95
|
+
return normalized;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Replace the logical range [from, to) with the inserted segments. A chip is
|
|
99
|
+
* atomic: it is removed iff its position falls inside the range (range edges
|
|
100
|
+
* never split it).
|
|
101
|
+
*/
|
|
102
|
+
export const spliceSegments = (doc, from, to, insert) => {
|
|
103
|
+
const before = [];
|
|
104
|
+
const after = [];
|
|
105
|
+
let offset = 0;
|
|
106
|
+
for (const segment of doc) {
|
|
107
|
+
const segmentLength = segment.type === "text" ? segment.text.length : 1;
|
|
108
|
+
const segmentStart = offset;
|
|
109
|
+
const segmentEnd = offset + segmentLength;
|
|
110
|
+
offset = segmentEnd;
|
|
111
|
+
if (segmentEnd <= from) {
|
|
112
|
+
before.push(segment);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (segmentStart >= to) {
|
|
116
|
+
after.push(segment);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Overlapping the range. Chips are atomic — fully covered, fully removed.
|
|
120
|
+
if (segment.type === "chip")
|
|
121
|
+
continue;
|
|
122
|
+
const keepLeft = segment.text.slice(0, Math.max(0, from - segmentStart));
|
|
123
|
+
const keepRight = segment.text.slice(Math.min(segmentLength, to - segmentStart));
|
|
124
|
+
if (keepLeft)
|
|
125
|
+
before.push({ type: "text", text: keepLeft });
|
|
126
|
+
if (keepRight)
|
|
127
|
+
after.push({ type: "text", text: keepRight });
|
|
128
|
+
}
|
|
129
|
+
return normalizeSegments([...before, ...insert, ...after]);
|
|
130
|
+
};
|
|
131
|
+
/** Extract [from, to) as a sub-document. Chips are included iff fully inside. */
|
|
132
|
+
export const sliceSegments = (doc, from, to) => {
|
|
133
|
+
const slice = [];
|
|
134
|
+
let offset = 0;
|
|
135
|
+
for (const segment of doc) {
|
|
136
|
+
const segmentLength = segment.type === "text" ? segment.text.length : 1;
|
|
137
|
+
const segmentStart = offset;
|
|
138
|
+
const segmentEnd = offset + segmentLength;
|
|
139
|
+
offset = segmentEnd;
|
|
140
|
+
if (segmentEnd <= from || segmentStart >= to)
|
|
141
|
+
continue;
|
|
142
|
+
if (segment.type === "chip") {
|
|
143
|
+
slice.push(segment);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const text = segment.text.slice(Math.max(0, from - segmentStart), Math.min(segmentLength, to - segmentStart));
|
|
147
|
+
if (text)
|
|
148
|
+
slice.push({ type: "text", text });
|
|
149
|
+
}
|
|
150
|
+
return normalizeSegments(slice);
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Fit segments into `room` logical units for maxLength: text is sliced at the
|
|
154
|
+
* boundary; a chip that doesn't fit whole is dropped entirely.
|
|
155
|
+
*/
|
|
156
|
+
export const truncateToFit = (insert, room) => {
|
|
157
|
+
const fitted = [];
|
|
158
|
+
let remaining = Math.max(0, room);
|
|
159
|
+
for (const segment of insert) {
|
|
160
|
+
if (remaining === 0)
|
|
161
|
+
break;
|
|
162
|
+
if (segment.type === "chip") {
|
|
163
|
+
fitted.push(segment);
|
|
164
|
+
remaining -= 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const text = segment.text.slice(0, remaining);
|
|
168
|
+
if (text)
|
|
169
|
+
fitted.push({ type: "text", text });
|
|
170
|
+
remaining -= text.length;
|
|
171
|
+
}
|
|
172
|
+
return normalizeSegments(fitted);
|
|
173
|
+
};
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Position mapping — carry a position across one change.
|
|
176
|
+
// The tracker's sticky token rests on the bias semantics: insertion exactly
|
|
177
|
+
// at a position stays before it with bias -1 and moves after it with bias +1,
|
|
178
|
+
// which is what lets the token end grow while its start holds.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
export const mapPositionResult = (position, change, bias) => {
|
|
181
|
+
const { rangeStart, rangeEnd, insertedLength } = change;
|
|
182
|
+
if (position < rangeStart)
|
|
183
|
+
return { position, deleted: false };
|
|
184
|
+
if (position > rangeEnd) {
|
|
185
|
+
return { position: position + insertedLength - (rangeEnd - rangeStart), deleted: false };
|
|
186
|
+
}
|
|
187
|
+
if (position === rangeStart) {
|
|
188
|
+
return bias < 0
|
|
189
|
+
? { position: rangeStart, deleted: false }
|
|
190
|
+
: { position: rangeStart + insertedLength, deleted: false };
|
|
191
|
+
}
|
|
192
|
+
if (position === rangeEnd) {
|
|
193
|
+
return {
|
|
194
|
+
position: rangeStart + insertedLength,
|
|
195
|
+
deleted: bias < 0 && rangeEnd > rangeStart,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// Strictly inside the replaced range — the content at this position is gone.
|
|
199
|
+
return {
|
|
200
|
+
position: bias < 0 ? rangeStart : rangeStart + insertedLength,
|
|
201
|
+
deleted: true,
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
export const mapPosition = (position, change, bias) => mapPositionResult(position, change, bias).position;
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Diffing — recover the TextChange from a native edit (there is no transaction
|
|
207
|
+
// to consume; the browser already mutated the DOM). Common prefix/suffix diff;
|
|
208
|
+
// the caret disambiguates repeated-character runs (prefer the edit that ends
|
|
209
|
+
// at the caret, à la CodeMirror).
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
export const diffFlatText = (oldFlat, newFlat, caretHint) => {
|
|
212
|
+
if (oldFlat === newFlat)
|
|
213
|
+
return null;
|
|
214
|
+
let prefix = 0;
|
|
215
|
+
const maxPrefix = Math.min(oldFlat.length, newFlat.length);
|
|
216
|
+
while (prefix < maxPrefix && oldFlat[prefix] === newFlat[prefix])
|
|
217
|
+
prefix++;
|
|
218
|
+
let suffix = 0;
|
|
219
|
+
const maxSuffix = Math.min(oldFlat.length, newFlat.length) - prefix;
|
|
220
|
+
while (suffix < maxSuffix &&
|
|
221
|
+
oldFlat[oldFlat.length - 1 - suffix] === newFlat[newFlat.length - 1 - suffix]) {
|
|
222
|
+
suffix++;
|
|
223
|
+
}
|
|
224
|
+
let rangeStart = prefix;
|
|
225
|
+
let rangeEnd = oldFlat.length - suffix;
|
|
226
|
+
const insertedLength = newFlat.length - suffix - prefix;
|
|
227
|
+
// Slide the window left over equal characters until the inserted region
|
|
228
|
+
// ends at the caret ("aa" + typed "a": rightmost by default, caret decides).
|
|
229
|
+
if (caretHint !== undefined) {
|
|
230
|
+
while (rangeStart + insertedLength > caretHint &&
|
|
231
|
+
rangeStart > 0 &&
|
|
232
|
+
oldFlat[rangeStart - 1] === newFlat[rangeStart + insertedLength - 1]) {
|
|
233
|
+
rangeStart--;
|
|
234
|
+
rangeEnd--;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { rangeStart, rangeEnd, insertedLength };
|
|
238
|
+
};
|
|
239
|
+
// Text-level inputTypes the browser may perform natively (subject to the
|
|
240
|
+
// chip-intersection and maxLength checks below).
|
|
241
|
+
const NATIVE_SAFE_INPUT_TYPES = new Set([
|
|
242
|
+
"insertText",
|
|
243
|
+
"insertReplacementText",
|
|
244
|
+
"deleteContentBackward",
|
|
245
|
+
"deleteContentForward",
|
|
246
|
+
"deleteWordBackward",
|
|
247
|
+
"deleteWordForward",
|
|
248
|
+
"deleteSoftLineBackward",
|
|
249
|
+
"deleteHardLineBackward",
|
|
250
|
+
"deleteEntireSoftLine",
|
|
251
|
+
"deleteContent",
|
|
252
|
+
"deleteByCut",
|
|
253
|
+
]);
|
|
254
|
+
const isInsertType = (inputType) => inputType === "insertText" || inputType === "insertReplacementText";
|
|
255
|
+
export const planBeforeInput = (args) => {
|
|
256
|
+
const { inputType, data, doc, maxLength } = args;
|
|
257
|
+
const range = args.targetRange ?? args.caret;
|
|
258
|
+
// IME composition is not cancelable — the composition guard owns it.
|
|
259
|
+
if (inputType === "insertCompositionText")
|
|
260
|
+
return { kind: "native" };
|
|
261
|
+
// Enter/Shift+Enter backstop (keydown normally consumes these first):
|
|
262
|
+
// emulate as a soft break through the programmatic pipeline.
|
|
263
|
+
if (inputType === "insertParagraph" || inputType === "insertLineBreak") {
|
|
264
|
+
return planTextInsertion(doc, range, "\n", maxLength);
|
|
265
|
+
}
|
|
266
|
+
if (!NATIVE_SAFE_INPUT_TYPES.has(inputType))
|
|
267
|
+
return { kind: "block" };
|
|
268
|
+
const rangeTouchesChip = chipRangesOf(doc).some((chip) => chip.start < range.end && chip.end > range.start);
|
|
269
|
+
if (isInsertType(inputType)) {
|
|
270
|
+
const insertedText = data ?? "";
|
|
271
|
+
// Chip in the replaced selection (select-all + type-over) or over the cap:
|
|
272
|
+
// do the splice ourselves. Otherwise the browser handles it.
|
|
273
|
+
const over = maxLength !== undefined &&
|
|
274
|
+
documentLength(doc) - (range.end - range.start) + insertedText.length > maxLength;
|
|
275
|
+
if (!rangeTouchesChip && !over)
|
|
276
|
+
return { kind: "native" };
|
|
277
|
+
return planTextInsertion(doc, range, insertedText, maxLength);
|
|
278
|
+
}
|
|
279
|
+
// Delete variants: native unless the target range touches a chip — engines
|
|
280
|
+
// disagree on atom deletion (WebKit caret placement, word-deletes across
|
|
281
|
+
// atoms), so chip deletion always goes through the model.
|
|
282
|
+
if (!rangeTouchesChip)
|
|
283
|
+
return { kind: "native" };
|
|
284
|
+
return { kind: "splice", from: range.start, to: range.end, insert: [], caret: range.start };
|
|
285
|
+
};
|
|
286
|
+
const planTextInsertion = (doc, range, text, maxLength) => {
|
|
287
|
+
let insertedText = text;
|
|
288
|
+
if (maxLength !== undefined) {
|
|
289
|
+
const room = maxLength - (documentLength(doc) - (range.end - range.start));
|
|
290
|
+
if (room <= 0)
|
|
291
|
+
return { kind: "block" };
|
|
292
|
+
insertedText = insertedText.slice(0, room);
|
|
293
|
+
}
|
|
294
|
+
if (insertedText.length === 0)
|
|
295
|
+
return { kind: "block" };
|
|
296
|
+
return {
|
|
297
|
+
kind: "splice",
|
|
298
|
+
from: range.start,
|
|
299
|
+
to: range.end,
|
|
300
|
+
insert: [{ type: "text", text: insertedText }],
|
|
301
|
+
caret: range.start + insertedText.length,
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
export const segmentsToParagraphJSON = (doc) => {
|
|
305
|
+
const paragraphs = [{ type: "paragraph", content: [] }];
|
|
306
|
+
const pushInline = (node) => {
|
|
307
|
+
const target = paragraphs.at(-1);
|
|
308
|
+
if (!target)
|
|
309
|
+
return;
|
|
310
|
+
target.content = target.content ?? [];
|
|
311
|
+
target.content.push(node);
|
|
312
|
+
};
|
|
313
|
+
for (const segment of doc) {
|
|
314
|
+
if (segment.type === "chip") {
|
|
315
|
+
pushInline({
|
|
316
|
+
type: "mentionChip",
|
|
317
|
+
attrs: {
|
|
318
|
+
prefix: segment.chip.prefix,
|
|
319
|
+
value: segment.chip.value,
|
|
320
|
+
label: segment.chip.label,
|
|
321
|
+
...(segment.chip.icon ? { icon: segment.chip.icon } : {}),
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const lines = segment.text.split("\n");
|
|
327
|
+
lines.forEach((line, lineIndex) => {
|
|
328
|
+
if (lineIndex > 0)
|
|
329
|
+
paragraphs.push({ type: "paragraph", content: [] });
|
|
330
|
+
if (line.length > 0)
|
|
331
|
+
pushInline({ type: "text", text: line });
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// Empty paragraphs stay — they encode blank lines ("\n\n"), and dropping
|
|
335
|
+
// them would make applySnapshot(getSnapshot()) lossy. (The legacy paste
|
|
336
|
+
// path filters them; the snapshot path must not.)
|
|
337
|
+
return paragraphs;
|
|
338
|
+
};
|
|
339
|
+
export const segmentsFromParagraphJSON = (paragraphs, makeId = nextChipId) => {
|
|
340
|
+
const doc = [];
|
|
341
|
+
paragraphs.forEach((paragraph, paragraphIndex) => {
|
|
342
|
+
if (paragraphIndex > 0)
|
|
343
|
+
doc.push({ type: "text", text: "\n" });
|
|
344
|
+
for (const node of paragraph.content ?? []) {
|
|
345
|
+
if (node.type === "text") {
|
|
346
|
+
doc.push({ type: "text", text: node.text });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// A legacy-engine snapshot may carry node types we don't model
|
|
350
|
+
// (hardBreak etc.) — only mention chips convert; the rest are skipped.
|
|
351
|
+
if (node.type !== "mentionChip")
|
|
352
|
+
continue;
|
|
353
|
+
doc.push({
|
|
354
|
+
type: "chip",
|
|
355
|
+
id: makeId(),
|
|
356
|
+
chip: {
|
|
357
|
+
prefix: node.attrs.prefix,
|
|
358
|
+
value: node.attrs.value,
|
|
359
|
+
label: node.attrs.label,
|
|
360
|
+
...(node.attrs.icon ? { icon: node.attrs.icon } : {}),
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
return normalizeSegments(doc);
|
|
366
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import type { AskUserOptionsHandle } from "../ask-user/index.js";
|
|
3
|
+
import { type AttachmentErrorCode, type AttachmentItem } from "../attachments/index.js";
|
|
4
|
+
import { type AnswerEntry } from "./ask-user-machine.js";
|
|
5
|
+
import { type AttachmentStoreConfig } from "./attachments-machine.js";
|
|
6
|
+
import type { AskUserQuestion, ComposerAnswerEntry, ComposerEditorHandle, RegisteredEditor } from "./types.js";
|
|
7
|
+
export type ComposerEditorState = ComposerEditorHandle & {
|
|
8
|
+
getText: () => string;
|
|
9
|
+
setText: (text: string) => void;
|
|
10
|
+
serialize: () => {
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
ensureFocus: () => void;
|
|
14
|
+
};
|
|
15
|
+
export type ComposerAttachmentsState = {
|
|
16
|
+
items: AttachmentItem[];
|
|
17
|
+
add: (files: File[] | FileList) => void;
|
|
18
|
+
remove: (id: string) => void;
|
|
19
|
+
openFileDialog: () => void;
|
|
20
|
+
error: AttachmentErrorCode | null;
|
|
21
|
+
isDragging: boolean;
|
|
22
|
+
fileInputRef: RefObject<HTMLInputElement | null>;
|
|
23
|
+
globalDropRef: RefObject<boolean>;
|
|
24
|
+
};
|
|
25
|
+
export type ComposerAskUserState = ComposerPanelSlice & {
|
|
26
|
+
questions: AskUserQuestion[] | null;
|
|
27
|
+
step: number;
|
|
28
|
+
answers: Map<number, AnswerEntry>;
|
|
29
|
+
toggleOption: (label: string) => void;
|
|
30
|
+
continueStep: (freeText?: string) => void;
|
|
31
|
+
dismissStep: () => void;
|
|
32
|
+
isLastStep: boolean;
|
|
33
|
+
isSingle: boolean;
|
|
34
|
+
clearSelections: () => void;
|
|
35
|
+
goBack: () => void;
|
|
36
|
+
goNext: () => void;
|
|
37
|
+
optionsRef: RefObject<AskUserOptionsHandle | null>;
|
|
38
|
+
};
|
|
39
|
+
export type ComposerPanelSlice = {
|
|
40
|
+
active: boolean;
|
|
41
|
+
present: boolean;
|
|
42
|
+
};
|
|
43
|
+
export type ComposerCommandsState = ComposerPanelSlice & {
|
|
44
|
+
trigger: string | null;
|
|
45
|
+
query: string;
|
|
46
|
+
highlightIndex: number;
|
|
47
|
+
/** DOM id of the highlighted option row — the editor's aria-activedescendant. Written by the mounted Command as the highlight resolves. */
|
|
48
|
+
activeOptionId: string | null;
|
|
49
|
+
};
|
|
50
|
+
export type ComposerState = {
|
|
51
|
+
textarea: {
|
|
52
|
+
hasContent: boolean;
|
|
53
|
+
};
|
|
54
|
+
isSubmitting: boolean;
|
|
55
|
+
commands: ComposerCommandsState;
|
|
56
|
+
attachments: ComposerAttachmentsState;
|
|
57
|
+
askUser: ComposerAskUserState;
|
|
58
|
+
};
|
|
59
|
+
export type ComposerStore = {
|
|
60
|
+
subscribe: (listener: () => void) => () => void;
|
|
61
|
+
getSnapshot: () => ComposerState;
|
|
62
|
+
setHasContent: (value: boolean) => void;
|
|
63
|
+
setIsSubmitting: (value: boolean) => void;
|
|
64
|
+
setCommands: (next: {
|
|
65
|
+
active: boolean;
|
|
66
|
+
trigger: string | null;
|
|
67
|
+
query: string;
|
|
68
|
+
}) => void;
|
|
69
|
+
setActiveOptionId: (id: string | null) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Stable per-instance id for the command listbox — the editor references it
|
|
72
|
+
* via aria-controls; option rows derive their ids from it. Assigned by the
|
|
73
|
+
* mounting Composer.Root from React's useId (SSR-stable); the factory can't
|
|
74
|
+
* mint it because explicit handles are created outside React.
|
|
75
|
+
*/
|
|
76
|
+
listboxId: string;
|
|
77
|
+
moveHighlight: (direction: number) => void;
|
|
78
|
+
setHighlight: (index: number) => void;
|
|
79
|
+
setQuestions: (questions: AskUserQuestion[] | null) => void;
|
|
80
|
+
setDragging: (active: boolean) => void;
|
|
81
|
+
resetAttachments: () => void;
|
|
82
|
+
activateAskUser: () => () => void;
|
|
83
|
+
finalizePanelClose: () => void;
|
|
84
|
+
reset: () => void;
|
|
85
|
+
editorRef: RefObject<RegisteredEditor | null>;
|
|
86
|
+
controller: ComposerEditorState;
|
|
87
|
+
registerEditor: (editor: RegisteredEditor) => () => void;
|
|
88
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
89
|
+
attachmentConfigRef: RefObject<AttachmentStoreConfig>;
|
|
90
|
+
submitAnswersRef: RefObject<((answers: ComposerAnswerEntry[]) => void) | null>;
|
|
91
|
+
commandSelectRef: RefObject<(() => void) | null>;
|
|
92
|
+
};
|
|
93
|
+
export declare const createComposerStore: () => ComposerStore;
|
|
94
|
+
export declare const ComposerStoreContext: import("react").Context<ComposerStore | null>;
|
|
95
|
+
export declare const useComposerContextStore: () => ComposerStore;
|
|
96
|
+
export declare const useComposerStore: <Selected = ComposerState>(store: ComposerStore, selector?: (composer: ComposerState) => Selected) => Selected;
|
|
97
|
+
export declare const useComposer: <Selected = ComposerState>(selector?: (composer: ComposerState) => Selected) => Selected;
|