slice-machine-ui 2.19.2-beta.1 → 2.19.2-beta.3
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/out/404.html +1 -1
- package/out/_next/static/chunks/125-00b909bdbab2ca15.js +1 -0
- package/out/_next/static/chunks/{344-fdb3008f4bb3b0c1.js → 344-b64f09e670634ed1.js} +1 -1
- package/out/_next/static/chunks/{500-d3989390f5e8da53.js → 444-d39213143f782fec.js} +1 -1
- package/out/_next/static/chunks/{489-ce3053e1d81ade83.js → 489-32281540712d98bb.js} +1 -1
- package/out/_next/static/chunks/593-7ffd1197c3405ef8.js +1 -0
- package/out/_next/static/chunks/633-275b9968b5aaa920.js +1 -0
- package/out/_next/static/chunks/66-d9d3bcb5d041cb6d.js +1 -0
- package/out/_next/static/chunks/882-48d61b2fabee28d8.js +1 -0
- package/out/_next/static/chunks/pages/{_app-6abab3a0500ed1ed.js → _app-9cc2da8ff60c3087.js} +1 -1
- package/out/_next/static/chunks/pages/{changes-8af4acbb8f974cb2.js → changes-e66094f57453cf9c.js} +1 -1
- package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-af9376721beb489e.js → [customTypeId]-6d613b67e6967ae5.js} +1 -1
- package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-a24665e91b882169.js → [pageTypeId]-40207b66190e3fcd.js} +1 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-e973a443d8b8a75d.js → [variation]-22ffa2561ac66557.js} +1 -1
- package/out/_next/static/chunks/pages/{slices-81c1c3f1bcad60f4.js → slices-a9b4d6d022cfcd88.js} +1 -1
- package/out/_next/static/uC4K1_hgf7dK4FL02cbUH/_buildManifest.js +1 -0
- package/out/changelog.html +1 -1
- package/out/changes.html +1 -1
- package/out/custom-types/[customTypeId].html +1 -1
- package/out/custom-types.html +1 -1
- package/out/index.html +1 -1
- package/out/labs.html +1 -1
- package/out/page-types/[pageTypeId].html +1 -1
- package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
- package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
- package/out/slices.html +1 -1
- package/package.json +3 -3
- package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/CreateSliceFromImageModal.tsx +602 -238
- package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +50 -13
- package/src/icons/FigmaIcon.tsx +33 -34
- package/src/icons/FigmaIconSquare.tsx +45 -0
- package/src/legacy/components/ScreenshotChangesModal/index.tsx +2 -2
- package/src/legacy/lib/builders/CustomTypeBuilder/SliceZone/index.tsx +1 -1
- package/test/__setup__.ts +7 -0
- package/out/_next/static/MQ0zmvVfr20Ca-bgbzvFk/_buildManifest.js +0 -1
- package/out/_next/static/chunks/34-28725deef8b874b1.js +0 -1
- package/out/_next/static/chunks/658-8231c0b729e0124a.js +0 -1
- package/out/_next/static/chunks/907-180eb33eefccc237.js +0 -1
- package/out/_next/static/chunks/918-fa4f2563cb5fd014.js +0 -1
- /package/out/_next/static/{MQ0zmvVfr20Ca-bgbzvFk → uC4K1_hgf7dK4FL02cbUH}/_ssgManifest.js +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlankSlate,
|
|
3
|
-
BlankSlateActions,
|
|
4
|
-
BlankSlateDescription,
|
|
5
3
|
BlankSlateIcon,
|
|
6
|
-
BlankSlateTitle,
|
|
7
4
|
Box,
|
|
5
|
+
Button,
|
|
8
6
|
Dialog,
|
|
9
7
|
DialogActionButton,
|
|
10
8
|
DialogActions,
|
|
@@ -14,16 +12,22 @@ import {
|
|
|
14
12
|
DialogHeader,
|
|
15
13
|
FileDropZone,
|
|
16
14
|
FileUploadButton,
|
|
15
|
+
ProgressCircle,
|
|
17
16
|
ScrollArea,
|
|
17
|
+
Text,
|
|
18
18
|
} from "@prismicio/editor-ui";
|
|
19
19
|
import { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
|
|
20
20
|
import { useEffect, useRef, useState } from "react";
|
|
21
|
+
import { useHotkeys } from "react-hotkeys-hook";
|
|
21
22
|
import { toast } from "react-toastify";
|
|
23
|
+
import { z } from "zod";
|
|
22
24
|
|
|
23
25
|
import { getState, telemetry } from "@/apiClient";
|
|
24
26
|
import { addAiFeedback } from "@/features/aiFeedback";
|
|
25
27
|
import { useOnboarding } from "@/features/onboarding/useOnboarding";
|
|
26
28
|
import { useAutoSync } from "@/features/sync/AutoSyncProvider";
|
|
29
|
+
import { useExperimentVariant } from "@/hooks/useExperimentVariant";
|
|
30
|
+
import { FigmaIcon } from "@/icons/FigmaIcon";
|
|
27
31
|
import { managerClient } from "@/managerClient";
|
|
28
32
|
import useSliceMachineActions from "@/modules/useSliceMachineActions";
|
|
29
33
|
|
|
@@ -34,25 +38,29 @@ const IMAGE_UPLOAD_LIMIT = 10;
|
|
|
34
38
|
interface CreateSliceFromImageModalProps {
|
|
35
39
|
open: boolean;
|
|
36
40
|
location: "custom_type" | "page_type" | "slices";
|
|
37
|
-
onSuccess: (args: {
|
|
38
|
-
slices: {
|
|
39
|
-
model: SharedSlice;
|
|
40
|
-
langSmithUrl?: string;
|
|
41
|
-
}[];
|
|
42
|
-
library: string;
|
|
43
|
-
}) => void;
|
|
41
|
+
onSuccess: (args: { slices: SharedSlice[]; library: string }) => void;
|
|
44
42
|
onClose: () => void;
|
|
45
43
|
}
|
|
46
44
|
|
|
45
|
+
const clipboardDataSchema = z.object({
|
|
46
|
+
__type: z.literal("figma-to-prismic/clipboard-data"),
|
|
47
|
+
name: z.string(),
|
|
48
|
+
image: z.string().startsWith("data:image/"),
|
|
49
|
+
});
|
|
50
|
+
|
|
47
51
|
export function CreateSliceFromImageModal(
|
|
48
52
|
props: CreateSliceFromImageModalProps,
|
|
49
53
|
) {
|
|
50
54
|
const { open, location, onSuccess, onClose } = props;
|
|
51
55
|
const [slices, setSlices] = useState<Slice[]>([]);
|
|
52
|
-
const [
|
|
56
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
57
|
+
|
|
53
58
|
const { syncChanges } = useAutoSync();
|
|
54
59
|
const { createSliceSuccess } = useSliceMachineActions();
|
|
55
60
|
const { completeStep } = useOnboarding();
|
|
61
|
+
const existingSlices = useExistingSlices({ open });
|
|
62
|
+
const isFigmaEnabled = useIsFigmaEnabled();
|
|
63
|
+
const { libraryID, isLoading: isLoadingLibraryID } = useLibraryID();
|
|
56
64
|
|
|
57
65
|
/**
|
|
58
66
|
* Keeps track of the current instance id.
|
|
@@ -60,6 +68,15 @@ export function CreateSliceFromImageModal(
|
|
|
60
68
|
*/
|
|
61
69
|
const id = useRef(crypto.randomUUID());
|
|
62
70
|
|
|
71
|
+
useHotkeys(
|
|
72
|
+
["meta+v", "ctrl+v"],
|
|
73
|
+
(event) => {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
void handlePaste();
|
|
76
|
+
},
|
|
77
|
+
{ enabled: open && isFigmaEnabled },
|
|
78
|
+
);
|
|
79
|
+
|
|
63
80
|
const setSlice = (args: {
|
|
64
81
|
index: number;
|
|
65
82
|
slice: (prevSlice: Slice) => Slice;
|
|
@@ -68,14 +85,9 @@ export function CreateSliceFromImageModal(
|
|
|
68
85
|
setSlices((slices) => slices.map((s, i) => (i === index ? slice(s) : s)));
|
|
69
86
|
};
|
|
70
87
|
|
|
71
|
-
const onOpenChange = (open: boolean) => {
|
|
72
|
-
if (open || isCreatingSlices) return;
|
|
73
|
-
onClose();
|
|
74
|
-
id.current = crypto.randomUUID();
|
|
75
|
-
setSlices([]);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
88
|
const onImagesSelected = (images: File[]) => {
|
|
89
|
+
if (hasTriggeredGeneration) return;
|
|
90
|
+
|
|
79
91
|
if (images.length > IMAGE_UPLOAD_LIMIT) {
|
|
80
92
|
toast.error(
|
|
81
93
|
`You can only upload ${IMAGE_UPLOAD_LIMIT} images at a time.`,
|
|
@@ -83,18 +95,30 @@ export function CreateSliceFromImageModal(
|
|
|
83
95
|
return;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
const startIndex = slices.length;
|
|
99
|
+
setSlices((prevSlices) => [
|
|
100
|
+
...prevSlices,
|
|
101
|
+
...images.map(
|
|
102
|
+
(image): Slice => ({
|
|
103
|
+
source: "upload",
|
|
104
|
+
status: "uploading",
|
|
105
|
+
image,
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
images.forEach((image, relativeIndex) => {
|
|
111
|
+
const index = startIndex + relativeIndex;
|
|
112
|
+
void uploadImage({ index, image, source: "upload" });
|
|
113
|
+
});
|
|
94
114
|
};
|
|
95
115
|
|
|
96
|
-
const uploadImage = (args: {
|
|
97
|
-
|
|
116
|
+
const uploadImage = async (args: {
|
|
117
|
+
index: number;
|
|
118
|
+
image: File;
|
|
119
|
+
source: "figma" | "upload";
|
|
120
|
+
}) => {
|
|
121
|
+
const { index, image, source } = args;
|
|
98
122
|
const currentId = id.current;
|
|
99
123
|
|
|
100
124
|
setSlice({
|
|
@@ -102,33 +126,45 @@ export function CreateSliceFromImageModal(
|
|
|
102
126
|
slice: (prevSlice) => ({
|
|
103
127
|
...prevSlice,
|
|
104
128
|
status: "uploading",
|
|
129
|
+
image,
|
|
130
|
+
source,
|
|
105
131
|
}),
|
|
106
132
|
});
|
|
107
133
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
try {
|
|
135
|
+
const imageUrl = await getImageUrl({ image });
|
|
136
|
+
if (currentId !== id.current) return;
|
|
137
|
+
|
|
138
|
+
setSlice({
|
|
139
|
+
index,
|
|
140
|
+
slice: (prevSlice) => ({
|
|
141
|
+
...prevSlice,
|
|
142
|
+
status: "pending",
|
|
143
|
+
thumbnailUrl: imageUrl,
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
if (currentId !== id.current) return;
|
|
148
|
+
setSlice({
|
|
149
|
+
index,
|
|
150
|
+
slice: (prevSlice) => ({
|
|
151
|
+
...prevSlice,
|
|
152
|
+
status: "uploadError",
|
|
153
|
+
onRetry: () => void uploadImage({ index, image, source }),
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
125
157
|
};
|
|
126
158
|
|
|
127
|
-
const
|
|
159
|
+
const inferSlice = async (args: {
|
|
160
|
+
index: number;
|
|
161
|
+
imageUrl: string;
|
|
162
|
+
source: "figma" | "upload";
|
|
163
|
+
}) => {
|
|
164
|
+
if (libraryID === undefined) return;
|
|
128
165
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const currentId = id.current;
|
|
166
|
+
const { index, imageUrl, source } = args;
|
|
167
|
+
let currentId = id.current;
|
|
132
168
|
|
|
133
169
|
setSlice({
|
|
134
170
|
index,
|
|
@@ -139,149 +175,449 @@ export function CreateSliceFromImageModal(
|
|
|
139
175
|
}),
|
|
140
176
|
});
|
|
141
177
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
try {
|
|
179
|
+
const inferResult = await managerClient.customTypes.inferSlice({
|
|
180
|
+
source,
|
|
181
|
+
libraryID,
|
|
182
|
+
imageUrl,
|
|
183
|
+
});
|
|
145
184
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
langSmithUrl,
|
|
159
|
-
}
|
|
160
|
-
: prevSlice,
|
|
161
|
-
),
|
|
162
|
-
);
|
|
163
|
-
},
|
|
164
|
-
() => {
|
|
165
|
-
if (currentId !== id.current) return;
|
|
166
|
-
setSlice({
|
|
167
|
-
index,
|
|
168
|
-
slice: (prevSlice) => ({
|
|
185
|
+
if (currentId !== id.current) return;
|
|
186
|
+
|
|
187
|
+
const model = sliceWithoutConflicts({
|
|
188
|
+
existingSlices: existingSlices.current,
|
|
189
|
+
newSlices: slices,
|
|
190
|
+
slice: inferResult.slice,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
setSlices((prevSlices) => {
|
|
194
|
+
return prevSlices.map((prevSlice, i) => {
|
|
195
|
+
if (i !== index) return prevSlice;
|
|
196
|
+
return {
|
|
169
197
|
...prevSlice,
|
|
170
|
-
status: "
|
|
198
|
+
status: "success",
|
|
171
199
|
thumbnailUrl: imageUrl,
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
model,
|
|
201
|
+
langSmithUrl: inferResult.langSmithUrl,
|
|
202
|
+
};
|
|
174
203
|
});
|
|
175
|
-
}
|
|
176
|
-
);
|
|
177
|
-
};
|
|
204
|
+
});
|
|
178
205
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return acc;
|
|
183
|
-
}, []);
|
|
184
|
-
if (!newSlices.length) return;
|
|
206
|
+
if (source === "upload") {
|
|
207
|
+
currentId = id.current;
|
|
208
|
+
const currentSlice = slices[index];
|
|
185
209
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
const serverState = await getState();
|
|
193
|
-
createSliceSuccess(serverState.libraries);
|
|
194
|
-
syncChanges();
|
|
195
|
-
|
|
196
|
-
onSuccess({ slices, library });
|
|
197
|
-
|
|
198
|
-
setIsCreatingSlices(false);
|
|
199
|
-
id.current = crypto.randomUUID();
|
|
200
|
-
setSlices([]);
|
|
201
|
-
|
|
202
|
-
void completeStep("createSlice");
|
|
203
|
-
|
|
204
|
-
for (const { model, langSmithUrl } of slices) {
|
|
205
|
-
void telemetry.track({
|
|
206
|
-
event: "slice:created",
|
|
207
|
-
id: model.id,
|
|
208
|
-
name: model.name,
|
|
209
|
-
library,
|
|
210
|
-
location,
|
|
211
|
-
mode: "ai",
|
|
212
|
-
langSmithUrl,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
addAiFeedback({
|
|
216
|
-
type: "model",
|
|
217
|
-
library,
|
|
218
|
-
sliceId: model.id,
|
|
219
|
-
variationId: model.variations[0].id,
|
|
220
|
-
langSmithUrl,
|
|
221
|
-
});
|
|
210
|
+
const { errors } = await managerClient.slices.createSlice({
|
|
211
|
+
libraryID,
|
|
212
|
+
model: model,
|
|
213
|
+
});
|
|
214
|
+
if (errors.length) {
|
|
215
|
+
throw new Error(`Failed to create slice ${model.id}.`);
|
|
222
216
|
}
|
|
223
|
-
|
|
224
|
-
|
|
217
|
+
|
|
218
|
+
await managerClient.slices.updateSliceScreenshot({
|
|
219
|
+
libraryID,
|
|
220
|
+
sliceID: model.id,
|
|
221
|
+
variationID: model.variations[0].id,
|
|
222
|
+
data: currentSlice.image,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
225
|
if (currentId !== id.current) return;
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
void completeStep("createSlice");
|
|
229
|
+
|
|
230
|
+
void telemetry.track({
|
|
231
|
+
event: "slice:created",
|
|
232
|
+
id: model.id,
|
|
233
|
+
name: model.name,
|
|
234
|
+
library: libraryID,
|
|
235
|
+
location,
|
|
236
|
+
mode: "ai",
|
|
237
|
+
langSmithUrl: inferResult.langSmithUrl,
|
|
228
238
|
});
|
|
239
|
+
|
|
240
|
+
addAiFeedback({
|
|
241
|
+
type: "model",
|
|
242
|
+
library: libraryID,
|
|
243
|
+
sliceId: model.id,
|
|
244
|
+
variationId: model.variations[0].id,
|
|
245
|
+
langSmithUrl: inferResult.langSmithUrl,
|
|
246
|
+
});
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (currentId !== id.current) return;
|
|
249
|
+
|
|
250
|
+
setSlice({
|
|
251
|
+
index,
|
|
252
|
+
slice: (prevSlice) => ({
|
|
253
|
+
...prevSlice,
|
|
254
|
+
status: "generateError",
|
|
255
|
+
thumbnailUrl: imageUrl,
|
|
256
|
+
onRetry: () => {
|
|
257
|
+
void inferSlice({ index, imageUrl, source });
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
229
262
|
};
|
|
230
263
|
|
|
231
|
-
const
|
|
232
|
-
(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
264
|
+
const generatePendingSlices = () => {
|
|
265
|
+
if (libraryID === undefined) return;
|
|
266
|
+
|
|
267
|
+
slices.forEach((slice, index) => {
|
|
268
|
+
if (slice.status === "pending") {
|
|
269
|
+
void inferSlice({
|
|
270
|
+
index,
|
|
271
|
+
imageUrl: slice.thumbnailUrl,
|
|
272
|
+
source: slice.source,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const generatingSliceCount = slices.filter((slice) => {
|
|
279
|
+
return slice.status === "generating";
|
|
280
|
+
}).length;
|
|
281
|
+
|
|
282
|
+
const uploadingSliceCount = slices.filter((slice) => {
|
|
283
|
+
return slice.status === "uploading";
|
|
284
|
+
}).length;
|
|
285
|
+
|
|
286
|
+
const loadingSliceCount = generatingSliceCount + uploadingSliceCount;
|
|
287
|
+
|
|
288
|
+
const pendingSliceCount = slices.filter((slice) => {
|
|
289
|
+
return slice.status === "pending";
|
|
290
|
+
}).length;
|
|
291
|
+
|
|
292
|
+
const completedSliceCount = slices.filter((slice) => {
|
|
293
|
+
return slice.status === "success";
|
|
294
|
+
}).length;
|
|
295
|
+
|
|
296
|
+
const hasTriggeredGeneration = slices.some((slice) => {
|
|
297
|
+
return slice.status === "generating" || slice.status === "success";
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const generateSliceCount = loadingSliceCount + pendingSliceCount;
|
|
301
|
+
|
|
302
|
+
const closeModal = () => {
|
|
303
|
+
if (loadingSliceCount > 0) return;
|
|
304
|
+
onClose();
|
|
305
|
+
id.current = crypto.randomUUID();
|
|
306
|
+
setTimeout(() => setSlices([]), 250); // wait for the modal fade animation
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const onSubmit = async () => {
|
|
310
|
+
if (libraryID === undefined) return;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
setIsSubmitting(true);
|
|
314
|
+
|
|
315
|
+
const serverState = await getState();
|
|
316
|
+
createSliceSuccess(serverState.libraries);
|
|
317
|
+
syncChanges();
|
|
318
|
+
|
|
319
|
+
onSuccess({
|
|
320
|
+
slices: slices.flatMap((slice) =>
|
|
321
|
+
slice.status === "success" ? slice.model : [],
|
|
322
|
+
),
|
|
323
|
+
library: libraryID,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
closeModal();
|
|
327
|
+
} finally {
|
|
328
|
+
setIsSubmitting(false);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const handlePaste = async () => {
|
|
333
|
+
if (
|
|
334
|
+
!open ||
|
|
335
|
+
!isFigmaEnabled ||
|
|
336
|
+
// For now we only support one Figma slice at a time
|
|
337
|
+
slices.some((slice) => slice.source === "figma")
|
|
338
|
+
) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Don't allow pasting while uploads or generation are in progress
|
|
343
|
+
const isLoading = slices.some(
|
|
344
|
+
(slice) => slice.status === "uploading" || slice.status === "generating",
|
|
345
|
+
);
|
|
346
|
+
if (isLoading) return;
|
|
347
|
+
|
|
348
|
+
const supportsClipboardRead =
|
|
349
|
+
typeof navigator.clipboard?.read === "function";
|
|
350
|
+
|
|
351
|
+
if (!supportsClipboardRead) {
|
|
352
|
+
toast.error("Clipboard paste is not supported in this browser.");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
const clipboardItems = await navigator.clipboard.read();
|
|
358
|
+
if (clipboardItems.length === 0) {
|
|
359
|
+
toast.error("No data found in clipboard.");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let imageName = "pasted-image.png";
|
|
364
|
+
let imageBlob: Blob | null = null;
|
|
365
|
+
let success = false;
|
|
366
|
+
|
|
367
|
+
// Method 1: Try to extract image from clipboard image/png blob (preferred)
|
|
368
|
+
for (const item of clipboardItems) {
|
|
369
|
+
const imageType = item.types.find((type) => type.startsWith("image/"));
|
|
370
|
+
if (imageType !== undefined) {
|
|
371
|
+
imageBlob = await item.getType(imageType);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Method 2: Read JSON from text/plain to get metadata and base64 image as fallback
|
|
377
|
+
for (const item of clipboardItems) {
|
|
378
|
+
if (item.types.includes("text/plain")) {
|
|
379
|
+
try {
|
|
380
|
+
const textBlob = await item.getType("text/plain");
|
|
381
|
+
const text = await textBlob.text();
|
|
382
|
+
|
|
383
|
+
const result = clipboardDataSchema.safeParse(JSON.parse(text));
|
|
384
|
+
if (result.success) {
|
|
385
|
+
success = true;
|
|
386
|
+
const data = result.data;
|
|
387
|
+
imageName = `${data.name}.png`;
|
|
388
|
+
|
|
389
|
+
// Use base64 image as fallback if no blob was found
|
|
390
|
+
if (!imageBlob) {
|
|
391
|
+
const response = await fetch(data.image);
|
|
392
|
+
imageBlob = await response.blob();
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
console.warn("Clipboard data validation failed:", result.error);
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.warn("Failed to parse JSON from clipboard:", error);
|
|
399
|
+
// Continue - we may still have imageBlob from Method 1
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!imageBlob) {
|
|
405
|
+
if (success) {
|
|
406
|
+
toast.error(
|
|
407
|
+
"Could not extract Figma data from clipboard. Please try copying again using the Prismic Figma plugin.",
|
|
408
|
+
);
|
|
409
|
+
} else {
|
|
410
|
+
toast.error(
|
|
411
|
+
"No Figma data found in clipboard. Make sure you've copied a design using the Prismic Figma plugin.",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check if we're at the limit
|
|
418
|
+
const currentSliceCount = slices.length;
|
|
419
|
+
if (currentSliceCount >= IMAGE_UPLOAD_LIMIT) {
|
|
420
|
+
toast.error(
|
|
421
|
+
`You can only upload ${IMAGE_UPLOAD_LIMIT} images at a time.`,
|
|
422
|
+
);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Create File object from blob and append to existing slices
|
|
427
|
+
const image = new File([imageBlob], imageName, {
|
|
428
|
+
type: imageBlob.type,
|
|
429
|
+
});
|
|
430
|
+
const newIndex = currentSliceCount;
|
|
431
|
+
|
|
432
|
+
// Append new slice to existing ones
|
|
433
|
+
setSlices((prevSlices) => [
|
|
434
|
+
...prevSlices,
|
|
435
|
+
{
|
|
436
|
+
source: "figma",
|
|
437
|
+
status: "uploading",
|
|
438
|
+
image,
|
|
439
|
+
},
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
// Start uploading the new image
|
|
443
|
+
void uploadImage({ index: newIndex, image, source: "figma" });
|
|
444
|
+
|
|
445
|
+
toast.success(`Pasted ${imageName}${success ? " from Figma" : ""}`);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error("Failed to paste from clipboard:", error);
|
|
448
|
+
toast.error(
|
|
449
|
+
"Failed to paste from clipboard. Please check browser permissions and try again.",
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
236
453
|
|
|
237
454
|
return (
|
|
238
|
-
<Dialog open={open} onOpenChange={
|
|
239
|
-
<DialogHeader title="Generate
|
|
455
|
+
<Dialog open={open} onOpenChange={(open) => !open && closeModal()}>
|
|
456
|
+
<DialogHeader title="Generate with AI" />
|
|
240
457
|
<DialogContent gap={0}>
|
|
241
458
|
<DialogDescription hidden>
|
|
242
459
|
Upload images to generate slices with AI
|
|
243
460
|
</DialogDescription>
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
461
|
+
{!isLoadingLibraryID ? (
|
|
462
|
+
<>
|
|
463
|
+
{slices.length === 0 ? (
|
|
464
|
+
<Box
|
|
465
|
+
padding={16}
|
|
466
|
+
height="100%"
|
|
467
|
+
gap={16}
|
|
468
|
+
display="flex"
|
|
469
|
+
flexDirection="column"
|
|
470
|
+
>
|
|
471
|
+
{isFigmaEnabled && (
|
|
472
|
+
<Box
|
|
473
|
+
display="flex"
|
|
474
|
+
gap={16}
|
|
475
|
+
alignItems="center"
|
|
476
|
+
backgroundColor="grey2"
|
|
477
|
+
padding={16}
|
|
478
|
+
borderRadius={12}
|
|
479
|
+
>
|
|
480
|
+
<Box
|
|
481
|
+
display="flex"
|
|
482
|
+
gap={8}
|
|
483
|
+
alignItems="center"
|
|
484
|
+
flexGrow={1}
|
|
485
|
+
>
|
|
486
|
+
<Box
|
|
487
|
+
width={48}
|
|
488
|
+
height={48}
|
|
489
|
+
backgroundColor="grey12"
|
|
490
|
+
borderRadius="100%"
|
|
491
|
+
display="flex"
|
|
492
|
+
alignItems="center"
|
|
493
|
+
justifyContent="center"
|
|
494
|
+
>
|
|
495
|
+
<FigmaIcon height={25} />
|
|
496
|
+
</Box>
|
|
497
|
+
<Box display="flex" flexDirection="column" flexGrow={1}>
|
|
498
|
+
<Text variant="bold">Want to work faster?</Text>
|
|
499
|
+
<Text variant="small" color="grey11">
|
|
500
|
+
Copy frames from Figma with the Slice Machine plugin
|
|
501
|
+
and paste them here.
|
|
502
|
+
</Text>
|
|
503
|
+
</Box>
|
|
504
|
+
</Box>
|
|
505
|
+
<Button
|
|
506
|
+
endIcon="arrowForward"
|
|
507
|
+
color="indigo"
|
|
508
|
+
onClick={() =>
|
|
509
|
+
window.open(
|
|
510
|
+
"https://www.figma.com/community/plugin/TODO",
|
|
511
|
+
"_blank",
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
sx={{ marginRight: 8 }}
|
|
515
|
+
invisible
|
|
516
|
+
>
|
|
517
|
+
Install plugin
|
|
518
|
+
</Button>
|
|
519
|
+
</Box>
|
|
520
|
+
)}
|
|
521
|
+
<FileDropZone
|
|
252
522
|
onFilesSelected={onImagesSelected}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
523
|
+
assetType="image"
|
|
524
|
+
maxFiles={IMAGE_UPLOAD_LIMIT}
|
|
525
|
+
overlay={
|
|
526
|
+
<UploadBlankSlate
|
|
527
|
+
onFilesSelected={onImagesSelected}
|
|
528
|
+
onPaste={() => void handlePaste()}
|
|
529
|
+
droppingFiles
|
|
530
|
+
/>
|
|
531
|
+
}
|
|
532
|
+
>
|
|
533
|
+
<UploadBlankSlate
|
|
534
|
+
onFilesSelected={onImagesSelected}
|
|
535
|
+
onPaste={() => void handlePaste()}
|
|
536
|
+
/>
|
|
537
|
+
</FileDropZone>
|
|
538
|
+
</Box>
|
|
539
|
+
) : (
|
|
540
|
+
<>
|
|
541
|
+
<Box
|
|
542
|
+
display="flex"
|
|
543
|
+
alignItems="center"
|
|
544
|
+
justifyContent="space-between"
|
|
545
|
+
padding={16}
|
|
546
|
+
>
|
|
547
|
+
<Text variant="h3">Design</Text>
|
|
548
|
+
<FileUploadButton
|
|
549
|
+
size="medium"
|
|
550
|
+
color="grey"
|
|
551
|
+
onFilesSelected={onImagesSelected}
|
|
552
|
+
startIcon="attachFile"
|
|
553
|
+
disabled={hasTriggeredGeneration}
|
|
554
|
+
>
|
|
555
|
+
Add images
|
|
556
|
+
</FileUploadButton>
|
|
557
|
+
</Box>
|
|
558
|
+
<ScrollArea stableScrollbar={false}>
|
|
559
|
+
<Box
|
|
560
|
+
display="grid"
|
|
561
|
+
gridTemplateColumns="1fr 1fr"
|
|
562
|
+
gap={16}
|
|
563
|
+
padding={16}
|
|
564
|
+
>
|
|
565
|
+
{slices.map((slice, index) => (
|
|
566
|
+
<SliceCard slice={slice} key={`slice-${index}`} />
|
|
567
|
+
))}
|
|
568
|
+
</Box>
|
|
569
|
+
</ScrollArea>
|
|
570
|
+
</>
|
|
571
|
+
)}
|
|
572
|
+
<DialogActions>
|
|
573
|
+
<DialogCancelButton
|
|
574
|
+
onClick={() => closeModal()}
|
|
575
|
+
size="medium"
|
|
576
|
+
disabled={loadingSliceCount > 0}
|
|
577
|
+
sx={{ marginRight: 8 }}
|
|
578
|
+
invisible
|
|
579
|
+
>
|
|
580
|
+
Close
|
|
581
|
+
</DialogCancelButton>
|
|
582
|
+
{completedSliceCount === 0 || loadingSliceCount > 0 ? (
|
|
583
|
+
<DialogActionButton
|
|
584
|
+
color="purple"
|
|
585
|
+
startIcon="autoFixHigh"
|
|
586
|
+
onClick={() => void generatePendingSlices()}
|
|
587
|
+
disabled={
|
|
588
|
+
hasTriggeredGeneration ||
|
|
589
|
+
loadingSliceCount > 0 ||
|
|
590
|
+
pendingSliceCount === 0
|
|
591
|
+
}
|
|
592
|
+
loading={loadingSliceCount > 0}
|
|
593
|
+
size="medium"
|
|
594
|
+
>
|
|
595
|
+
Generate{" "}
|
|
596
|
+
{generateSliceCount > 0 ? `(${generateSliceCount}) ` : ""}
|
|
597
|
+
{generateSliceCount === 1 ? "Slice" : "Slices"}
|
|
598
|
+
</DialogActionButton>
|
|
599
|
+
) : (
|
|
600
|
+
<DialogActionButton
|
|
601
|
+
color="purple"
|
|
602
|
+
onClick={() => void onSubmit()}
|
|
603
|
+
loading={isSubmitting}
|
|
604
|
+
size="medium"
|
|
605
|
+
>
|
|
606
|
+
{getSubmitButtonLabel(location, completedSliceCount)}
|
|
607
|
+
</DialogActionButton>
|
|
608
|
+
)}
|
|
609
|
+
</DialogActions>
|
|
610
|
+
</>
|
|
260
611
|
) : (
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
padding={16}
|
|
267
|
-
>
|
|
268
|
-
{slices.map((slice, index) => (
|
|
269
|
-
<SliceCard slice={slice} key={`slice-${index}`} />
|
|
270
|
-
))}
|
|
271
|
-
</Box>
|
|
272
|
-
</ScrollArea>
|
|
273
|
-
)}
|
|
274
|
-
|
|
275
|
-
<DialogActions>
|
|
276
|
-
<DialogCancelButton disabled={isCreatingSlices} />
|
|
277
|
-
<DialogActionButton
|
|
278
|
-
disabled={!someSlicesReady || areSlicesLoading}
|
|
279
|
-
loading={isCreatingSlices}
|
|
280
|
-
onClick={onSubmit}
|
|
612
|
+
<Box
|
|
613
|
+
display="flex"
|
|
614
|
+
justifyContent="center"
|
|
615
|
+
alignItems="center"
|
|
616
|
+
height="100%"
|
|
281
617
|
>
|
|
282
|
-
|
|
283
|
-
</
|
|
284
|
-
|
|
618
|
+
<ProgressCircle color="purple9" />
|
|
619
|
+
</Box>
|
|
620
|
+
)}
|
|
285
621
|
</DialogContent>
|
|
286
622
|
</Dialog>
|
|
287
623
|
);
|
|
@@ -290,8 +626,10 @@ export function CreateSliceFromImageModal(
|
|
|
290
626
|
function UploadBlankSlate(props: {
|
|
291
627
|
droppingFiles?: boolean;
|
|
292
628
|
onFilesSelected: (files: File[]) => void;
|
|
629
|
+
onPaste: () => void;
|
|
293
630
|
}) {
|
|
294
|
-
const { droppingFiles = false, onFilesSelected } = props;
|
|
631
|
+
const { droppingFiles = false, onFilesSelected, onPaste } = props;
|
|
632
|
+
const isFigmaEnabled = useIsFigmaEnabled();
|
|
295
633
|
|
|
296
634
|
return (
|
|
297
635
|
<Box
|
|
@@ -302,27 +640,70 @@ function UploadBlankSlate(props: {
|
|
|
302
640
|
border
|
|
303
641
|
borderStyle="dashed"
|
|
304
642
|
borderColor={droppingFiles ? "purple9" : "grey6"}
|
|
643
|
+
borderRadius={12}
|
|
644
|
+
flexGrow={1}
|
|
305
645
|
>
|
|
306
646
|
<BlankSlate>
|
|
307
|
-
<
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
startIcon="attachFile"
|
|
320
|
-
onFilesSelected={onFilesSelected}
|
|
321
|
-
color="grey"
|
|
647
|
+
<Box display="flex" flexDirection="column" gap={16} alignItems="center">
|
|
648
|
+
<BlankSlateIcon
|
|
649
|
+
lineColor="purple11"
|
|
650
|
+
backgroundColor="purple5"
|
|
651
|
+
name="cloudUpload"
|
|
652
|
+
size="large"
|
|
653
|
+
/>
|
|
654
|
+
<Box
|
|
655
|
+
display="flex"
|
|
656
|
+
flexDirection="column"
|
|
657
|
+
gap={4}
|
|
658
|
+
alignItems="center"
|
|
322
659
|
>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
660
|
+
{isFigmaEnabled ? (
|
|
661
|
+
<>
|
|
662
|
+
<Text>Generate slices from your designs</Text>
|
|
663
|
+
<Text variant="small" color="grey11">
|
|
664
|
+
Upload your design images or paste them directly from Figma.
|
|
665
|
+
</Text>
|
|
666
|
+
</>
|
|
667
|
+
) : (
|
|
668
|
+
<>
|
|
669
|
+
<Text>Upload your design images.</Text>
|
|
670
|
+
<Text variant="small" color="grey11">
|
|
671
|
+
Once uploaded, you can generate slices automatically using AI.
|
|
672
|
+
</Text>
|
|
673
|
+
</>
|
|
674
|
+
)}
|
|
675
|
+
</Box>
|
|
676
|
+
<Box display="flex" alignItems="center" gap={16}>
|
|
677
|
+
{isFigmaEnabled ? (
|
|
678
|
+
<>
|
|
679
|
+
<Button
|
|
680
|
+
size="small"
|
|
681
|
+
renderStartIcon={() => <FigmaIcon height={16} />}
|
|
682
|
+
color="grey"
|
|
683
|
+
onClick={onPaste}
|
|
684
|
+
>
|
|
685
|
+
Paste from Figma
|
|
686
|
+
</Button>
|
|
687
|
+
<FileUploadButton
|
|
688
|
+
size="small"
|
|
689
|
+
onFilesSelected={onFilesSelected}
|
|
690
|
+
color="purple"
|
|
691
|
+
invisible
|
|
692
|
+
>
|
|
693
|
+
Add images
|
|
694
|
+
</FileUploadButton>
|
|
695
|
+
</>
|
|
696
|
+
) : (
|
|
697
|
+
<FileUploadButton
|
|
698
|
+
startIcon="attachFile"
|
|
699
|
+
onFilesSelected={onFilesSelected}
|
|
700
|
+
color="grey"
|
|
701
|
+
>
|
|
702
|
+
Add images
|
|
703
|
+
</FileUploadButton>
|
|
704
|
+
)}
|
|
705
|
+
</Box>
|
|
706
|
+
</Box>
|
|
326
707
|
</BlankSlate>
|
|
327
708
|
</Box>
|
|
328
709
|
);
|
|
@@ -345,12 +726,6 @@ async function getImageUrl({ image }: { image: File }) {
|
|
|
345
726
|
return url;
|
|
346
727
|
}
|
|
347
728
|
|
|
348
|
-
type NewSlice = {
|
|
349
|
-
image: File;
|
|
350
|
-
model: SharedSlice;
|
|
351
|
-
langSmithUrl?: string;
|
|
352
|
-
};
|
|
353
|
-
|
|
354
729
|
/**
|
|
355
730
|
* Keeps track of the existing slices in the project.
|
|
356
731
|
* Re-fetches them when the modal is opened.
|
|
@@ -422,53 +797,42 @@ function sliceWithoutConflicts({
|
|
|
422
797
|
};
|
|
423
798
|
}
|
|
424
799
|
|
|
425
|
-
async function addSlices(newSlices: NewSlice[]) {
|
|
426
|
-
// use the first library
|
|
427
|
-
const { libraries = [] } =
|
|
428
|
-
await managerClient.project.getSliceMachineConfig();
|
|
429
|
-
const library = libraries[0];
|
|
430
|
-
if (!library) {
|
|
431
|
-
throw new Error("No library found in the config.");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
for (const { model } of newSlices) {
|
|
435
|
-
const { errors } = await managerClient.slices.createSlice({
|
|
436
|
-
libraryID: library,
|
|
437
|
-
model,
|
|
438
|
-
});
|
|
439
|
-
if (errors.length) {
|
|
440
|
-
throw new Error(`Failed to create slice ${model.id}.`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// for each added slice, set the variation screenshot
|
|
445
|
-
const slices = await Promise.all(
|
|
446
|
-
newSlices.map(async ({ model, image, langSmithUrl }) => {
|
|
447
|
-
await managerClient.slices.updateSliceScreenshot({
|
|
448
|
-
libraryID: library,
|
|
449
|
-
sliceID: model.id,
|
|
450
|
-
variationID: model.variations[0].id,
|
|
451
|
-
data: image,
|
|
452
|
-
});
|
|
453
|
-
return {
|
|
454
|
-
model,
|
|
455
|
-
langSmithUrl,
|
|
456
|
-
};
|
|
457
|
-
}),
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
return { library, slices };
|
|
461
|
-
}
|
|
462
|
-
|
|
463
800
|
const getSubmitButtonLabel = (
|
|
464
801
|
location: "custom_type" | "page_type" | "slices",
|
|
802
|
+
completedSliceCount: number,
|
|
465
803
|
) => {
|
|
466
804
|
switch (location) {
|
|
467
805
|
case "custom_type":
|
|
468
|
-
return
|
|
806
|
+
return `Add to type (${completedSliceCount})`;
|
|
469
807
|
case "page_type":
|
|
470
|
-
return
|
|
808
|
+
return `Add to page (${completedSliceCount})`;
|
|
471
809
|
case "slices":
|
|
472
|
-
return "
|
|
810
|
+
return "Done";
|
|
473
811
|
}
|
|
474
812
|
};
|
|
813
|
+
|
|
814
|
+
function useIsFigmaEnabled() {
|
|
815
|
+
const experiment = useExperimentVariant("llm-proxy-access");
|
|
816
|
+
return experiment?.value === "on";
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function useLibraryID() {
|
|
820
|
+
const [libraryID, setLibraryID] = useState<string | undefined>();
|
|
821
|
+
|
|
822
|
+
useEffect(() => {
|
|
823
|
+
managerClient.project
|
|
824
|
+
.getSliceMachineConfig()
|
|
825
|
+
.then((smConfig) => {
|
|
826
|
+
const libraryID = smConfig?.libraries?.[0];
|
|
827
|
+
if (libraryID === undefined) {
|
|
828
|
+
throw new Error("No library found in the config.");
|
|
829
|
+
}
|
|
830
|
+
setLibraryID(libraryID);
|
|
831
|
+
})
|
|
832
|
+
.catch(() => {
|
|
833
|
+
throw new Error("Could not get library ID from the config.");
|
|
834
|
+
});
|
|
835
|
+
}, []);
|
|
836
|
+
|
|
837
|
+
return { libraryID, isLoading: libraryID === undefined };
|
|
838
|
+
}
|