@ecsplico/editor 3.5.0 → 3.5.1
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 +7 -2
- package/dist/code/CodeSurface.vue.d.ts +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +1 -1
- package/dist/features/ai/protocol/generate.d.ts +1 -1
- package/dist/features/image-studio/node-core.d.ts +46 -0
- package/dist/features/image-studio/node-core.js +79 -0
- package/dist/features/image-studio/node-core.js.map +1 -0
- package/dist/features/image-studio/node-store.d.ts +3 -11
- package/dist/features/image-studio/node-store.js +15 -44
- package/dist/features/image-studio/node-store.js.map +1 -1
- package/dist/features/image-studio/react/ImageStudioChat.js +101 -0
- package/dist/features/image-studio/react/ImageStudioChat.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioDialog.d.ts +19 -0
- package/dist/features/image-studio/react/ImageStudioDialog.js +357 -0
- package/dist/features/image-studio/react/ImageStudioDialog.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioTile.js +51 -0
- package/dist/features/image-studio/react/ImageStudioTile.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioView.js +126 -0
- package/dist/features/image-studio/react/ImageStudioView.js.map +1 -0
- package/dist/features/image-studio/react/store.d.ts +37 -0
- package/dist/features/image-studio/react/store.js +46 -0
- package/dist/features/image-studio/react/store.js.map +1 -0
- package/dist/features/image-studio/react.d.ts +18 -0
- package/dist/features/image-studio/react.js +21 -0
- package/dist/features/image-studio/react.js.map +1 -0
- package/dist/markdown/MarkdownSurface.vue.d.ts +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { fetchErrorMessage } from "../../../core/fetch-error.js";
|
|
2
|
+
import { useEditorI18n } from "../../../react/use-editor-i18n.js";
|
|
3
|
+
import { cn } from "../../../react/ui/utils.js";
|
|
4
|
+
import { Icon } from "../../../react/Icon.js";
|
|
5
|
+
import { Button } from "../../../react/ui/button.js";
|
|
6
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../../../react/ui/dialog.js";
|
|
7
|
+
import { Textarea } from "../../../react/ui/textarea.js";
|
|
8
|
+
import { Alert, AlertTitle } from "../../../react/ui/alert.js";
|
|
9
|
+
import { Switch } from "../../../react/ui/switch.js";
|
|
10
|
+
import { addGeneration, addReference, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock } from "../state.js";
|
|
11
|
+
import { ImageStudioChat } from "./ImageStudioChat.js";
|
|
12
|
+
import { ImageStudioTile } from "./ImageStudioTile.js";
|
|
13
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
//#region src/features/image-studio/react/ImageStudioDialog.tsx
|
|
16
|
+
const COMMIT_DELAY_MS = 500;
|
|
17
|
+
/**
|
|
18
|
+
* The Bild-Studio: description and Prompt-Bausteine on the left, references and previous generations in the middle,
|
|
19
|
+
* the block's image and the chat on the right. The dialog applies the reducers of `../state` to `store.state` and
|
|
20
|
+
* hands every result to `store.update`; the store only generates, uploads and persists (see `ImageStudioReactStore`).
|
|
21
|
+
*
|
|
22
|
+
* The description is committed 500 ms after the last keystroke and at once on blur, on „Erzeugen“ and on close, so the
|
|
23
|
+
* host never builds a prompt from a stale text. Closing (or unmounting) aborts a running generation; an aborted
|
|
24
|
+
* generation adds no turn, a failed one adds a turn with the problem.
|
|
25
|
+
*/
|
|
26
|
+
function ImageStudioDialog({ open, onOpenChange, store, title }) {
|
|
27
|
+
const { t } = useEditorI18n();
|
|
28
|
+
const uid = useId();
|
|
29
|
+
const { state, promptBlocks, problem } = store;
|
|
30
|
+
const images = [...state.generations, ...state.references];
|
|
31
|
+
const picture = selectedImage(state);
|
|
32
|
+
const storeRef = useRef(store);
|
|
33
|
+
storeRef.current = store;
|
|
34
|
+
const latest = useRef(state);
|
|
35
|
+
const seen = useRef(state);
|
|
36
|
+
if (seen.current !== state) {
|
|
37
|
+
seen.current = state;
|
|
38
|
+
latest.current = state;
|
|
39
|
+
}
|
|
40
|
+
const apply = (next) => {
|
|
41
|
+
latest.current = next;
|
|
42
|
+
storeRef.current.update(next);
|
|
43
|
+
};
|
|
44
|
+
const [draft, setDraft] = useState(state.description);
|
|
45
|
+
const draftRef = useRef(draft);
|
|
46
|
+
/** The description as last committed or received: the host's echo of our own commit must not overwrite a newer draft. */
|
|
47
|
+
const known = useRef(state.description);
|
|
48
|
+
const commitTimer = useRef(void 0);
|
|
49
|
+
function commitDescription() {
|
|
50
|
+
clearTimeout(commitTimer.current);
|
|
51
|
+
commitTimer.current = void 0;
|
|
52
|
+
if (draftRef.current === latest.current.description) return;
|
|
53
|
+
known.current = draftRef.current;
|
|
54
|
+
apply(setDescription(latest.current, draftRef.current));
|
|
55
|
+
}
|
|
56
|
+
function onDescriptionChange(event) {
|
|
57
|
+
const text = event.target.value;
|
|
58
|
+
draftRef.current = text;
|
|
59
|
+
setDraft(text);
|
|
60
|
+
if (text === latest.current.description) return;
|
|
61
|
+
clearTimeout(commitTimer.current);
|
|
62
|
+
commitTimer.current = setTimeout(commitDescription, COMMIT_DELAY_MS);
|
|
63
|
+
}
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (state.description === known.current) return;
|
|
66
|
+
known.current = state.description;
|
|
67
|
+
if (commitTimer.current === void 0) {
|
|
68
|
+
draftRef.current = state.description;
|
|
69
|
+
setDraft(state.description);
|
|
70
|
+
}
|
|
71
|
+
}, [state.description]);
|
|
72
|
+
const fileInput = useRef(null);
|
|
73
|
+
const [uploading, setUploading] = useState(0);
|
|
74
|
+
const [uploadProblem, setUploadProblem] = useState();
|
|
75
|
+
const [dragging, setDragging] = useState(false);
|
|
76
|
+
async function upload(files) {
|
|
77
|
+
const pictures = files.filter((file) => file.type.startsWith("image/"));
|
|
78
|
+
if (!pictures.length) return;
|
|
79
|
+
setUploadProblem(void 0);
|
|
80
|
+
setUploading((count) => count + pictures.length);
|
|
81
|
+
await Promise.all(pictures.map(async (file) => {
|
|
82
|
+
try {
|
|
83
|
+
const image = await storeRef.current.upload(file);
|
|
84
|
+
apply(addReference(latest.current, image));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
setUploadProblem(t("imageStudio.dialog.references.failed", { message: fetchErrorMessage(error) }));
|
|
87
|
+
} finally {
|
|
88
|
+
setUploading((count) => count - 1);
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
function onFilesChosen(event) {
|
|
93
|
+
const input = event.target;
|
|
94
|
+
const files = [...input.files ?? []];
|
|
95
|
+
input.value = "";
|
|
96
|
+
upload(files);
|
|
97
|
+
}
|
|
98
|
+
function onDrop(event) {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
setDragging(false);
|
|
101
|
+
upload([...event.dataTransfer.files]);
|
|
102
|
+
}
|
|
103
|
+
const [running, setRunning] = useState(false);
|
|
104
|
+
const controller = useRef(void 0);
|
|
105
|
+
function abortGeneration() {
|
|
106
|
+
controller.current?.abort();
|
|
107
|
+
controller.current = void 0;
|
|
108
|
+
setRunning(false);
|
|
109
|
+
}
|
|
110
|
+
async function generate(instruction) {
|
|
111
|
+
if (controller.current) return;
|
|
112
|
+
commitDescription();
|
|
113
|
+
const own = new AbortController();
|
|
114
|
+
controller.current = own;
|
|
115
|
+
setRunning(true);
|
|
116
|
+
const turn = {
|
|
117
|
+
id: crypto.randomUUID(),
|
|
118
|
+
instruction,
|
|
119
|
+
imageId: null,
|
|
120
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
121
|
+
};
|
|
122
|
+
try {
|
|
123
|
+
const image = await storeRef.current.generate(instruction, own.signal);
|
|
124
|
+
if (own.signal.aborted) return;
|
|
125
|
+
apply(addGeneration(latest.current, image, turn));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (own.signal.aborted) return;
|
|
128
|
+
apply({
|
|
129
|
+
...latest.current,
|
|
130
|
+
turns: [...latest.current.turns, {
|
|
131
|
+
...turn,
|
|
132
|
+
problem: fetchErrorMessage(error)
|
|
133
|
+
}]
|
|
134
|
+
});
|
|
135
|
+
} finally {
|
|
136
|
+
if (controller.current === own) {
|
|
137
|
+
controller.current = void 0;
|
|
138
|
+
setRunning(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function close() {
|
|
143
|
+
commitDescription();
|
|
144
|
+
abortGeneration();
|
|
145
|
+
onOpenChange(false);
|
|
146
|
+
}
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (open) return;
|
|
149
|
+
commitDescription();
|
|
150
|
+
abortGeneration();
|
|
151
|
+
}, [open]);
|
|
152
|
+
useEffect(() => () => {
|
|
153
|
+
commitDescription();
|
|
154
|
+
abortGeneration();
|
|
155
|
+
}, []);
|
|
156
|
+
const heading = "text-sm font-medium text-foreground";
|
|
157
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
158
|
+
open,
|
|
159
|
+
onOpenChange: (value) => {
|
|
160
|
+
if (!value) close();
|
|
161
|
+
},
|
|
162
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
163
|
+
className: "max-h-[calc(100vh-2rem)] overflow-y-auto sm:max-w-[min(72rem,calc(100vw-2rem))]",
|
|
164
|
+
"aria-describedby": void 0,
|
|
165
|
+
children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: title ?? t("imageStudio.dialog.title") }) }), /* @__PURE__ */ jsxs("div", {
|
|
166
|
+
className: "flex flex-col gap-3",
|
|
167
|
+
"data-image-studio-dialog": true,
|
|
168
|
+
children: [problem && /* @__PURE__ */ jsx(Alert, {
|
|
169
|
+
variant: "destructive",
|
|
170
|
+
children: /* @__PURE__ */ jsx(AlertTitle, { children: problem })
|
|
171
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
172
|
+
className: "grid gap-4 lg:h-[68vh] lg:grid-cols-[17rem_minmax(0,1fr)_minmax(0,22rem)]",
|
|
173
|
+
children: [
|
|
174
|
+
/* @__PURE__ */ jsxs("section", {
|
|
175
|
+
className: "flex min-h-0 flex-col gap-3 lg:overflow-y-auto",
|
|
176
|
+
"aria-labelledby": `${uid}-description`,
|
|
177
|
+
children: [
|
|
178
|
+
/* @__PURE__ */ jsx("h3", {
|
|
179
|
+
id: `${uid}-description`,
|
|
180
|
+
className: heading,
|
|
181
|
+
children: t("imageStudio.dialog.description.heading")
|
|
182
|
+
}),
|
|
183
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
184
|
+
value: draft,
|
|
185
|
+
rows: 6,
|
|
186
|
+
placeholder: t("imageStudio.dialog.description.placeholder"),
|
|
187
|
+
"aria-label": t("imageStudio.dialog.description.heading"),
|
|
188
|
+
onChange: onDescriptionChange,
|
|
189
|
+
onBlur: commitDescription
|
|
190
|
+
}),
|
|
191
|
+
/* @__PURE__ */ jsx("h3", {
|
|
192
|
+
id: `${uid}-blocks`,
|
|
193
|
+
className: cn(heading, "mt-2"),
|
|
194
|
+
children: t("imageStudio.dialog.blocks.heading")
|
|
195
|
+
}),
|
|
196
|
+
promptBlocks.length ? /* @__PURE__ */ jsx("ul", {
|
|
197
|
+
className: "flex flex-col gap-3",
|
|
198
|
+
"aria-labelledby": `${uid}-blocks`,
|
|
199
|
+
children: promptBlocks.map((block) => /* @__PURE__ */ jsxs("li", {
|
|
200
|
+
className: "flex items-start gap-2",
|
|
201
|
+
children: [/* @__PURE__ */ jsx(Switch, {
|
|
202
|
+
id: `${uid}-block-${block.id}`,
|
|
203
|
+
checked: state.promptBlockIds.includes(block.id),
|
|
204
|
+
"aria-labelledby": `${uid}-block-${block.id}-label`,
|
|
205
|
+
"aria-describedby": `${uid}-block-${block.id}-text`,
|
|
206
|
+
onCheckedChange: () => apply(togglePromptBlock(latest.current, block.id))
|
|
207
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
208
|
+
className: "flex min-w-0 flex-col gap-0.5",
|
|
209
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
210
|
+
id: `${uid}-block-${block.id}-label`,
|
|
211
|
+
htmlFor: `${uid}-block-${block.id}`,
|
|
212
|
+
className: "text-sm font-medium text-foreground",
|
|
213
|
+
children: block.label
|
|
214
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
215
|
+
id: `${uid}-block-${block.id}-text`,
|
|
216
|
+
className: "whitespace-pre-wrap break-words text-xs text-muted-foreground",
|
|
217
|
+
children: block.text
|
|
218
|
+
})]
|
|
219
|
+
})]
|
|
220
|
+
}, block.id))
|
|
221
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
222
|
+
className: "text-sm text-muted-foreground",
|
|
223
|
+
children: t("imageStudio.dialog.blocks.empty")
|
|
224
|
+
})
|
|
225
|
+
]
|
|
226
|
+
}),
|
|
227
|
+
/* @__PURE__ */ jsxs("div", {
|
|
228
|
+
className: "flex min-h-0 flex-col gap-4 lg:overflow-y-auto",
|
|
229
|
+
children: [/* @__PURE__ */ jsxs("section", {
|
|
230
|
+
className: "flex flex-col gap-2 rounded-md border border-dashed border-transparent p-1 data-[dragging=true]:border-primary",
|
|
231
|
+
"data-dragging": dragging,
|
|
232
|
+
"aria-labelledby": `${uid}-references`,
|
|
233
|
+
"data-image-studio-drop": true,
|
|
234
|
+
onDragEnter: (event) => {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
setDragging(true);
|
|
237
|
+
},
|
|
238
|
+
onDragOver: (event) => {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
setDragging(true);
|
|
241
|
+
},
|
|
242
|
+
onDragLeave: () => setDragging(false),
|
|
243
|
+
onDrop,
|
|
244
|
+
children: [
|
|
245
|
+
/* @__PURE__ */ jsxs("div", {
|
|
246
|
+
className: "flex items-center justify-between gap-2",
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ jsx("h3", {
|
|
249
|
+
id: `${uid}-references`,
|
|
250
|
+
className: heading,
|
|
251
|
+
children: t("imageStudio.dialog.references.heading")
|
|
252
|
+
}),
|
|
253
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
254
|
+
size: "xs",
|
|
255
|
+
variant: "outline",
|
|
256
|
+
disabled: uploading > 0,
|
|
257
|
+
onClick: () => fileInput.current?.click(),
|
|
258
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
259
|
+
name: uploading > 0 ? "i-lucide-loader-circle" : "i-lucide-upload",
|
|
260
|
+
className: cn(uploading > 0 && "animate-spin")
|
|
261
|
+
}), t("imageStudio.dialog.references.upload")]
|
|
262
|
+
}),
|
|
263
|
+
/* @__PURE__ */ jsx("input", {
|
|
264
|
+
ref: fileInput,
|
|
265
|
+
type: "file",
|
|
266
|
+
accept: "image/*",
|
|
267
|
+
multiple: true,
|
|
268
|
+
hidden: true,
|
|
269
|
+
"data-image-studio-file": true,
|
|
270
|
+
onChange: onFilesChosen
|
|
271
|
+
})
|
|
272
|
+
]
|
|
273
|
+
}),
|
|
274
|
+
uploading > 0 && /* @__PURE__ */ jsx("p", {
|
|
275
|
+
className: "text-xs text-muted-foreground",
|
|
276
|
+
role: "status",
|
|
277
|
+
children: t("imageStudio.dialog.references.uploading")
|
|
278
|
+
}),
|
|
279
|
+
uploadProblem && /* @__PURE__ */ jsx(Alert, {
|
|
280
|
+
variant: "destructive",
|
|
281
|
+
children: /* @__PURE__ */ jsx(AlertTitle, { children: uploadProblem })
|
|
282
|
+
}),
|
|
283
|
+
state.references.length ? /* @__PURE__ */ jsx("ul", {
|
|
284
|
+
className: "grid grid-cols-2 gap-2 sm:grid-cols-3",
|
|
285
|
+
children: state.references.map((image) => /* @__PURE__ */ jsx(ImageStudioTile, {
|
|
286
|
+
image,
|
|
287
|
+
selected: state.selected === image.id,
|
|
288
|
+
onToggle: () => apply(toggleImage(latest.current, image.id)),
|
|
289
|
+
onSelect: () => apply(select(latest.current, image.id)),
|
|
290
|
+
onRemove: () => apply(removeImage(latest.current, image.id))
|
|
291
|
+
}, image.id))
|
|
292
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
293
|
+
className: "text-sm text-muted-foreground",
|
|
294
|
+
children: t("imageStudio.dialog.references.drop")
|
|
295
|
+
})
|
|
296
|
+
]
|
|
297
|
+
}), /* @__PURE__ */ jsxs("section", {
|
|
298
|
+
className: "flex flex-col gap-2 p-1",
|
|
299
|
+
"aria-labelledby": `${uid}-generations`,
|
|
300
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
301
|
+
id: `${uid}-generations`,
|
|
302
|
+
className: heading,
|
|
303
|
+
children: t("imageStudio.dialog.generations.heading")
|
|
304
|
+
}), state.generations.length ? /* @__PURE__ */ jsx("ul", {
|
|
305
|
+
className: "grid grid-cols-2 gap-2 sm:grid-cols-3",
|
|
306
|
+
children: state.generations.map((image) => /* @__PURE__ */ jsx(ImageStudioTile, {
|
|
307
|
+
image,
|
|
308
|
+
selected: state.selected === image.id,
|
|
309
|
+
onToggle: () => apply(toggleImage(latest.current, image.id)),
|
|
310
|
+
onSelect: () => apply(select(latest.current, image.id)),
|
|
311
|
+
onRemove: () => apply(removeImage(latest.current, image.id))
|
|
312
|
+
}, image.id))
|
|
313
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
314
|
+
className: "text-sm text-muted-foreground",
|
|
315
|
+
children: t("imageStudio.dialog.generations.empty")
|
|
316
|
+
})]
|
|
317
|
+
})]
|
|
318
|
+
}),
|
|
319
|
+
/* @__PURE__ */ jsxs("div", {
|
|
320
|
+
className: "flex min-h-0 flex-col gap-3",
|
|
321
|
+
children: [/* @__PURE__ */ jsxs("section", {
|
|
322
|
+
className: "flex flex-col gap-2",
|
|
323
|
+
"aria-labelledby": `${uid}-preview`,
|
|
324
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
325
|
+
id: `${uid}-preview`,
|
|
326
|
+
className: heading,
|
|
327
|
+
children: t("imageStudio.dialog.preview.heading")
|
|
328
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
329
|
+
className: "flex aspect-square items-center justify-center overflow-hidden rounded-md border border-border bg-muted",
|
|
330
|
+
children: picture ? /* @__PURE__ */ jsx("img", {
|
|
331
|
+
src: picture.url,
|
|
332
|
+
alt: picture.title || t("imageStudio.dialog.tile.untitled"),
|
|
333
|
+
className: "size-full object-contain",
|
|
334
|
+
"data-image-studio-preview": true
|
|
335
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
336
|
+
className: "text-sm text-muted-foreground",
|
|
337
|
+
children: t("imageStudio.dialog.preview.empty")
|
|
338
|
+
})
|
|
339
|
+
})]
|
|
340
|
+
}), /* @__PURE__ */ jsx(ImageStudioChat, {
|
|
341
|
+
turns: state.turns,
|
|
342
|
+
images,
|
|
343
|
+
running,
|
|
344
|
+
emptyAllowed: draft.trim() !== "",
|
|
345
|
+
onSend: (instruction) => void generate(instruction)
|
|
346
|
+
})]
|
|
347
|
+
})
|
|
348
|
+
]
|
|
349
|
+
})]
|
|
350
|
+
})]
|
|
351
|
+
})
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
export { ImageStudioDialog };
|
|
356
|
+
|
|
357
|
+
//# sourceMappingURL=ImageStudioDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageStudioDialog.js","names":[],"sources":["../../../../src/features/image-studio/react/ImageStudioDialog.tsx"],"sourcesContent":["import { type ChangeEvent, type DragEvent, useEffect, useId, useRef, useState } from 'react'\nimport { fetchErrorMessage } from '../../../core'\nimport { Icon } from '../../../react/Icon'\nimport { Alert, AlertTitle } from '../../../react/ui/alert'\nimport { Button } from '../../../react/ui/button'\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../react/ui/dialog'\nimport { Switch } from '../../../react/ui/switch'\nimport { Textarea } from '../../../react/ui/textarea'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport {\n addGeneration,\n addReference,\n type ImageStudioState,\n type ImageStudioTurn,\n removeImage,\n select,\n selectedImage,\n setDescription,\n toggleImage,\n togglePromptBlock,\n} from '../state'\nimport { ImageStudioChat } from './ImageStudioChat'\nimport { ImageStudioTile } from './ImageStudioTile'\nimport type { ImageStudioReactStore } from './store'\n\nconst COMMIT_DELAY_MS = 500\n\n/**\n * The Bild-Studio: description and Prompt-Bausteine on the left, references and previous generations in the middle,\n * the block's image and the chat on the right. The dialog applies the reducers of `../state` to `store.state` and\n * hands every result to `store.update`; the store only generates, uploads and persists (see `ImageStudioReactStore`).\n *\n * The description is committed 500 ms after the last keystroke and at once on blur, on „Erzeugen“ and on close, so the\n * host never builds a prompt from a stale text. Closing (or unmounting) aborts a running generation; an aborted\n * generation adds no turn, a failed one adds a turn with the problem.\n */\nexport function ImageStudioDialog({ open, onOpenChange, store, title }: {\n open: boolean\n onOpenChange: (open: boolean) => void\n store: ImageStudioReactStore\n title?: string\n}) {\n const { t } = useEditorI18n()\n const uid = useId()\n const { state, promptBlocks, problem } = store\n const images = [...state.generations, ...state.references]\n const picture = selectedImage(state)\n\n // The latest store and the latest state: async continuations (an upload, a generation) must build on the newest\n // state, also when a render has not caught up with the `update` just made. A new `store.state` replaces it.\n const storeRef = useRef(store)\n storeRef.current = store\n const latest = useRef(state)\n const seen = useRef(state)\n if (seen.current !== state) {\n seen.current = state\n latest.current = state\n }\n const apply = (next: ImageStudioState) => {\n latest.current = next\n storeRef.current.update(next)\n }\n\n // --- Description --------------------------------------------------------------------------------------------------\n\n const [draft, setDraft] = useState(state.description)\n const draftRef = useRef(draft)\n /** The description as last committed or received: the host's echo of our own commit must not overwrite a newer draft. */\n const known = useRef(state.description)\n const commitTimer = useRef<ReturnType<typeof setTimeout>>(undefined)\n\n function commitDescription() {\n clearTimeout(commitTimer.current)\n commitTimer.current = undefined\n if (draftRef.current === latest.current.description) return\n known.current = draftRef.current\n apply(setDescription(latest.current, draftRef.current))\n }\n\n function onDescriptionChange(event: ChangeEvent<HTMLTextAreaElement>) {\n const text = event.target.value\n draftRef.current = text\n setDraft(text)\n if (text === latest.current.description) return\n clearTimeout(commitTimer.current)\n commitTimer.current = setTimeout(commitDescription, COMMIT_DELAY_MS)\n }\n\n // A change of the host (undo, another tab) shows unless the user has typed something that is not committed yet.\n useEffect(() => {\n if (state.description === known.current) return\n known.current = state.description\n if (commitTimer.current === undefined) {\n draftRef.current = state.description\n setDraft(state.description)\n }\n }, [state.description])\n\n // --- References: upload and drop ----------------------------------------------------------------------------------\n\n const fileInput = useRef<HTMLInputElement>(null)\n const [uploading, setUploading] = useState(0)\n const [uploadProblem, setUploadProblem] = useState<string>()\n const [dragging, setDragging] = useState(false)\n\n async function upload(files: readonly File[]) {\n const pictures = files.filter(file => file.type.startsWith('image/'))\n if (!pictures.length) return\n setUploadProblem(undefined)\n setUploading(count => count + pictures.length)\n await Promise.all(pictures.map(async (file) => {\n try {\n const image = await storeRef.current.upload(file)\n apply(addReference(latest.current, image))\n }\n catch (error) {\n setUploadProblem(t('imageStudio.dialog.references.failed', { message: fetchErrorMessage(error) }))\n }\n finally {\n setUploading(count => count - 1)\n }\n }))\n }\n\n function onFilesChosen(event: ChangeEvent<HTMLInputElement>) {\n const input = event.target\n const files = [...(input.files ?? [])]\n input.value = '' // the same file can be chosen again\n void upload(files)\n }\n\n function onDrop(event: DragEvent) {\n event.preventDefault()\n setDragging(false)\n void upload([...event.dataTransfer.files])\n }\n\n // --- Generation ---------------------------------------------------------------------------------------------------\n\n const [running, setRunning] = useState(false)\n const controller = useRef<AbortController>(undefined)\n\n function abortGeneration() {\n controller.current?.abort()\n controller.current = undefined\n setRunning(false)\n }\n\n async function generate(instruction: string) {\n if (controller.current) return\n commitDescription()\n const own = new AbortController()\n controller.current = own\n setRunning(true)\n const turn: ImageStudioTurn = { id: crypto.randomUUID(), instruction, imageId: null, createdAt: new Date().toISOString() }\n try {\n const image = await storeRef.current.generate(instruction, own.signal)\n if (own.signal.aborted) return\n apply(addGeneration(latest.current, image, turn))\n }\n catch (error) {\n if (own.signal.aborted) return\n apply({ ...latest.current, turns: [...latest.current.turns, { ...turn, problem: fetchErrorMessage(error) }] })\n }\n finally {\n if (controller.current === own) {\n controller.current = undefined\n setRunning(false)\n }\n }\n }\n\n // --- Closing ------------------------------------------------------------------------------------------------------\n\n function close() {\n commitDescription()\n abortGeneration()\n onOpenChange(false)\n }\n\n // The host closes the dialog itself, or the component goes: commit what was typed, stop what runs.\n useEffect(() => {\n if (open) return\n commitDescription()\n abortGeneration()\n }, [open])\n useEffect(() => () => {\n commitDescription()\n abortGeneration()\n }, [])\n\n const heading = 'text-sm font-medium text-foreground'\n\n return (\n <Dialog open={open} onOpenChange={(value) => { if (!value) close() }}>\n <DialogContent className=\"max-h-[calc(100vh-2rem)] overflow-y-auto sm:max-w-[min(72rem,calc(100vw-2rem))]\" aria-describedby={undefined}>\n <DialogHeader>\n <DialogTitle>{title ?? t('imageStudio.dialog.title')}</DialogTitle>\n </DialogHeader>\n <div className=\"flex flex-col gap-3\" data-image-studio-dialog>\n {problem && (\n <Alert variant=\"destructive\">\n <AlertTitle>{problem}</AlertTitle>\n </Alert>\n )}\n <div className=\"grid gap-4 lg:h-[68vh] lg:grid-cols-[17rem_minmax(0,1fr)_minmax(0,22rem)]\">\n {/* Left: what the picture shows. */}\n <section className=\"flex min-h-0 flex-col gap-3 lg:overflow-y-auto\" aria-labelledby={`${uid}-description`}>\n <h3 id={`${uid}-description`} className={heading}>{t('imageStudio.dialog.description.heading')}</h3>\n <Textarea\n value={draft}\n rows={6}\n placeholder={t('imageStudio.dialog.description.placeholder')}\n aria-label={t('imageStudio.dialog.description.heading')}\n onChange={onDescriptionChange}\n onBlur={commitDescription}\n />\n <h3 id={`${uid}-blocks`} className={cn(heading, 'mt-2')}>{t('imageStudio.dialog.blocks.heading')}</h3>\n {promptBlocks.length\n ? (\n <ul className=\"flex flex-col gap-3\" aria-labelledby={`${uid}-blocks`}>\n {promptBlocks.map(block => (\n <li key={block.id} className=\"flex items-start gap-2\">\n <Switch\n id={`${uid}-block-${block.id}`}\n checked={state.promptBlockIds.includes(block.id)}\n aria-labelledby={`${uid}-block-${block.id}-label`}\n aria-describedby={`${uid}-block-${block.id}-text`}\n onCheckedChange={() => apply(togglePromptBlock(latest.current, block.id))}\n />\n <div className=\"flex min-w-0 flex-col gap-0.5\">\n <label id={`${uid}-block-${block.id}-label`} htmlFor={`${uid}-block-${block.id}`} className=\"text-sm font-medium text-foreground\">\n {block.label}\n </label>\n <p id={`${uid}-block-${block.id}-text`} className=\"whitespace-pre-wrap break-words text-xs text-muted-foreground\">\n {block.text}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )\n : <p className=\"text-sm text-muted-foreground\">{t('imageStudio.dialog.blocks.empty')}</p>}\n </section>\n\n {/* Middle: references and previous generations. */}\n <div className=\"flex min-h-0 flex-col gap-4 lg:overflow-y-auto\">\n <section\n className=\"flex flex-col gap-2 rounded-md border border-dashed border-transparent p-1 data-[dragging=true]:border-primary\"\n data-dragging={dragging}\n aria-labelledby={`${uid}-references`}\n data-image-studio-drop\n onDragEnter={(event) => { event.preventDefault(); setDragging(true) }}\n onDragOver={(event) => { event.preventDefault(); setDragging(true) }}\n onDragLeave={() => setDragging(false)}\n onDrop={onDrop}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <h3 id={`${uid}-references`} className={heading}>{t('imageStudio.dialog.references.heading')}</h3>\n <Button size=\"xs\" variant=\"outline\" disabled={uploading > 0} onClick={() => fileInput.current?.click()}>\n <Icon name={uploading > 0 ? 'i-lucide-loader-circle' : 'i-lucide-upload'} className={cn(uploading > 0 && 'animate-spin')} />\n {t('imageStudio.dialog.references.upload')}\n </Button>\n <input ref={fileInput} type=\"file\" accept=\"image/*\" multiple hidden data-image-studio-file onChange={onFilesChosen} />\n </div>\n {uploading > 0 && <p className=\"text-xs text-muted-foreground\" role=\"status\">{t('imageStudio.dialog.references.uploading')}</p>}\n {uploadProblem && (\n <Alert variant=\"destructive\">\n <AlertTitle>{uploadProblem}</AlertTitle>\n </Alert>\n )}\n {state.references.length\n ? (\n <ul className=\"grid grid-cols-2 gap-2 sm:grid-cols-3\">\n {state.references.map(image => (\n <ImageStudioTile\n key={image.id}\n image={image}\n selected={state.selected === image.id}\n onToggle={() => apply(toggleImage(latest.current, image.id))}\n onSelect={() => apply(select(latest.current, image.id))}\n onRemove={() => apply(removeImage(latest.current, image.id))}\n />\n ))}\n </ul>\n )\n : <p className=\"text-sm text-muted-foreground\">{t('imageStudio.dialog.references.drop')}</p>}\n </section>\n\n <section className=\"flex flex-col gap-2 p-1\" aria-labelledby={`${uid}-generations`}>\n <h3 id={`${uid}-generations`} className={heading}>{t('imageStudio.dialog.generations.heading')}</h3>\n {state.generations.length\n ? (\n <ul className=\"grid grid-cols-2 gap-2 sm:grid-cols-3\">\n {state.generations.map(image => (\n <ImageStudioTile\n key={image.id}\n image={image}\n selected={state.selected === image.id}\n onToggle={() => apply(toggleImage(latest.current, image.id))}\n onSelect={() => apply(select(latest.current, image.id))}\n onRemove={() => apply(removeImage(latest.current, image.id))}\n />\n ))}\n </ul>\n )\n : <p className=\"text-sm text-muted-foreground\">{t('imageStudio.dialog.generations.empty')}</p>}\n </section>\n </div>\n\n {/* Right: the block's image and the chat. */}\n <div className=\"flex min-h-0 flex-col gap-3\">\n <section className=\"flex flex-col gap-2\" aria-labelledby={`${uid}-preview`}>\n <h3 id={`${uid}-preview`} className={heading}>{t('imageStudio.dialog.preview.heading')}</h3>\n <div className=\"flex aspect-square items-center justify-center overflow-hidden rounded-md border border-border bg-muted\">\n {picture\n ? (\n <img\n src={picture.url}\n alt={picture.title || t('imageStudio.dialog.tile.untitled')}\n className=\"size-full object-contain\"\n data-image-studio-preview\n />\n )\n : <p className=\"text-sm text-muted-foreground\">{t('imageStudio.dialog.preview.empty')}</p>}\n </div>\n </section>\n <ImageStudioChat\n turns={state.turns}\n images={images}\n running={running}\n emptyAllowed={draft.trim() !== ''}\n onSend={instruction => void generate(instruction)}\n />\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useEditorI18n } from "../../../react/use-editor-i18n.js";
|
|
2
|
+
import { Icon } from "../../../react/Icon.js";
|
|
3
|
+
import { Button } from "../../../react/ui/button.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/features/image-studio/react/ImageStudioTile.tsx
|
|
6
|
+
/**
|
|
7
|
+
* One reference or previous generation of the Bild-Studio. A click on the picture flips `active` (goes into the next
|
|
8
|
+
* generation, ring via `data-active`), „Als Bild verwenden“ makes it the block's image, „Entfernen“ drops it from the block.
|
|
9
|
+
*/
|
|
10
|
+
function ImageStudioTile({ image, selected, onToggle, onSelect, onRemove }) {
|
|
11
|
+
const { t } = useEditorI18n();
|
|
12
|
+
const title = image.title || t("imageStudio.dialog.tile.untitled");
|
|
13
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
14
|
+
className: "flex flex-col gap-1.5 rounded-md border border-border p-1.5 data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-primary",
|
|
15
|
+
"data-active": image.active,
|
|
16
|
+
"data-selected": selected,
|
|
17
|
+
"data-image-studio-tile": true,
|
|
18
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
19
|
+
type: "button",
|
|
20
|
+
className: "block overflow-hidden rounded bg-muted focus-visible:outline-2 focus-visible:outline-primary",
|
|
21
|
+
"aria-pressed": image.active,
|
|
22
|
+
"aria-label": t("imageStudio.dialog.tile.toggle", { title }),
|
|
23
|
+
onClick: onToggle,
|
|
24
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
25
|
+
src: image.url,
|
|
26
|
+
alt: title,
|
|
27
|
+
className: "aspect-square w-full object-cover",
|
|
28
|
+
draggable: false
|
|
29
|
+
})
|
|
30
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
31
|
+
className: "flex items-center justify-between gap-1",
|
|
32
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
33
|
+
size: "xs",
|
|
34
|
+
variant: selected ? "default" : "outline",
|
|
35
|
+
"aria-pressed": selected,
|
|
36
|
+
onClick: onSelect,
|
|
37
|
+
children: [selected && /* @__PURE__ */ jsx(Icon, { name: "i-lucide-check" }), t("imageStudio.dialog.tile.use")]
|
|
38
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
39
|
+
size: "icon-xs",
|
|
40
|
+
variant: "ghost",
|
|
41
|
+
"aria-label": t("imageStudio.dialog.tile.remove"),
|
|
42
|
+
onClick: onRemove,
|
|
43
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "i-lucide-trash-2" })
|
|
44
|
+
})]
|
|
45
|
+
})]
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { ImageStudioTile };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=ImageStudioTile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageStudioTile.js","names":[],"sources":["../../../../src/features/image-studio/react/ImageStudioTile.tsx"],"sourcesContent":["import { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport type { ImageStudioImage } from '../state'\n\n/**\n * One reference or previous generation of the Bild-Studio. A click on the picture flips `active` (goes into the next\n * generation, ring via `data-active`), „Als Bild verwenden“ makes it the block's image, „Entfernen“ drops it from the block.\n */\nexport function ImageStudioTile({ image, selected, onToggle, onSelect, onRemove }: {\n image: ImageStudioImage\n selected: boolean\n onToggle: () => void\n onSelect: () => void\n onRemove: () => void\n}) {\n const { t } = useEditorI18n()\n const title = image.title || t('imageStudio.dialog.tile.untitled')\n return (\n <li\n className=\"flex flex-col gap-1.5 rounded-md border border-border p-1.5 data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-primary\"\n data-active={image.active}\n data-selected={selected}\n data-image-studio-tile\n >\n <button\n type=\"button\"\n className=\"block overflow-hidden rounded bg-muted focus-visible:outline-2 focus-visible:outline-primary\"\n aria-pressed={image.active}\n aria-label={t('imageStudio.dialog.tile.toggle', { title })}\n onClick={onToggle}\n >\n <img src={image.url} alt={title} className=\"aspect-square w-full object-cover\" draggable={false} />\n </button>\n <div className=\"flex items-center justify-between gap-1\">\n <Button size=\"xs\" variant={selected ? 'default' : 'outline'} aria-pressed={selected} onClick={onSelect}>\n {selected && <Icon name=\"i-lucide-check\" />}\n {t('imageStudio.dialog.tile.use')}\n </Button>\n <Button size=\"icon-xs\" variant=\"ghost\" aria-label={t('imageStudio.dialog.tile.remove')} onClick={onRemove}>\n <Icon name=\"i-lucide-trash-2\" />\n </Button>\n </div>\n </li>\n )\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,gBAAgB,EAAE,OAAO,UAAU,UAAU,UAAU,YAMpE;CACD,MAAM,EAAE,MAAM,cAAc;CAC5B,MAAM,QAAQ,MAAM,SAAS,EAAE,kCAAkC;CACjE,OACE,qBAAC,MAAD;EACE,WAAU;EACV,eAAa,MAAM;EACnB,iBAAe;EACf,0BAAA;EAJF,UAAA,CAME,oBAAC,UAAD;GACE,MAAK;GACL,WAAU;GACV,gBAAc,MAAM;GACpB,cAAY,EAAE,kCAAkC,EAAE,MAAM,CAAC;GACzD,SAAS;GAET,UAAA,oBAAC,OAAD;IAAK,KAAK,MAAM;IAAK,KAAK;IAAO,WAAU;IAAoC,WAAW;GAAQ,CAAA;EAC5F,CAAA,GACR,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,qBAAC,QAAD;IAAQ,MAAK;IAAK,SAAS,WAAW,YAAY;IAAW,gBAAc;IAAU,SAAS;IAA9F,UAAA,CACG,YAAY,oBAAC,MAAD,EAAM,MAAK,iBAAkB,CAAA,GACzC,EAAE,6BAA6B,CAC1B;GACR,CAAA,GAAA,oBAAC,QAAD;IAAQ,MAAK;IAAU,SAAQ;IAAQ,cAAY,EAAE,gCAAgC;IAAG,SAAS;IAC/F,UAAA,oBAAC,MAAD,EAAM,MAAK,mBAAoB,CAAA;GACzB,CAAA,CACL;EACH,CAAA,CAAA;;AAER"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { editorEntryOf } from "../../../core/editor-ui.js";
|
|
2
|
+
import "../../media/assets.js";
|
|
3
|
+
import { useEditorI18n } from "../../../react/use-editor-i18n.js";
|
|
4
|
+
import { cn } from "../../../react/ui/utils.js";
|
|
5
|
+
import { Icon } from "../../../react/Icon.js";
|
|
6
|
+
import { Button } from "../../../react/ui/button.js";
|
|
7
|
+
import { useNotifier } from "../../../react/use-notifier.js";
|
|
8
|
+
import { normalizeImageStudioState } from "../state.js";
|
|
9
|
+
import { IMAGE_STUDIO_OPEN } from "../feature.js";
|
|
10
|
+
import { ImageStudioDialog } from "./ImageStudioDialog.js";
|
|
11
|
+
import { useImageStudioNodeStore } from "./store.js";
|
|
12
|
+
import { useEffect, useRef, useState } from "react";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { NodeViewWrapper } from "@tiptap/react";
|
|
15
|
+
//#region src/features/image-studio/react/ImageStudioView.tsx
|
|
16
|
+
/** The studio of one block: its store (and with it the Prompt-Bausteine list) exists from the first opening on. */
|
|
17
|
+
function NodeStudio({ editor, getPos, backend, documentId, open, onOpenChange }) {
|
|
18
|
+
const { t } = useEditorI18n();
|
|
19
|
+
const store = useImageStudioNodeStore({
|
|
20
|
+
editor,
|
|
21
|
+
getPos,
|
|
22
|
+
backend,
|
|
23
|
+
documentId
|
|
24
|
+
});
|
|
25
|
+
return store ? /* @__PURE__ */ jsx(ImageStudioDialog, {
|
|
26
|
+
open,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
store,
|
|
29
|
+
title: t("imageStudio.node.dialogTitle")
|
|
30
|
+
}) : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The `imageStudio` block: the selected picture (or the description as a placeholder), the description as caption and the
|
|
34
|
+
* „Bild-Studio öffnen“ button. The studio dialog opens from that button, on double-click and through the block action
|
|
35
|
+
* `image-studio.open` (the `IMAGE_STUDIO_OPEN` event on the block's node view element); a read-only editor opens none,
|
|
36
|
+
* a backend without image generation explains itself.
|
|
37
|
+
*/
|
|
38
|
+
function ImageStudioView({ node, selected, editor, getPos }) {
|
|
39
|
+
const { t } = useEditorI18n();
|
|
40
|
+
const notify = useNotifier(editor);
|
|
41
|
+
const description = normalizeImageStudioState(node.attrs.studio).description.trim();
|
|
42
|
+
const [target, setTarget] = useState();
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
function openStudio() {
|
|
45
|
+
if (!editor.isEditable) return;
|
|
46
|
+
const ctx = editorEntryOf(editor)?.ctx;
|
|
47
|
+
const backend = ctx?.backend.images;
|
|
48
|
+
if (!ctx || !backend) {
|
|
49
|
+
notify({
|
|
50
|
+
id: "image-studio.unavailable",
|
|
51
|
+
title: t("imageStudio.action.notConfigured"),
|
|
52
|
+
kind: "warning"
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setTarget((previous) => previous?.backend === backend ? previous : {
|
|
57
|
+
backend,
|
|
58
|
+
documentId: ctx.documentId ?? "scratch"
|
|
59
|
+
});
|
|
60
|
+
setOpen(true);
|
|
61
|
+
}
|
|
62
|
+
const figure = useRef(null);
|
|
63
|
+
const openRef = useRef(openStudio);
|
|
64
|
+
openRef.current = openStudio;
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const dom = figure.current?.parentElement;
|
|
67
|
+
if (!dom) return;
|
|
68
|
+
const onOpen = () => openRef.current();
|
|
69
|
+
dom.addEventListener(IMAGE_STUDIO_OPEN, onOpen);
|
|
70
|
+
return () => dom.removeEventListener(IMAGE_STUDIO_OPEN, onOpen);
|
|
71
|
+
}, []);
|
|
72
|
+
return /* @__PURE__ */ jsxs(NodeViewWrapper, {
|
|
73
|
+
ref: figure,
|
|
74
|
+
as: "figure",
|
|
75
|
+
className: cn("my-3 flex flex-col gap-2 rounded-md", selected && "outline-2 outline-offset-2 outline-primary"),
|
|
76
|
+
"data-image-studio-block": true,
|
|
77
|
+
onDoubleClick: openStudio,
|
|
78
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
79
|
+
className: "flex flex-col gap-2",
|
|
80
|
+
contentEditable: false,
|
|
81
|
+
children: [
|
|
82
|
+
node.attrs.src ? /* @__PURE__ */ jsx("img", {
|
|
83
|
+
src: node.attrs.src,
|
|
84
|
+
alt: node.attrs.alt || description,
|
|
85
|
+
className: "m-0 max-w-full rounded-md"
|
|
86
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
87
|
+
className: "flex min-h-32 flex-col items-center justify-center gap-2 rounded-md border border-dashed border-border p-4 text-center text-sm",
|
|
88
|
+
children: [/* @__PURE__ */ jsx(Icon, {
|
|
89
|
+
name: "i-lucide-image-plus",
|
|
90
|
+
className: "size-6 text-muted-foreground/60"
|
|
91
|
+
}), description ? /* @__PURE__ */ jsx("span", {
|
|
92
|
+
className: "whitespace-pre-wrap text-muted-foreground",
|
|
93
|
+
children: description
|
|
94
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
95
|
+
className: "text-muted-foreground/60",
|
|
96
|
+
children: t("imageStudio.node.noDescription")
|
|
97
|
+
})]
|
|
98
|
+
}),
|
|
99
|
+
node.attrs.src && description && /* @__PURE__ */ jsx("figcaption", {
|
|
100
|
+
className: "whitespace-pre-wrap px-1 text-sm text-muted-foreground",
|
|
101
|
+
children: description
|
|
102
|
+
}),
|
|
103
|
+
editor.isEditable && /* @__PURE__ */ jsx("div", {
|
|
104
|
+
className: "flex",
|
|
105
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
106
|
+
size: "xs",
|
|
107
|
+
variant: "secondary",
|
|
108
|
+
onClick: openStudio,
|
|
109
|
+
children: [/* @__PURE__ */ jsx(Icon, { name: "i-lucide-wand-sparkles" }), t("imageStudio.node.open")]
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
}), target && /* @__PURE__ */ jsx(NodeStudio, {
|
|
114
|
+
editor,
|
|
115
|
+
getPos,
|
|
116
|
+
backend: target.backend,
|
|
117
|
+
documentId: target.documentId,
|
|
118
|
+
open,
|
|
119
|
+
onOpenChange: setOpen
|
|
120
|
+
})]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { ImageStudioView };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=ImageStudioView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageStudioView.js","names":[],"sources":["../../../../src/features/image-studio/react/ImageStudioView.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { NodeViewWrapper, type NodeViewProps } from '@tiptap/react'\nimport { useEffect, useRef, useState } from 'react'\nimport { editorEntryOf } from '../../../core'\nimport type { EditorBackend } from '../../../core/backend'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport { useNotifier } from '../../../react/use-notifier'\nimport { SCRATCH_DOCUMENT_ID } from '../../media/assets'\nimport { IMAGE_STUDIO_OPEN } from '../feature'\nimport { normalizeImageStudioState } from '../state'\nimport { ImageStudioDialog } from './ImageStudioDialog'\nimport { useImageStudioNodeStore } from './store'\n\n/** The studio of one block: its store (and with it the Prompt-Bausteine list) exists from the first opening on. */\nfunction NodeStudio({ editor, getPos, backend, documentId, open, onOpenChange }: {\n editor: Editor\n getPos: NodeViewProps['getPos']\n backend: NonNullable<EditorBackend['images']>\n documentId: string\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n const { t } = useEditorI18n()\n const store = useImageStudioNodeStore({ editor, getPos, backend, documentId })\n return store\n ? <ImageStudioDialog open={open} onOpenChange={onOpenChange} store={store} title={t('imageStudio.node.dialogTitle')} />\n : null\n}\n\n/**\n * The `imageStudio` block: the selected picture (or the description as a placeholder), the description as caption and the\n * „Bild-Studio öffnen“ button. The studio dialog opens from that button, on double-click and through the block action\n * `image-studio.open` (the `IMAGE_STUDIO_OPEN` event on the block's node view element); a read-only editor opens none,\n * a backend without image generation explains itself.\n */\nexport function ImageStudioView({ node, selected, editor, getPos }: NodeViewProps) {\n const { t } = useEditorI18n()\n const notify = useNotifier(editor)\n const studio = normalizeImageStudioState(node.attrs.studio)\n const description = studio.description.trim()\n\n const [target, setTarget] = useState<{ backend: NonNullable<EditorBackend['images']>, documentId: string }>()\n const [open, setOpen] = useState(false)\n\n function openStudio() {\n if (!editor.isEditable) return\n const ctx = editorEntryOf(editor)?.ctx\n const backend = ctx?.backend.images\n if (!ctx || !backend) {\n notify({ id: 'image-studio.unavailable', title: t('imageStudio.action.notConfigured'), kind: 'warning' })\n return\n }\n setTarget(previous => previous?.backend === backend ? previous : { backend, documentId: ctx.documentId ?? SCRATCH_DOCUMENT_ID })\n setOpen(true)\n }\n\n // The block action dispatches `IMAGE_STUDIO_OPEN` on the node view's element, the parent of the wrapper rendered here.\n const figure = useRef<HTMLElement>(null)\n const openRef = useRef(openStudio)\n openRef.current = openStudio\n useEffect(() => {\n const dom = figure.current?.parentElement\n if (!dom) return\n const onOpen = () => openRef.current()\n dom.addEventListener(IMAGE_STUDIO_OPEN, onOpen)\n return () => dom.removeEventListener(IMAGE_STUDIO_OPEN, onOpen)\n }, [])\n\n return (\n <NodeViewWrapper\n ref={figure}\n as=\"figure\"\n className={cn('my-3 flex flex-col gap-2 rounded-md', selected && 'outline-2 outline-offset-2 outline-primary')}\n data-image-studio-block\n onDoubleClick={openStudio}\n >\n <div className=\"flex flex-col gap-2\" contentEditable={false}>\n {node.attrs.src\n ? <img src={node.attrs.src} alt={node.attrs.alt || description} className=\"m-0 max-w-full rounded-md\" />\n : (\n <div className=\"flex min-h-32 flex-col items-center justify-center gap-2 rounded-md border border-dashed border-border p-4 text-center text-sm\">\n <Icon name=\"i-lucide-image-plus\" className=\"size-6 text-muted-foreground/60\" />\n {description\n ? <span className=\"whitespace-pre-wrap text-muted-foreground\">{description}</span>\n : <span className=\"text-muted-foreground/60\">{t('imageStudio.node.noDescription')}</span>}\n </div>\n )}\n {node.attrs.src && description && (\n <figcaption className=\"whitespace-pre-wrap px-1 text-sm text-muted-foreground\">{description}</figcaption>\n )}\n {editor.isEditable && (\n <div className=\"flex\">\n <Button size=\"xs\" variant=\"secondary\" onClick={openStudio}>\n <Icon name=\"i-lucide-wand-sparkles\" />\n {t('imageStudio.node.open')}\n </Button>\n </div>\n )}\n </div>\n {target && (\n <NodeStudio\n editor={editor}\n getPos={getPos}\n backend={target.backend}\n documentId={target.documentId}\n open={open}\n onOpenChange={setOpen}\n />\n )}\n </NodeViewWrapper>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,SAAS,WAAW,EAAE,QAAQ,QAAQ,SAAS,YAAY,MAAM,gBAO9D;CACD,MAAM,EAAE,MAAM,cAAc;CAC5B,MAAM,QAAQ,wBAAwB;EAAE;EAAQ;EAAQ;EAAS;CAAW,CAAC;CAC7E,OAAO,QACH,oBAAC,mBAAD;EAAyB;EAAoB;EAAqB;EAAO,OAAO,EAAE,8BAA8B;CAAI,CAAA,IACpH;AACN;;;;;;;AAQA,SAAgB,gBAAgB,EAAE,MAAM,UAAU,QAAQ,UAAyB;CACjF,MAAM,EAAE,MAAM,cAAc;CAC5B,MAAM,SAAS,YAAY,MAAM;CAEjC,MAAM,cADS,0BAA0B,KAAK,MAAM,MAC3B,CAAC,CAAC,YAAY,KAAK;CAE5C,MAAM,CAAC,QAAQ,aAAa,SAAgF;CAC5G,MAAM,CAAC,MAAM,WAAW,SAAS,KAAK;CAEtC,SAAS,aAAa;EACpB,IAAI,CAAC,OAAO,YAAY;EACxB,MAAM,MAAM,cAAc,MAAM,CAAC,EAAE;EACnC,MAAM,UAAU,KAAK,QAAQ;EAC7B,IAAI,CAAC,OAAO,CAAC,SAAS;GACpB,OAAO;IAAE,IAAI;IAA4B,OAAO,EAAE,kCAAkC;IAAG,MAAM;GAAU,CAAC;GACxG;EACF;EACA,WAAU,aAAY,UAAU,YAAY,UAAU,WAAW;GAAE;GAAS,YAAY,IAAI,cAAA;EAAkC,CAAC;EAC/H,QAAQ,IAAI;CACd;CAGA,MAAM,SAAS,OAAoB,IAAI;CACvC,MAAM,UAAU,OAAO,UAAU;CACjC,QAAQ,UAAU;CAClB,gBAAgB;EACd,MAAM,MAAM,OAAO,SAAS;EAC5B,IAAI,CAAC,KAAK;EACV,MAAM,eAAe,QAAQ,QAAQ;EACrC,IAAI,iBAAiB,mBAAmB,MAAM;EAC9C,aAAa,IAAI,oBAAoB,mBAAmB,MAAM;CAChE,GAAG,CAAC,CAAC;CAEL,OACE,qBAAC,iBAAD;EACE,KAAK;EACL,IAAG;EACH,WAAW,GAAG,uCAAuC,YAAY,4CAA4C;EAC7G,2BAAA;EACA,eAAe;EALjB,UAAA,CAOE,qBAAC,OAAD;GAAK,WAAU;GAAsB,iBAAiB;GAAtD,UAAA;IACG,KAAK,MAAM,MACR,oBAAC,OAAD;KAAK,KAAK,KAAK,MAAM;KAAK,KAAK,KAAK,MAAM,OAAO;KAAa,WAAU;IAA6B,CAAA,IAEnG,qBAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,oBAAC,MAAD;MAAM,MAAK;MAAsB,WAAU;KAAmC,CAAA,GAC7E,cACG,oBAAC,QAAD;MAAM,WAAU;MAA6C,UAAA;KAAkB,CAAA,IAC/E,oBAAC,QAAD;MAAM,WAAU;MAA4B,UAAA,EAAE,gCAAgC;KAAQ,CAAA,CACvF;;IAEV,KAAK,MAAM,OAAO,eACjB,oBAAC,cAAD;KAAY,WAAU;KAA0D,UAAA;IAAwB,CAAA;IAEzG,OAAO,cACN,oBAAC,OAAD;KAAK,WAAU;KACb,UAAA,qBAAC,QAAD;MAAQ,MAAK;MAAK,SAAQ;MAAY,SAAS;MAA/C,UAAA,CACE,oBAAC,MAAD,EAAM,MAAK,yBAA0B,CAAA,GACpC,EAAE,uBAAuB,CACpB;;IACL,CAAA;GAEJ;EACJ,CAAA,GAAA,UACC,oBAAC,YAAD;GACU;GACA;GACR,SAAS,OAAO;GAChB,YAAY,OAAO;GACb;GACN,cAAc;EACf,CAAA,CAEY;;AAErB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ImageStudioImage, ImageStudioPromptBlock } from "../../../core/backend-types.js";
|
|
2
|
+
import { ImageStudioState } from "../state.js";
|
|
3
|
+
import { ImageStudioNodeCoreOptions } from "../node-core.js";
|
|
4
|
+
//#region src/features/image-studio/react/store.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What the React Bild-Studio dialog drives; a host supplies it. The same contract as the Vue `ImageStudioStore`, with
|
|
7
|
+
* plain values where Vue has refs: the host re-renders the dialog with a new `state` after `update`.
|
|
8
|
+
*
|
|
9
|
+
* The dialog applies the reducers of `../state` and hands the result to `update`; the store only persists it. The
|
|
10
|
+
* store's methods never touch `state` themselves:
|
|
11
|
+
* - `update` must lead to a new `state` (the dialog keeps working on its own latest value until `state` changes).
|
|
12
|
+
* - `generate` produces one image from the block's `state.description`, the active Prompt-Bausteine and the active
|
|
13
|
+
* images (the host builds the prompt). When it resolves, the dialog calls `update(addGeneration(state, image, turn))`;
|
|
14
|
+
* when it rejects (and the signal was not aborted) the dialog appends a turn with `imageId: null` and the message as
|
|
15
|
+
* `problem`. An aborted generation adds nothing.
|
|
16
|
+
* - `upload` stores the file and returns the image; the dialog then calls `update(addReference(state, image))`.
|
|
17
|
+
* - `problem` is a problem of the store itself (the Prompt-Bausteine could not be loaded, say); the dialog shows it as a notice.
|
|
18
|
+
*/
|
|
19
|
+
export interface ImageStudioReactStore {
|
|
20
|
+
readonly state: ImageStudioState;
|
|
21
|
+
/** The managed list of Prompt-Bausteine, loaded by the host. */
|
|
22
|
+
readonly promptBlocks: readonly ImageStudioPromptBlock[];
|
|
23
|
+
/** Called with the state after a reducer; the host persists it. */
|
|
24
|
+
update: (next: ImageStudioState) => void;
|
|
25
|
+
generate: (instruction: string, signal: AbortSignal) => Promise<ImageStudioImage>;
|
|
26
|
+
upload: (file: File) => Promise<ImageStudioImage>;
|
|
27
|
+
readonly problem: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The `ImageStudioReactStore` of one `imageStudio` node (the React wrapper of `createImageStudioNodeCore`): `state`
|
|
31
|
+
* follows the node's `studio` attribute, `update` writes it back as one transaction, the rest goes to
|
|
32
|
+
* `backend.images`. The core lives for as long as the component and the `editor`, `backend` and `documentId` stay the
|
|
33
|
+
* same; the first render returns `undefined` (the core is built in an effect), so render the dialog once there is a store.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useImageStudioNodeStore(options: ImageStudioNodeCoreOptions): ImageStudioReactStore | undefined;
|
|
36
|
+
//#endregion
|
|
37
|
+
//# sourceMappingURL=store.d.ts.map
|