@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,431 @@
|
|
|
1
|
+
// Editor DOM — the adapters between the contenteditable element and the flat
|
|
2
|
+
// segment model. The reader and position mapping are pure (they walk a
|
|
3
|
+
// minimal structural node view, so tests need no DOM); the materializers at
|
|
4
|
+
// the bottom are thin real-DOM wrappers over the pure spec builder.
|
|
5
|
+
//
|
|
6
|
+
// Writer/reader pact for line breaks: every "\n" renders as <br>, plus one
|
|
7
|
+
// padding <br> when the document is empty or ends with "\n" (an unpadded
|
|
8
|
+
// trailing line is unreachable/zero-height). The reader inverts this by
|
|
9
|
+
// dropping exactly one trailing "\n" when the last rendered node was a <br>.
|
|
10
|
+
// Round-trips: "" ↔ <br>, "a\n" ↔ a<br><br>, "a\n\n" ↔ a<br><br><br>.
|
|
11
|
+
import { nextChipId } from "./segments.js";
|
|
12
|
+
const TEXT_NODE = 3;
|
|
13
|
+
const ELEMENT_NODE = 1;
|
|
14
|
+
// Elements whose appearance inside the editable means something rewrote our
|
|
15
|
+
// flat structure (paste leftovers, extensions) — content still reads, but the
|
|
16
|
+
// document gets renormalized from the model afterwards.
|
|
17
|
+
const BLOCK_NODE_NAMES = new Set([
|
|
18
|
+
"DIV",
|
|
19
|
+
"P",
|
|
20
|
+
"PRE",
|
|
21
|
+
"BLOCKQUOTE",
|
|
22
|
+
"UL",
|
|
23
|
+
"OL",
|
|
24
|
+
"LI",
|
|
25
|
+
"H1",
|
|
26
|
+
"H2",
|
|
27
|
+
"H3",
|
|
28
|
+
"H4",
|
|
29
|
+
"H5",
|
|
30
|
+
"H6",
|
|
31
|
+
"TABLE",
|
|
32
|
+
]);
|
|
33
|
+
export const toReadable = (node) => node;
|
|
34
|
+
const chipIdOf = (node) => node.nodeType === ELEMENT_NODE ? (node.getAttribute?.("data-chip-id") ?? null) : null;
|
|
35
|
+
// Non-editable elements without a chip identity are pure presentation (the
|
|
36
|
+
// badge's hint span) — invisible to the model: the reader skips them and the
|
|
37
|
+
// position mappers give them zero width.
|
|
38
|
+
const isPresentationOnly = (node) => node.nodeType === ELEMENT_NODE &&
|
|
39
|
+
node.getAttribute?.("contenteditable") === "false" &&
|
|
40
|
+
chipIdOf(node) === null;
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Reader — DOM → segments. Adjacent text concatenates (fragmentation is
|
|
43
|
+
// normalized logically, never via root.normalize() — WebKit collapses the
|
|
44
|
+
// caret when the selection's text node gets merged), NBSP reads as a plain
|
|
45
|
+
// space, badge spans and unknown inline elements are transparent wrappers,
|
|
46
|
+
// chip spans resolve through the registry.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export const readDocumentFromDom = (root, resolveChip, makeId = nextChipId) => {
|
|
49
|
+
const doc = [];
|
|
50
|
+
let dirty = false;
|
|
51
|
+
let pendingText = "";
|
|
52
|
+
let lastWasBreak = false;
|
|
53
|
+
const seenChipIds = new Set();
|
|
54
|
+
const flushText = () => {
|
|
55
|
+
if (pendingText.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const previous = doc.at(-1);
|
|
58
|
+
if (previous?.type === "text") {
|
|
59
|
+
doc[doc.length - 1] = { type: "text", text: previous.text + pendingText };
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
doc.push({ type: "text", text: pendingText });
|
|
63
|
+
}
|
|
64
|
+
pendingText = "";
|
|
65
|
+
};
|
|
66
|
+
const visit = (node) => {
|
|
67
|
+
if (node.nodeType === TEXT_NODE) {
|
|
68
|
+
const content = (node.data ?? "").replace(/\u00A0/g, " ");
|
|
69
|
+
if (content.length > 0) {
|
|
70
|
+
pendingText += content;
|
|
71
|
+
lastWasBreak = false;
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (node.nodeType !== ELEMENT_NODE)
|
|
76
|
+
return;
|
|
77
|
+
if (node.nodeName === "BR") {
|
|
78
|
+
pendingText += "\n";
|
|
79
|
+
lastWasBreak = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const chipId = chipIdOf(node);
|
|
83
|
+
if (chipId !== null) {
|
|
84
|
+
// Chip span content is presentation — never read. Unknown ids are
|
|
85
|
+
// leftovers from DOM the model doesn't know (mark dirty, skip);
|
|
86
|
+
// duplicated ids (clone paths) get a fresh identity.
|
|
87
|
+
const chip = resolveChip(chipId);
|
|
88
|
+
if (!chip) {
|
|
89
|
+
dirty = true;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
flushText();
|
|
93
|
+
const id = seenChipIds.has(chipId) ? makeId() : chipId;
|
|
94
|
+
seenChipIds.add(chipId);
|
|
95
|
+
doc.push({ type: "chip", id, chip });
|
|
96
|
+
lastWasBreak = false;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Presentation-only elements (the badge's hint span) never reach the model.
|
|
100
|
+
if (isPresentationOnly(node))
|
|
101
|
+
return;
|
|
102
|
+
// Badge span / unknown inline: transparent wrapper. Block elements also
|
|
103
|
+
// read through, but flag the document for renormalization.
|
|
104
|
+
if (BLOCK_NODE_NAMES.has(node.nodeName))
|
|
105
|
+
dirty = true;
|
|
106
|
+
for (const child of Array.from(node.childNodes))
|
|
107
|
+
visit(child);
|
|
108
|
+
};
|
|
109
|
+
for (const child of Array.from(root.childNodes))
|
|
110
|
+
visit(child);
|
|
111
|
+
// Invert the writer's padding <br>: one trailing "\n" is the padding, not
|
|
112
|
+
// content — but only when a <br> was actually the last rendered node.
|
|
113
|
+
if (lastWasBreak && pendingText.endsWith("\n")) {
|
|
114
|
+
pendingText = pendingText.slice(0, -1);
|
|
115
|
+
}
|
|
116
|
+
flushText();
|
|
117
|
+
return { doc, dirty };
|
|
118
|
+
};
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Position mapping — DOM points ↔ logical positions. Chips count 1, <br>
|
|
121
|
+
// counts 1 (it renders a "\n"), badge/unknown wrappers are transparent.
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
/** Logical position of (node, offset), or null when the point isn't inside root. */
|
|
124
|
+
export const logicalRangeFromDom = (root, targetNode, targetOffset) => {
|
|
125
|
+
let position = 0;
|
|
126
|
+
let found = null;
|
|
127
|
+
const visit = (node) => {
|
|
128
|
+
if (node === targetNode && node.nodeType !== TEXT_NODE && chipIdOf(node) === null) {
|
|
129
|
+
// Element point: offset is a child index — resolve by walking children
|
|
130
|
+
// until the index, then record.
|
|
131
|
+
const children = Array.from(node.childNodes).slice(0, targetOffset);
|
|
132
|
+
for (const child of children) {
|
|
133
|
+
if (measure(child))
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
found = position;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (node.nodeType === TEXT_NODE) {
|
|
140
|
+
if (node === targetNode) {
|
|
141
|
+
found = position + Math.min(targetOffset, (node.data ?? "").length);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
position += (node.data ?? "").length;
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (node.nodeType !== ELEMENT_NODE)
|
|
148
|
+
return false;
|
|
149
|
+
if (node.nodeName === "BR") {
|
|
150
|
+
position += 1;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (chipIdOf(node) !== null) {
|
|
154
|
+
// A point inside a chip clamps to the chip's start.
|
|
155
|
+
if (node === targetNode || contains(node, targetNode)) {
|
|
156
|
+
found = position;
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
position += 1;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
// Presentation-only elements are zero-width; a point inside one clamps to
|
|
163
|
+
// its boundary.
|
|
164
|
+
if (isPresentationOnly(node)) {
|
|
165
|
+
if (node === targetNode || contains(node, targetNode)) {
|
|
166
|
+
found = position;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
for (const child of Array.from(node.childNodes)) {
|
|
172
|
+
if (visit(child))
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
// Advance `position` past a whole subtree (used for element-point offsets).
|
|
178
|
+
const measure = (node) => visit(node);
|
|
179
|
+
const contains = (parent, target) => {
|
|
180
|
+
for (const child of Array.from(parent.childNodes)) {
|
|
181
|
+
if (child === target || contains(child, target))
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
};
|
|
186
|
+
if (root === targetNode) {
|
|
187
|
+
const children = Array.from(root.childNodes).slice(0, targetOffset);
|
|
188
|
+
for (const child of children) {
|
|
189
|
+
if (measure(child))
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
return position;
|
|
193
|
+
}
|
|
194
|
+
for (const child of Array.from(root.childNodes)) {
|
|
195
|
+
if (visit(child))
|
|
196
|
+
return found;
|
|
197
|
+
}
|
|
198
|
+
return found;
|
|
199
|
+
};
|
|
200
|
+
export const documentToDomSpec = (doc) => {
|
|
201
|
+
const specs = [];
|
|
202
|
+
for (const segment of doc) {
|
|
203
|
+
if (segment.type === "chip") {
|
|
204
|
+
specs.push({ kind: "chip", id: segment.id });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const lines = segment.text.split("\n");
|
|
208
|
+
lines.forEach((line, lineIndex) => {
|
|
209
|
+
if (lineIndex > 0)
|
|
210
|
+
specs.push({ kind: "br", padding: false });
|
|
211
|
+
if (line.length > 0)
|
|
212
|
+
specs.push({ kind: "text", text: line });
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// Padding <br>: an empty document or one ending in a line break needs a
|
|
216
|
+
// trailing <br> for the last line to be reachable and have height.
|
|
217
|
+
const last = specs.at(-1);
|
|
218
|
+
if (!last || last.kind === "br")
|
|
219
|
+
specs.push({ kind: "br", padding: true });
|
|
220
|
+
return specs;
|
|
221
|
+
};
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Real-DOM materializers — thin, untested by design; all decisions live in
|
|
224
|
+
// the pure layers above.
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
const createChipSpan = (id) => {
|
|
227
|
+
const span = document.createElement("span");
|
|
228
|
+
span.contentEditable = "false";
|
|
229
|
+
span.setAttribute("data-mention-chip", "");
|
|
230
|
+
span.setAttribute("data-chip-id", id);
|
|
231
|
+
// A chip is atomic inline content: it must sit on the text baseline and must
|
|
232
|
+
// not inherit the editor's break-spaces whitespace mode, or its label wraps
|
|
233
|
+
// mid-chip.
|
|
234
|
+
span.style.display = "inline";
|
|
235
|
+
span.style.verticalAlign = "baseline";
|
|
236
|
+
span.style.whiteSpace = "normal";
|
|
237
|
+
return span;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Render the canonical child list. Existing chip spans are reused by id —
|
|
241
|
+
* moving a node preserves its React portal; recreating it would remount the
|
|
242
|
+
* chip content. Returns the live id → element map for the portal layer.
|
|
243
|
+
*/
|
|
244
|
+
export const renderDocumentToDom = (root, doc) => {
|
|
245
|
+
const existingSpans = new Map();
|
|
246
|
+
for (const span of root.querySelectorAll("[data-chip-id]")) {
|
|
247
|
+
const id = span.getAttribute("data-chip-id");
|
|
248
|
+
if (id && !existingSpans.has(id))
|
|
249
|
+
existingSpans.set(id, span);
|
|
250
|
+
}
|
|
251
|
+
const chipsById = new Map(doc.flatMap((segment) => segment.type === "chip" ? [[segment.id, segment.chip]] : []));
|
|
252
|
+
const chipElements = new Map();
|
|
253
|
+
const children = documentToDomSpec(doc).map((spec) => {
|
|
254
|
+
if (spec.kind === "text")
|
|
255
|
+
return document.createTextNode(spec.text);
|
|
256
|
+
if (spec.kind === "br")
|
|
257
|
+
return document.createElement("br");
|
|
258
|
+
const span = existingSpans.get(spec.id) ?? createChipSpan(spec.id);
|
|
259
|
+
// Without an accessible boundary the chip reads as bare prose inside the
|
|
260
|
+
// textbox — label + type gives AT an atomic token ("Rasmus, @ mention").
|
|
261
|
+
const chip = chipsById.get(spec.id);
|
|
262
|
+
if (chip)
|
|
263
|
+
span.setAttribute("aria-label", `${chip.label}, ${chip.prefix} mention`);
|
|
264
|
+
chipElements.set(spec.id, span);
|
|
265
|
+
return span;
|
|
266
|
+
});
|
|
267
|
+
root.replaceChildren(...children);
|
|
268
|
+
return chipElements;
|
|
269
|
+
};
|
|
270
|
+
/** DOM point for a logical position, preferring text-node points. */
|
|
271
|
+
export const domPointFromLogical = (root, position) => {
|
|
272
|
+
let remaining = position;
|
|
273
|
+
const walk = (parent) => {
|
|
274
|
+
for (let index = 0; index < parent.childNodes.length; index++) {
|
|
275
|
+
const child = parent.childNodes[index];
|
|
276
|
+
if (!child)
|
|
277
|
+
continue;
|
|
278
|
+
if (child.nodeType === TEXT_NODE) {
|
|
279
|
+
const length = child.data.length;
|
|
280
|
+
if (remaining <= length)
|
|
281
|
+
return { node: child, offset: remaining };
|
|
282
|
+
remaining -= length;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (child.nodeType !== ELEMENT_NODE)
|
|
286
|
+
continue;
|
|
287
|
+
const element = child;
|
|
288
|
+
if (element.nodeName === "BR" || element.hasAttribute("data-chip-id")) {
|
|
289
|
+
if (remaining === 0)
|
|
290
|
+
return { node: parent, offset: index };
|
|
291
|
+
remaining -= 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
// Presentation-only (the badge's hint span): zero width, never a target.
|
|
295
|
+
if (isPresentationOnly(toReadable(element)))
|
|
296
|
+
continue;
|
|
297
|
+
// Transparent wrapper (badge span): descend.
|
|
298
|
+
const inner = walk(element);
|
|
299
|
+
if (inner)
|
|
300
|
+
return inner;
|
|
301
|
+
}
|
|
302
|
+
return remaining === 0 ? { node: parent, offset: parent.childNodes.length } : null;
|
|
303
|
+
};
|
|
304
|
+
return walk(root) ?? { node: root, offset: root.childNodes.length };
|
|
305
|
+
};
|
|
306
|
+
export const readSelectionRange = (root) => {
|
|
307
|
+
const selection = root.ownerDocument.getSelection();
|
|
308
|
+
if (!selection || selection.rangeCount === 0)
|
|
309
|
+
return null;
|
|
310
|
+
const range = selection.getRangeAt(0);
|
|
311
|
+
if (!root.contains(range.startContainer) || !root.contains(range.endContainer))
|
|
312
|
+
return null;
|
|
313
|
+
const start = logicalRangeFromDom(toReadable(root), toReadable(range.startContainer), range.startOffset);
|
|
314
|
+
const end = logicalRangeFromDom(toReadable(root), toReadable(range.endContainer), range.endOffset);
|
|
315
|
+
if (start === null || end === null)
|
|
316
|
+
return null;
|
|
317
|
+
return start <= end ? { start, end } : { start: end, end: start };
|
|
318
|
+
};
|
|
319
|
+
export const writeCaretToDom = (root, start, end = start) => {
|
|
320
|
+
const selection = root.ownerDocument.getSelection();
|
|
321
|
+
if (!selection)
|
|
322
|
+
return;
|
|
323
|
+
const anchor = domPointFromLogical(root, start);
|
|
324
|
+
const focus = end === start ? anchor : domPointFromLogical(root, end);
|
|
325
|
+
selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
|
|
326
|
+
};
|
|
327
|
+
const badgeSpansOf = (root) => [
|
|
328
|
+
...root.querySelectorAll("[data-command-badge]"),
|
|
329
|
+
];
|
|
330
|
+
const unwrapBadgeSpan = (span) => {
|
|
331
|
+
const parent = span.parentNode;
|
|
332
|
+
if (!parent)
|
|
333
|
+
return;
|
|
334
|
+
// Presentation children (the hint span) are the badge's own chrome — they
|
|
335
|
+
// die with it rather than spilling into content.
|
|
336
|
+
for (const child of [...span.children]) {
|
|
337
|
+
if (isPresentationOnly(toReadable(child)))
|
|
338
|
+
child.remove();
|
|
339
|
+
}
|
|
340
|
+
while (span.firstChild)
|
|
341
|
+
parent.insertBefore(span.firstChild, span);
|
|
342
|
+
parent.removeChild(span);
|
|
343
|
+
// No parent.normalize() — the reader concatenates fragmented text nodes,
|
|
344
|
+
// and merging the selection's node collapses the caret in WebKit.
|
|
345
|
+
};
|
|
346
|
+
// The badge's logical text length: direct text nodes only — the hint span is
|
|
347
|
+
// zero-width presentation and must not count toward the token range.
|
|
348
|
+
const badgeTextLength = (span) => {
|
|
349
|
+
let length = 0;
|
|
350
|
+
for (const child of [...span.childNodes]) {
|
|
351
|
+
if (child.nodeType === TEXT_NODE)
|
|
352
|
+
length += child.data.length;
|
|
353
|
+
}
|
|
354
|
+
return length;
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Make the DOM match the token (or its absence). Returns true when the DOM
|
|
358
|
+
* changed — the caller must restore the caret afterwards.
|
|
359
|
+
*/
|
|
360
|
+
export const syncBadge = (root, token) => {
|
|
361
|
+
const spans = badgeSpansOf(root);
|
|
362
|
+
if (!token) {
|
|
363
|
+
if (spans.length === 0)
|
|
364
|
+
return false;
|
|
365
|
+
for (const span of spans)
|
|
366
|
+
unwrapBadgeSpan(span);
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
// Fast path: one span already covering exactly [start, end] with the right
|
|
370
|
+
// placeholder marker.
|
|
371
|
+
const [onlySpan] = spans;
|
|
372
|
+
if (spans.length === 1 && onlySpan) {
|
|
373
|
+
const span = onlySpan;
|
|
374
|
+
const parent = span.parentNode;
|
|
375
|
+
if (parent) {
|
|
376
|
+
const index = [...parent.childNodes].indexOf(span);
|
|
377
|
+
const spanStart = logicalRangeFromDom(toReadable(root), toReadable(parent), index);
|
|
378
|
+
const spanLength = badgeTextLength(span);
|
|
379
|
+
const hasPlaceholder = span.hasAttribute("data-command-placeholder");
|
|
380
|
+
if (spanStart === token.start &&
|
|
381
|
+
spanStart + spanLength === token.end &&
|
|
382
|
+
hasPlaceholder === token.showPlaceholder) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const span of spans)
|
|
388
|
+
unwrapBadgeSpan(span);
|
|
389
|
+
// Wrap [start, end]: a Range handles boundary text-node splitting for us.
|
|
390
|
+
// The token is text-only by construction (chips scan as whitespace), so the
|
|
391
|
+
// extracted fragment never partially covers a non-text node.
|
|
392
|
+
const startPoint = domPointFromLogical(root, token.start);
|
|
393
|
+
const endPoint = domPointFromLogical(root, token.end);
|
|
394
|
+
const range = root.ownerDocument.createRange();
|
|
395
|
+
range.setStart(startPoint.node, startPoint.offset);
|
|
396
|
+
range.setEnd(endPoint.node, endPoint.offset);
|
|
397
|
+
const span = root.ownerDocument.createElement("span");
|
|
398
|
+
span.setAttribute("data-command-badge", "");
|
|
399
|
+
if (token.showPlaceholder)
|
|
400
|
+
span.setAttribute("data-command-placeholder", "");
|
|
401
|
+
span.appendChild(range.extractContents());
|
|
402
|
+
range.insertNode(span);
|
|
403
|
+
return true;
|
|
404
|
+
};
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Caret visibility — native typing auto-scrolls; programmatic writes don't.
|
|
407
|
+
// Nudge only the nearest scrollable ancestor (never scrollIntoView — it
|
|
408
|
+
// yanks the whole page).
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
export const scrollCaretIntoView = (root) => {
|
|
411
|
+
const selection = root.ownerDocument.getSelection();
|
|
412
|
+
if (!selection || selection.rangeCount === 0)
|
|
413
|
+
return;
|
|
414
|
+
const rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
415
|
+
let scroller = root;
|
|
416
|
+
while (scroller && scroller !== root.ownerDocument.body) {
|
|
417
|
+
const { overflowY } = getComputedStyle(scroller);
|
|
418
|
+
if ((overflowY === "auto" || overflowY === "scroll") &&
|
|
419
|
+
scroller.scrollHeight > scroller.clientHeight) {
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
scroller = scroller.parentElement;
|
|
423
|
+
}
|
|
424
|
+
if (!scroller || scroller === root.ownerDocument.body)
|
|
425
|
+
return;
|
|
426
|
+
const view = scroller.getBoundingClientRect();
|
|
427
|
+
if (rect.top < view.top)
|
|
428
|
+
scroller.scrollTop -= view.top - rect.top;
|
|
429
|
+
else if (rect.bottom > view.bottom)
|
|
430
|
+
scroller.scrollTop += rect.bottom - view.bottom;
|
|
431
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CommandItemData } from "./types.js";
|
|
2
|
+
export declare const fuzzyScore: (query: string, target: string) => number;
|
|
3
|
+
export declare const filterArrayItems: (items: CommandItemData[], query: string) => CommandItemData[];
|
|
4
|
+
export declare const suggestionRemainder: (query: string, label: string) => string | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Command list — fuzzy filtering. A prefix match wins outright (2); otherwise
|
|
2
|
+
// every matched character scores, with bonuses for adjacency and word-boundary
|
|
3
|
+
// hits, normalized by query length. Returns 0 when the query can't be matched.
|
|
4
|
+
export const fuzzyScore = (query, target) => {
|
|
5
|
+
if (!query)
|
|
6
|
+
return 1;
|
|
7
|
+
const lowerQuery = query.toLowerCase();
|
|
8
|
+
const lowerTarget = target.toLowerCase();
|
|
9
|
+
if (lowerTarget.startsWith(lowerQuery))
|
|
10
|
+
return 2;
|
|
11
|
+
let queryIndex = 0;
|
|
12
|
+
let score = 0;
|
|
13
|
+
let previousMatchIndex = -1;
|
|
14
|
+
for (let targetIndex = 0; targetIndex < lowerTarget.length && queryIndex < lowerQuery.length; targetIndex++) {
|
|
15
|
+
if (lowerTarget[targetIndex] === lowerQuery[queryIndex]) {
|
|
16
|
+
score += 1;
|
|
17
|
+
if (previousMatchIndex === targetIndex - 1)
|
|
18
|
+
score += 2;
|
|
19
|
+
if (targetIndex === 0 || lowerTarget[targetIndex - 1] === " ")
|
|
20
|
+
score += 1;
|
|
21
|
+
previousMatchIndex = targetIndex;
|
|
22
|
+
queryIndex++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return queryIndex === lowerQuery.length ? score / lowerQuery.length : 0;
|
|
26
|
+
};
|
|
27
|
+
export const filterArrayItems = (items, query) => {
|
|
28
|
+
if (!query)
|
|
29
|
+
return items;
|
|
30
|
+
// Score and filter in a single pass, then sort by score.
|
|
31
|
+
const scored = [];
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const target = `${item.label ?? item.value ?? ""} ${item.keywords ?? ""}`.trim();
|
|
34
|
+
const score = fuzzyScore(query, target);
|
|
35
|
+
if (score > 0)
|
|
36
|
+
scored.push({ item, score });
|
|
37
|
+
}
|
|
38
|
+
scored.sort((a, b) => b.score - a.score);
|
|
39
|
+
return scored.map(({ item }) => item);
|
|
40
|
+
};
|
|
41
|
+
// Ghost-text completion: the part of the highlighted item's label that would
|
|
42
|
+
// complete the query — only for a case-insensitive prefix match (a fuzzy hit
|
|
43
|
+
// completed inline would read as broken), with the label's own casing.
|
|
44
|
+
export const suggestionRemainder = (query, label) => {
|
|
45
|
+
if (!label.toLowerCase().startsWith(query.toLowerCase()))
|
|
46
|
+
return null;
|
|
47
|
+
const remainder = label.slice(query.length);
|
|
48
|
+
return remainder.length > 0 ? remainder : null;
|
|
49
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type { ComposerSubmitState, UseComposerSubmitOptions } from "./actions.js";
|
|
2
|
+
export { useComposerSubmit } from "./actions.js";
|
|
3
|
+
export type { ComposerAttachmentsProps, ComposerAttachmentTriggerProps } from "./attachments.js";
|
|
4
|
+
export type { CommandListState, ComposerCommandProps } from "./command-list.js";
|
|
5
|
+
export { useCommandListItems } from "./command-list.js";
|
|
6
|
+
export type { ComposerContainerProps } from "./container.js";
|
|
7
|
+
export { useComposerController } from "./controller.js";
|
|
8
|
+
export { filterArrayItems, fuzzyScore, suggestionRemainder } from "./fuzzy.js";
|
|
9
|
+
export * as Composer from "./index.parts.js";
|
|
10
|
+
export type { ComposerSubmitOn, EditorKeyAction, EditorKeyContext } from "./keyboard.js";
|
|
11
|
+
export { interpretAskUserKey, interpretEditorKey } from "./keyboard.js";
|
|
12
|
+
export type { ComposerPanelProps } from "./panel.js";
|
|
13
|
+
export type { ComposerPlaceholderProps } from "./placeholder.js";
|
|
14
|
+
export type { ComposerPopoverProps } from "./popover.js";
|
|
15
|
+
export type { ActiveTokenState, RegisteredPrefix } from "./prefix-detection.js";
|
|
16
|
+
export { CLOSED_COMMAND_STATE, detectActivePrefix } from "./prefix-detection.js";
|
|
17
|
+
export type { ComposerRootProps } from "./root.js";
|
|
18
|
+
export type { Segment, SegmentDoc, TextChange } from "./segments.js";
|
|
19
|
+
export { documentLength, getPlainText, serializeSegments, sliceSegments, spliceSegments, } from "./segments.js";
|
|
20
|
+
export type { ComposerAskUserState, ComposerAttachmentsState, ComposerCommandsState, ComposerEditorState, ComposerPanelSlice, ComposerState, ComposerStore, } from "./store.js";
|
|
21
|
+
export { useComposer, useComposerStore } from "./store.js";
|
|
22
|
+
export type { ComposerTextareaProps, ComposerTextareaState } from "./textarea.js";
|
|
23
|
+
export type { AskUserOption, AskUserQuestion, AttachmentsApi, ChipData, CommandItemData, CommandItemKind, ComposerAnswerEntry, ComposerAnswersSubmit, ComposerCommandsConfig, ComposerCommandsItems, ComposerCommandsMap, ComposerEditorHandle, ComposerMessageSubmit, ComposerSnapshot, ComposerSubmitData, PrefixOnSelectContext, RegisteredEditor, TriggerRule, } from "./types.js";
|
|
24
|
+
export type { ComposerEditableProps, UseComposerEditorOptions, UseComposerEditorResult, } from "./use-composer-editor.js";
|
|
25
|
+
export { useComposerEditor } from "./use-composer-editor.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Public surface of @intentface/chat/composer: the `Composer` namespace plus
|
|
2
|
+
// the store/controller/hook building blocks. No "use client" directive here —
|
|
3
|
+
// see index.parts.ts.
|
|
4
|
+
export { useComposerSubmit } from "./actions.js";
|
|
5
|
+
export { useCommandListItems } from "./command-list.js";
|
|
6
|
+
export { useComposerController } from "./controller.js";
|
|
7
|
+
export { filterArrayItems, fuzzyScore, suggestionRemainder } from "./fuzzy.js";
|
|
8
|
+
export * as Composer from "./index.parts.js";
|
|
9
|
+
export { interpretAskUserKey, interpretEditorKey } from "./keyboard.js";
|
|
10
|
+
export { CLOSED_COMMAND_STATE, detectActivePrefix } from "./prefix-detection.js";
|
|
11
|
+
export { documentLength, getPlainText, serializeSegments, sliceSegments, spliceSegments, } from "./segments.js";
|
|
12
|
+
export { useComposer, useComposerStore } from "./store.js";
|
|
13
|
+
export { useComposerEditor } from "./use-composer-editor.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { ComposerActions as Actions, ComposerContextWindow as ContextWindow, ComposerSubmit as Submit, } from "./actions.js";
|
|
2
|
+
export { ComposerAttachments as Attachments, ComposerAttachmentTrigger as AttachmentTrigger, } from "./attachments.js";
|
|
3
|
+
export { ComposerCommand as Command, ComposerCommandDismiss as CommandDismiss, ComposerCommandEmpty as CommandEmpty, ComposerCommandGroup as CommandGroup, ComposerCommandGroupLabel as CommandGroupLabel, ComposerCommandItem as CommandItem, ComposerCommandItemDescription as CommandItemDescription, ComposerCommandItemIcon as CommandItemIcon, ComposerCommandItemLabel as CommandItemLabel, ComposerCommandList as CommandList, ComposerCommandLoading as CommandLoading, } from "./command-list.js";
|
|
4
|
+
export { ComposerContainer as Container } from "./container.js";
|
|
5
|
+
export { ComposerPanel as Panel } from "./panel.js";
|
|
6
|
+
export { ComposerPlaceholder as Placeholder } from "./placeholder.js";
|
|
7
|
+
export { ComposerPopover as Popover } from "./popover.js";
|
|
8
|
+
export { ComposerRoot as Root } from "./root.js";
|
|
9
|
+
/** Create a standalone store handle: `<Composer.Root store={…}>` + `useComposerStore(store, selector)` + `store.controller` for imperative access. */
|
|
10
|
+
export { createComposerStore as createStore } from "./store.js";
|
|
11
|
+
export { ComposerTextarea as Textarea } from "./textarea.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Part names for the `Composer` namespace. Deliberately carries no
|
|
2
|
+
// "use client" directive: this module and index.ts must stay server-resolvable
|
|
3
|
+
// so a React Server Component can reach Composer.Root and its siblings through
|
|
4
|
+
// them. Each part module carries the directive itself.
|
|
5
|
+
export { ComposerActions as Actions, ComposerContextWindow as ContextWindow, ComposerSubmit as Submit, } from "./actions.js";
|
|
6
|
+
export { ComposerAttachments as Attachments, ComposerAttachmentTrigger as AttachmentTrigger, } from "./attachments.js";
|
|
7
|
+
export { ComposerCommand as Command, ComposerCommandDismiss as CommandDismiss, ComposerCommandEmpty as CommandEmpty, ComposerCommandGroup as CommandGroup, ComposerCommandGroupLabel as CommandGroupLabel, ComposerCommandItem as CommandItem, ComposerCommandItemDescription as CommandItemDescription, ComposerCommandItemIcon as CommandItemIcon, ComposerCommandItemLabel as CommandItemLabel, ComposerCommandList as CommandList, ComposerCommandLoading as CommandLoading, } from "./command-list.js";
|
|
8
|
+
export { ComposerContainer as Container } from "./container.js";
|
|
9
|
+
export { ComposerPanel as Panel } from "./panel.js";
|
|
10
|
+
export { ComposerPlaceholder as Placeholder } from "./placeholder.js";
|
|
11
|
+
export { ComposerPopover as Popover } from "./popover.js";
|
|
12
|
+
export { ComposerRoot as Root } from "./root.js";
|
|
13
|
+
/** Create a standalone store handle: `<Composer.Root store={…}>` + `useComposerStore(store, selector)` + `store.controller` for imperative access. */
|
|
14
|
+
export { createComposerStore as createStore } from "./store.js";
|
|
15
|
+
export { ComposerTextarea as Textarea } from "./textarea.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type RefObject, useEffect } from "react";
|
|
2
|
+
import type { RegisteredPrefix } from "./prefix-detection.js";
|
|
3
|
+
import type { ComposerCommandsMap, ComposerSnapshot, RegisteredEditor } from "./types.js";
|
|
4
|
+
export declare const useIsomorphicLayoutEffect: typeof useEffect;
|
|
5
|
+
export declare const useAsRef: <T>(value: T) => RefObject<T>;
|
|
6
|
+
export type ComposerInternalsValue = {
|
|
7
|
+
commands: ComposerCommandsMap;
|
|
8
|
+
getRegisteredPrefixes: () => RegisteredPrefix[];
|
|
9
|
+
reportEditorUpdate: () => void;
|
|
10
|
+
};
|
|
11
|
+
export declare const ComposerInternalsContext: import("react").Context<ComposerInternalsValue | null>;
|
|
12
|
+
export declare const useComposerInternals: () => ComposerInternalsValue;
|
|
13
|
+
export declare const useCommandRegistry: (commands: ComposerCommandsMap) => {
|
|
14
|
+
getRegisteredPrefixes: () => RegisteredPrefix[];
|
|
15
|
+
};
|
|
16
|
+
export declare const useDragDropFiles: ({ rootRef, globalDropRef, onFiles, setDragging, }: {
|
|
17
|
+
rootRef: RefObject<HTMLElement | null>;
|
|
18
|
+
globalDropRef: RefObject<boolean>;
|
|
19
|
+
onFiles: (files: FileList) => void;
|
|
20
|
+
setDragging: (active: boolean) => void;
|
|
21
|
+
}) => void;
|
|
22
|
+
export declare const useComposerSnapshot: ({ editorRef, defaultValue, value, onValueChange, }: {
|
|
23
|
+
editorRef: RefObject<RegisteredEditor | null>;
|
|
24
|
+
defaultValue?: ComposerSnapshot;
|
|
25
|
+
value?: ComposerSnapshot;
|
|
26
|
+
onValueChange?: (snapshot: ComposerSnapshot) => void;
|
|
27
|
+
}) => {
|
|
28
|
+
reportEditorUpdate: () => void;
|
|
29
|
+
};
|