@particle-academy/react-fancy 5.64.1 → 5.65.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/CHANGELOG.md +45 -0
- package/dist/{chunk-Z6LYBL5L.cjs → chunk-C463C4MM.cjs} +359 -29
- package/dist/chunk-C463C4MM.cjs.map +1 -0
- package/dist/{chunk-BTNF3QBR.js → chunk-COXHFZR6.js} +360 -32
- package/dist/chunk-COXHFZR6.js.map +1 -0
- package/dist/{index-BF5qsQur.d.cts → index-CCJVT9Hu.d.cts} +140 -3
- package/dist/{index-BZON-gXr.d.ts → index-DRlltbdV.d.ts} +140 -3
- package/dist/index.cjs +13 -5
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/prompt-input.cjs +10 -2
- package/dist/prompt-input.d.cts +1 -1
- package/dist/prompt-input.d.ts +1 -1
- package/dist/prompt-input.js +1 -1
- package/docs/PromptInput.md +78 -1
- package/package.json +1 -1
- package/dist/chunk-BTNF3QBR.js.map +0 -1
- package/dist/chunk-Z6LYBL5L.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [5.65.0] - 2026-10-27
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`PromptInput`: controlled state and draft save/restore** (genie#1261).
|
|
19
|
+
Before this, nothing outside the composer could read or set what had been
|
|
20
|
+
typed, so a host could not keep a draft across a restart. That also broke the
|
|
21
|
+
component contract, which requires controlled state.
|
|
22
|
+
- `value` / `defaultValue` / `onChange(text)` cover the simple case. With
|
|
23
|
+
paste pills on, `value` carries a `pasteMarker(id)` for each pill, the same
|
|
24
|
+
as `Composer`'s `value`.
|
|
25
|
+
- `draft` / `defaultDraft` / `onDraftChange(draft)` cover the whole draft.
|
|
26
|
+
A `PromptDraft` is plain JSON:
|
|
27
|
+
`{ v: 1, text, pills: [{ kind: "text", id, text } | { kind: "image", id, label, file }], attachments: [{ id, name, type, bytes, ref? }] }`.
|
|
28
|
+
- A `ref` now resolves to a `PromptInputHandle`: `getDraft()`,
|
|
29
|
+
`setDraft(draft, { focus?: false })` and `focus()`.
|
|
30
|
+
- **Restoring a draft never moves focus or a caret** (genie#1235). This holds
|
|
31
|
+
when nothing is focused and when another input is. Pass `{ focus: true }`
|
|
32
|
+
to focus the composer.
|
|
33
|
+
- `setDraft` does **not** call `onChange` / `onDraftChange`. The host set the
|
|
34
|
+
draft and already has it, and firing the callbacks would make a restore
|
|
35
|
+
look like typing (a dirty flag, a typing indicator, a save over what was
|
|
36
|
+
just loaded). This is the same as assigning `input.value` in the DOM.
|
|
37
|
+
- The file bytes are not in the draft. `persistAttachment(file, entry)`
|
|
38
|
+
stores each attached or pasted file wherever the host chooses and returns
|
|
39
|
+
a handle, which becomes the entry's `ref`. It can be sync or async.
|
|
40
|
+
`restoreAttachment(entry)` gets the File back on restore. To keep the bytes
|
|
41
|
+
inline, use the new `promptFileToDataUrl` / `promptDataUrlToFile` helpers.
|
|
42
|
+
A restored entry with no File still shows its chip or pill, and reaches
|
|
43
|
+
`onSubmit` with its `ref` and `file: undefined`.
|
|
44
|
+
- `PromptAttachment` gains an optional `ref`. Each attachment chip gets a
|
|
45
|
+
stable `data-react-fancy-prompt-input-attachment="<id>"` handle.
|
|
46
|
+
- A draft whose `v` is unknown is refused with a console warning rather than
|
|
47
|
+
guessed at. Pills restored into a composer with no pills turned on are
|
|
48
|
+
expanded in place, and image pills become attachments, so no raw markers
|
|
49
|
+
show.
|
|
50
|
+
|
|
51
|
+
**What you must do: nothing, unless you opt in.** Existing usage renders the
|
|
52
|
+
same markup as 5.64.1, checked against a snapshot written by 5.64.1 itself.
|
|
53
|
+
Two edge cases. `ref`, `onChange` and `defaultValue` used to fall through
|
|
54
|
+
`...rest` to the root `<div>`, but none of them was in the props type, so a
|
|
55
|
+
TypeScript consumer could not have passed them. If you passed one anyway
|
|
56
|
+
(an untyped `onChange` that listened for DOM change events bubbling to the
|
|
57
|
+
root), it now means the new prop.
|
|
58
|
+
|
|
14
59
|
## [5.64.1] - 2026-10-27
|
|
15
60
|
|
|
16
61
|
### Fixed
|
|
@@ -8,6 +8,95 @@ var chunkZU5Q77OT_cjs = require('./chunk-ZU5Q77OT.cjs');
|
|
|
8
8
|
var react = require('react');
|
|
9
9
|
var jsxRuntime = require('react/jsx-runtime');
|
|
10
10
|
|
|
11
|
+
// src/components/PromptInput/prompt-draft.ts
|
|
12
|
+
var EMPTY_DRAFT = { v: 1, text: "", pills: [], attachments: [] };
|
|
13
|
+
function readFile(raw, id) {
|
|
14
|
+
if (!raw || typeof raw !== "object") return null;
|
|
15
|
+
const f = raw;
|
|
16
|
+
const fileId = id ?? f.id;
|
|
17
|
+
if (typeof fileId !== "string" || typeof f.name !== "string") return null;
|
|
18
|
+
const out = {
|
|
19
|
+
id: fileId,
|
|
20
|
+
name: f.name,
|
|
21
|
+
type: typeof f.type === "string" ? f.type : "",
|
|
22
|
+
bytes: typeof f.bytes === "number" && Number.isFinite(f.bytes) ? f.bytes : 0
|
|
23
|
+
};
|
|
24
|
+
if (typeof f.ref === "string") out.ref = f.ref;
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
function readPill(raw) {
|
|
28
|
+
if (!raw || typeof raw !== "object") return null;
|
|
29
|
+
const p = raw;
|
|
30
|
+
if (typeof p.id !== "string") return null;
|
|
31
|
+
if (p.kind === "text" && typeof p.text === "string") {
|
|
32
|
+
return { kind: "text", id: p.id, text: p.text };
|
|
33
|
+
}
|
|
34
|
+
if (p.kind === "image" && typeof p.label === "string") {
|
|
35
|
+
const file = readFile(p.file, p.id);
|
|
36
|
+
return file ? { kind: "image", id: p.id, label: p.label, file } : null;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function readPromptDraft(raw, inline) {
|
|
41
|
+
if (!raw || typeof raw !== "object" || raw.v !== 1) return null;
|
|
42
|
+
const d = raw;
|
|
43
|
+
let text = typeof d.text === "string" ? d.text : "";
|
|
44
|
+
let pills = (Array.isArray(d.pills) ? d.pills : []).map(readPill).filter((p) => p !== null);
|
|
45
|
+
let attachments = (Array.isArray(d.attachments) ? d.attachments : []).map((a) => readFile(a)).filter((a) => a !== null);
|
|
46
|
+
const marked = new Set(chunkQNQNGARW_cjs.markedIds(text));
|
|
47
|
+
pills = pills.filter((p) => marked.has(p.id));
|
|
48
|
+
if (!inline && pills.length > 0) {
|
|
49
|
+
const texts = pills.flatMap((p) => p.kind === "text" ? [{ id: p.id, text: p.text }] : []);
|
|
50
|
+
const images = pills.flatMap((p) => p.kind === "image" ? [p] : []);
|
|
51
|
+
text = chunkQNQNGARW_cjs.expandMarkers(
|
|
52
|
+
text,
|
|
53
|
+
texts,
|
|
54
|
+
images.map((p) => ({ id: p.id, label: p.label }))
|
|
55
|
+
);
|
|
56
|
+
attachments = [...attachments, ...images.map((p) => p.file)];
|
|
57
|
+
pills = [];
|
|
58
|
+
}
|
|
59
|
+
return { v: 1, text, pills, attachments };
|
|
60
|
+
}
|
|
61
|
+
function clonePromptDraft(draft) {
|
|
62
|
+
return JSON.parse(JSON.stringify(draft));
|
|
63
|
+
}
|
|
64
|
+
function promptDraftFiles(draft) {
|
|
65
|
+
return [
|
|
66
|
+
...draft.attachments,
|
|
67
|
+
...draft.pills.flatMap((p) => p.kind === "image" ? [p.file] : [])
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function promptFileToDataUrl(file) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const reader = new FileReader();
|
|
73
|
+
reader.onload = () => resolve(String(reader.result));
|
|
74
|
+
reader.onerror = () => reject(reader.error);
|
|
75
|
+
reader.readAsDataURL(file);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function promptDataUrlToFile(dataUrl, name, type) {
|
|
79
|
+
if (!dataUrl.startsWith("data:")) return void 0;
|
|
80
|
+
const comma = dataUrl.indexOf(",");
|
|
81
|
+
if (comma < 0) return void 0;
|
|
82
|
+
const meta = dataUrl.slice(5, comma);
|
|
83
|
+
const body = dataUrl.slice(comma + 1);
|
|
84
|
+
const mime = type || meta.split(";")[0] || "application/octet-stream";
|
|
85
|
+
try {
|
|
86
|
+
let bytes;
|
|
87
|
+
if (meta.endsWith(";base64")) {
|
|
88
|
+
const bin = atob(body);
|
|
89
|
+
bytes = new Uint8Array(bin.length);
|
|
90
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
91
|
+
} else {
|
|
92
|
+
bytes = new TextEncoder().encode(decodeURIComponent(body));
|
|
93
|
+
}
|
|
94
|
+
return new File([bytes], name, { type: mime });
|
|
95
|
+
} catch {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
11
100
|
// src/utils/platform.ts
|
|
12
101
|
var APPLE_UA_DATA_PLATFORMS = /* @__PURE__ */ new Set(["macos", "ios", "ipados"]);
|
|
13
102
|
var APPLE_FREE_TEXT = /\b(mac|macintosh|iphone|ipad|ipod|darwin)/i;
|
|
@@ -34,7 +123,15 @@ var DEFAULT_MENTION_COLOR = {
|
|
|
34
123
|
file: "#10b981",
|
|
35
124
|
person: "#3b82f6"
|
|
36
125
|
};
|
|
37
|
-
|
|
126
|
+
var PromptInput = react.forwardRef(function PromptInput2({
|
|
127
|
+
value: controlledValue,
|
|
128
|
+
defaultValue,
|
|
129
|
+
onChange,
|
|
130
|
+
draft: controlledDraft,
|
|
131
|
+
defaultDraft,
|
|
132
|
+
onDraftChange,
|
|
133
|
+
persistAttachment,
|
|
134
|
+
restoreAttachment,
|
|
38
135
|
budgetTokens,
|
|
39
136
|
commands = [],
|
|
40
137
|
mentions = [],
|
|
@@ -60,9 +157,146 @@ function PromptInput({
|
|
|
60
157
|
meterShowAt = 0.75,
|
|
61
158
|
meterWarnAt = 0.9,
|
|
62
159
|
...rest
|
|
63
|
-
}) {
|
|
64
|
-
const [text, setText] = react.useState("");
|
|
160
|
+
}, ref) {
|
|
65
161
|
const inline = pasteThreshold !== void 0 || pasteImages;
|
|
162
|
+
const draftControlled = controlledDraft !== void 0;
|
|
163
|
+
const [innerDraft, setInnerDraft] = react.useState(
|
|
164
|
+
() => defaultDraft && readPromptDraft(defaultDraft, inline) || {
|
|
165
|
+
...EMPTY_DRAFT,
|
|
166
|
+
text: defaultValue ?? ""
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
const baseDraft = draftControlled ? controlledDraft : innerDraft;
|
|
170
|
+
const model = !draftControlled && controlledValue !== void 0 && controlledValue !== baseDraft.text ? { ...baseDraft, text: controlledValue } : baseDraft;
|
|
171
|
+
const modelRef = react.useRef(model);
|
|
172
|
+
modelRef.current = model;
|
|
173
|
+
const text = model.text;
|
|
174
|
+
const onChangeRef = react.useRef(onChange);
|
|
175
|
+
onChangeRef.current = onChange;
|
|
176
|
+
const onDraftChangeRef = react.useRef(onDraftChange);
|
|
177
|
+
onDraftChangeRef.current = onDraftChange;
|
|
178
|
+
const persistRef = react.useRef(persistAttachment);
|
|
179
|
+
persistRef.current = persistAttachment;
|
|
180
|
+
const commit = react.useCallback(
|
|
181
|
+
(update, notify = true) => {
|
|
182
|
+
const prev = modelRef.current;
|
|
183
|
+
const next = update(prev);
|
|
184
|
+
if (next === prev) return;
|
|
185
|
+
modelRef.current = next;
|
|
186
|
+
setInnerDraft(next);
|
|
187
|
+
if (!notify) return;
|
|
188
|
+
if (next.text !== prev.text) onChangeRef.current?.(next.text);
|
|
189
|
+
onDraftChangeRef.current?.(next);
|
|
190
|
+
},
|
|
191
|
+
[]
|
|
192
|
+
);
|
|
193
|
+
const filesRef = react.useRef(/* @__PURE__ */ new Map());
|
|
194
|
+
const placeholdersRef = react.useRef(/* @__PURE__ */ new Map());
|
|
195
|
+
const requestedRef = react.useRef(/* @__PURE__ */ new Set());
|
|
196
|
+
const [filesVersion, bumpFiles] = react.useReducer((n) => n + 1, 0);
|
|
197
|
+
const setFileRef = react.useCallback(
|
|
198
|
+
(id, fileRef2) => commit((m) => {
|
|
199
|
+
const withRef = (f) => f.id === id ? { ...f, ref: fileRef2 } : f;
|
|
200
|
+
if (!promptDraftFiles(m).some((f) => f.id === id)) return m;
|
|
201
|
+
return {
|
|
202
|
+
...m,
|
|
203
|
+
attachments: m.attachments.map(withRef),
|
|
204
|
+
pills: m.pills.map((p) => p.kind === "image" ? { ...p, file: withRef(p.file) } : p)
|
|
205
|
+
};
|
|
206
|
+
}),
|
|
207
|
+
[commit]
|
|
208
|
+
);
|
|
209
|
+
const persist = react.useCallback(
|
|
210
|
+
(file, entry) => {
|
|
211
|
+
const store = persistRef.current;
|
|
212
|
+
if (!store) return void 0;
|
|
213
|
+
let out;
|
|
214
|
+
try {
|
|
215
|
+
out = store(file, entry);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error("PromptInput: persistAttachment threw", error);
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
if (typeof out === "string") return out;
|
|
221
|
+
if (out && typeof out.then === "function") {
|
|
222
|
+
out.then(
|
|
223
|
+
(fileRef2) => {
|
|
224
|
+
if (typeof fileRef2 === "string") setFileRef(entry.id, fileRef2);
|
|
225
|
+
},
|
|
226
|
+
(error) => console.error("PromptInput: persistAttachment rejected", error)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return void 0;
|
|
230
|
+
},
|
|
231
|
+
[setFileRef]
|
|
232
|
+
);
|
|
233
|
+
const fileMeta = (id, file) => ({
|
|
234
|
+
id,
|
|
235
|
+
name: file.name,
|
|
236
|
+
type: file.type,
|
|
237
|
+
bytes: file.size
|
|
238
|
+
});
|
|
239
|
+
const fileFor = react.useCallback((entry) => {
|
|
240
|
+
const real = filesRef.current.get(entry.id);
|
|
241
|
+
if (real) return real;
|
|
242
|
+
let stand = placeholdersRef.current.get(entry.id);
|
|
243
|
+
if (!stand) {
|
|
244
|
+
stand = new File([], entry.name, { type: entry.type });
|
|
245
|
+
placeholdersRef.current.set(entry.id, stand);
|
|
246
|
+
}
|
|
247
|
+
return stand;
|
|
248
|
+
}, []);
|
|
249
|
+
const heldPastes = react.useMemo(
|
|
250
|
+
() => model.pills.flatMap((p) => p.kind === "text" ? [{ id: p.id, text: p.text }] : []),
|
|
251
|
+
[model.pills]
|
|
252
|
+
);
|
|
253
|
+
const heldImages = react.useMemo(
|
|
254
|
+
() => model.pills.flatMap(
|
|
255
|
+
(p) => p.kind === "image" ? [{ id: p.id, label: p.label, file: fileFor(p.file) }] : []
|
|
256
|
+
),
|
|
257
|
+
// filesVersion: a restored File arrived, so the stand-in can be swapped out.
|
|
258
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
|
+
[model.pills, fileFor, filesVersion]
|
|
260
|
+
);
|
|
261
|
+
const onHeldPastesChange = react.useCallback(
|
|
262
|
+
(next) => commit((m) => {
|
|
263
|
+
const current = m.pills.filter((p) => p.kind === "text");
|
|
264
|
+
if (current.length === next.length && current.every((p, i) => p.id === next[i].id && p.kind === "text" && p.text === next[i].text)) {
|
|
265
|
+
return m;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
...m,
|
|
269
|
+
pills: [
|
|
270
|
+
...next.map((p) => ({ kind: "text", id: p.id, text: p.text })),
|
|
271
|
+
...m.pills.filter((p) => p.kind === "image")
|
|
272
|
+
]
|
|
273
|
+
};
|
|
274
|
+
}),
|
|
275
|
+
[commit]
|
|
276
|
+
);
|
|
277
|
+
const onHeldImagesChange = react.useCallback(
|
|
278
|
+
(next) => commit((m) => {
|
|
279
|
+
const before = new Map(
|
|
280
|
+
m.pills.flatMap((p) => p.kind === "image" ? [[p.id, p]] : [])
|
|
281
|
+
);
|
|
282
|
+
if (before.size === next.length && next.every((i) => before.has(i.id))) return m;
|
|
283
|
+
const images = next.map((image) => {
|
|
284
|
+
const known = before.get(image.id);
|
|
285
|
+
if (known && known.kind === "image") return known;
|
|
286
|
+
filesRef.current.set(image.id, image.file);
|
|
287
|
+
const entry = fileMeta(image.id, image.file);
|
|
288
|
+
const fileRef2 = persist(image.file, entry);
|
|
289
|
+
return {
|
|
290
|
+
kind: "image",
|
|
291
|
+
id: image.id,
|
|
292
|
+
label: image.label,
|
|
293
|
+
file: fileRef2 === void 0 ? entry : { ...entry, ref: fileRef2 }
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
return { ...m, pills: [...m.pills.filter((p) => p.kind === "text"), ...images] };
|
|
297
|
+
}),
|
|
298
|
+
[commit, persist]
|
|
299
|
+
);
|
|
66
300
|
const editorRef = react.useRef(null);
|
|
67
301
|
const paste = chunkQNQNGARW_cjs.useInlinePaste({
|
|
68
302
|
pasteThreshold,
|
|
@@ -71,9 +305,55 @@ function PromptInput({
|
|
|
71
305
|
renderPastePreview,
|
|
72
306
|
classNames: pasteClassNames,
|
|
73
307
|
disabled: false,
|
|
308
|
+
heldPastes,
|
|
309
|
+
onHeldPastesChange,
|
|
310
|
+
heldImages,
|
|
311
|
+
onHeldImagesChange,
|
|
74
312
|
editorRef
|
|
75
313
|
});
|
|
76
|
-
const
|
|
314
|
+
const attachments = react.useMemo(
|
|
315
|
+
() => model.attachments.map((a) => ({
|
|
316
|
+
id: a.id,
|
|
317
|
+
name: a.name,
|
|
318
|
+
bytes: a.bytes,
|
|
319
|
+
file: filesRef.current.get(a.id),
|
|
320
|
+
type: a.type,
|
|
321
|
+
...a.ref !== void 0 ? { ref: a.ref } : {}
|
|
322
|
+
})),
|
|
323
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
324
|
+
[model.attachments, filesVersion]
|
|
325
|
+
);
|
|
326
|
+
const restoreRef = react.useRef(restoreAttachment);
|
|
327
|
+
restoreRef.current = restoreAttachment;
|
|
328
|
+
react.useEffect(() => {
|
|
329
|
+
const restore = restoreRef.current;
|
|
330
|
+
if (!restore) return;
|
|
331
|
+
for (const entry of promptDraftFiles(model)) {
|
|
332
|
+
if (filesRef.current.has(entry.id) || requestedRef.current.has(entry.id)) continue;
|
|
333
|
+
requestedRef.current.add(entry.id);
|
|
334
|
+
const accept = (file) => {
|
|
335
|
+
if (!(file instanceof Blob)) return;
|
|
336
|
+
filesRef.current.set(entry.id, file);
|
|
337
|
+
bumpFiles();
|
|
338
|
+
};
|
|
339
|
+
let out;
|
|
340
|
+
try {
|
|
341
|
+
out = restore(entry);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("PromptInput: restoreAttachment threw", error);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (out && typeof out.then === "function") {
|
|
347
|
+
out.then(
|
|
348
|
+
accept,
|
|
349
|
+
(error) => console.error("PromptInput: restoreAttachment rejected", error)
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
accept(out);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}, [model]);
|
|
356
|
+
const setText = (next) => commit((m) => m.text === next ? m : { ...m, text: next });
|
|
77
357
|
const [picker, setPicker] = react.useState(null);
|
|
78
358
|
const taRef = react.useRef(null);
|
|
79
359
|
const fileRef = react.useRef(null);
|
|
@@ -162,22 +442,68 @@ function PromptInput({
|
|
|
162
442
|
const submit = () => {
|
|
163
443
|
if (!text.trim() && attachments.length === 0) return;
|
|
164
444
|
if (inline) {
|
|
165
|
-
const pastedImages = paste.imagesIn(text).map((image) =>
|
|
166
|
-
id
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
445
|
+
const pastedImages = paste.imagesIn(text).map((image) => {
|
|
446
|
+
const entry = model.pills.find((p) => p.id === image.id);
|
|
447
|
+
const meta = entry?.kind === "image" ? entry.file : fileMeta(image.id, image.file);
|
|
448
|
+
return {
|
|
449
|
+
id: image.id,
|
|
450
|
+
name: meta.name,
|
|
451
|
+
bytes: meta.bytes,
|
|
452
|
+
file: filesRef.current.get(image.id),
|
|
453
|
+
type: meta.type,
|
|
454
|
+
...meta.ref !== void 0 ? { ref: meta.ref } : {}
|
|
455
|
+
};
|
|
456
|
+
});
|
|
172
457
|
onSubmit(paste.expand(text), [...attachments, ...pastedImages]);
|
|
173
|
-
paste.clear();
|
|
174
458
|
} else {
|
|
175
459
|
onSubmit(text, attachments);
|
|
176
460
|
}
|
|
177
|
-
|
|
178
|
-
|
|
461
|
+
commit(() => EMPTY_DRAFT);
|
|
462
|
+
filesRef.current.clear();
|
|
463
|
+
placeholdersRef.current.clear();
|
|
464
|
+
requestedRef.current.clear();
|
|
179
465
|
setPicker(null);
|
|
180
466
|
};
|
|
467
|
+
const pendingFocus = react.useRef(false);
|
|
468
|
+
const focusInput = react.useCallback(() => {
|
|
469
|
+
if (inline) {
|
|
470
|
+
editorRef.current?.focus();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const ta = taRef.current;
|
|
474
|
+
if (!ta) return;
|
|
475
|
+
ta.focus();
|
|
476
|
+
ta.setSelectionRange(ta.value.length, ta.value.length);
|
|
477
|
+
}, [inline]);
|
|
478
|
+
react.useLayoutEffect(() => {
|
|
479
|
+
if (!pendingFocus.current) return;
|
|
480
|
+
pendingFocus.current = false;
|
|
481
|
+
focusInput();
|
|
482
|
+
});
|
|
483
|
+
react.useImperativeHandle(
|
|
484
|
+
ref,
|
|
485
|
+
() => ({
|
|
486
|
+
getDraft: () => clonePromptDraft(modelRef.current),
|
|
487
|
+
setDraft(next, options) {
|
|
488
|
+
const read = readPromptDraft(next, inline);
|
|
489
|
+
if (!read) {
|
|
490
|
+
console.warn(
|
|
491
|
+
"PromptInput: setDraft ignored a draft it does not recognise (expected { v: 1, ... }).",
|
|
492
|
+
next
|
|
493
|
+
);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
for (const entry of promptDraftFiles(read)) {
|
|
497
|
+
if (!filesRef.current.has(entry.id)) requestedRef.current.delete(entry.id);
|
|
498
|
+
}
|
|
499
|
+
setPicker(null);
|
|
500
|
+
commit(() => read, false);
|
|
501
|
+
if (options?.focus) pendingFocus.current = true;
|
|
502
|
+
},
|
|
503
|
+
focus: focusInput
|
|
504
|
+
}),
|
|
505
|
+
[inline, commit, focusInput]
|
|
506
|
+
);
|
|
181
507
|
const onKeyDown = (e) => {
|
|
182
508
|
if (picker) {
|
|
183
509
|
if (e.key === "ArrowDown") {
|
|
@@ -211,16 +537,14 @@ function PromptInput({
|
|
|
211
537
|
const attach = (list) => {
|
|
212
538
|
const files = Array.from(list ?? []);
|
|
213
539
|
if (files.length === 0) return;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}))
|
|
223
|
-
]);
|
|
540
|
+
const added = files.map((f) => {
|
|
541
|
+
const id = crypto.randomUUID();
|
|
542
|
+
filesRef.current.set(id, f);
|
|
543
|
+
const entry = fileMeta(id, f);
|
|
544
|
+
const fileRef2 = persist(f, entry);
|
|
545
|
+
return fileRef2 === void 0 ? entry : { ...entry, ref: fileRef2 };
|
|
546
|
+
});
|
|
547
|
+
commit((m) => ({ ...m, attachments: [...m.attachments, ...added] }));
|
|
224
548
|
};
|
|
225
549
|
const onDrop = (e) => {
|
|
226
550
|
e.preventDefault();
|
|
@@ -256,6 +580,7 @@ function PromptInput({
|
|
|
256
580
|
attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1.5 border-b border-zinc-200 px-3 py-2 dark:border-zinc-800", children: attachments.map((a) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
257
581
|
"span",
|
|
258
582
|
{
|
|
583
|
+
"data-react-fancy-prompt-input-attachment": a.id,
|
|
259
584
|
className: "inline-flex items-center gap-1.5 rounded-md bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] px-2 py-0.5 text-[11px] dark:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]",
|
|
260
585
|
children: [
|
|
261
586
|
/* @__PURE__ */ jsxRuntime.jsx(chunkMXWR3T3N_cjs.Icon, { name: "paperclip", size: "xs", className: "text-zinc-400" }),
|
|
@@ -264,7 +589,10 @@ function PromptInput({
|
|
|
264
589
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
265
590
|
"button",
|
|
266
591
|
{
|
|
267
|
-
onClick: () =>
|
|
592
|
+
onClick: () => commit((m) => ({
|
|
593
|
+
...m,
|
|
594
|
+
attachments: m.attachments.filter((x) => x.id !== a.id)
|
|
595
|
+
})),
|
|
268
596
|
className: "opacity-50 hover:opacity-100",
|
|
269
597
|
"aria-label": "Remove attachment",
|
|
270
598
|
children: "\xD7"
|
|
@@ -306,7 +634,7 @@ function PromptInput({
|
|
|
306
634
|
chunkQNQNGARW_cjs.PastedFilesInput,
|
|
307
635
|
{
|
|
308
636
|
name: pasteFilesName,
|
|
309
|
-
files: paste.imagesIn(text).
|
|
637
|
+
files: paste.imagesIn(text).flatMap((i) => filesRef.current.has(i.id) ? [i.file] : [])
|
|
310
638
|
}
|
|
311
639
|
)
|
|
312
640
|
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -471,7 +799,7 @@ function PromptInput({
|
|
|
471
799
|
]
|
|
472
800
|
}
|
|
473
801
|
);
|
|
474
|
-
}
|
|
802
|
+
});
|
|
475
803
|
function fmtTokens(n) {
|
|
476
804
|
if (n < 1e3) return `${n}`;
|
|
477
805
|
return `${(n / 1e3).toFixed(1)}k`;
|
|
@@ -486,5 +814,7 @@ exports.PromptInput = PromptInput;
|
|
|
486
814
|
exports.browserPlatformHints = browserPlatformHints;
|
|
487
815
|
exports.detectPlatform = detectPlatform;
|
|
488
816
|
exports.modifierKeyLabel = modifierKeyLabel;
|
|
489
|
-
|
|
490
|
-
|
|
817
|
+
exports.promptDataUrlToFile = promptDataUrlToFile;
|
|
818
|
+
exports.promptFileToDataUrl = promptFileToDataUrl;
|
|
819
|
+
//# sourceMappingURL=chunk-C463C4MM.cjs.map
|
|
820
|
+
//# sourceMappingURL=chunk-C463C4MM.cjs.map
|