slice-machine-ui 2.19.2-alpha.jp-figma-to-prismic.7 → 2.19.2-alpha.jp-figma-to-slice-1.2
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/{31Ud_23VczjR1uJs9oG2p → bwDsGgVrRudGF6BhMNf48}/_buildManifest.js +1 -1
- package/out/_next/static/chunks/34-28725deef8b874b1.js +1 -0
- package/out/_next/static/chunks/344-a67ed5f369a194ae.js +1 -0
- package/out/_next/static/chunks/429-1137c819c2bf6b66.js +3 -0
- package/out/_next/static/chunks/{444-d39213143f782fec.js → 444-c3ffb44477761d17.js} +1 -1
- package/out/_next/static/chunks/{489-2e83dd8ae83fd5ed.js → 489-ce3053e1d81ade83.js} +1 -1
- package/out/_next/static/chunks/658-8231c0b729e0124a.js +1 -0
- package/out/_next/static/chunks/907-bf4215e6fc238ea0.js +1 -0
- package/out/_next/static/chunks/pages/{_app-426bac9cf22a56de.js → _app-69a74b6aaca04e69.js} +1 -1
- package/out/_next/static/chunks/pages/{changes-b3f45dfeb5dc08f0.js → changes-7f24b37f5bf872ae.js} +1 -1
- package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-02278526092bcf5c.js → [customTypeId]-1b47424a37b49dff.js} +1 -1
- package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-4d99de1b52de7c9b.js → [pageTypeId]-385f933c203e8b16.js} +1 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-330e5d545d9f6269.js → [variation]-08cdeefc96106c0c.js} +1 -1
- package/out/_next/static/chunks/pages/{slices-d6873e0fb6a46bb8.js → slices-bedcb854fbdca8cd.js} +1 -1
- 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 +310 -535
- package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +14 -57
- package/src/icons/FigmaIcon.tsx +34 -88
- package/src/legacy/components/ScreenshotChangesModal/index.tsx +1 -1
- package/src/legacy/lib/builders/CustomTypeBuilder/SliceZone/index.tsx +2 -1
- package/src/pages/slices.tsx +1 -0
- package/out/_next/static/chunks/20-169231cb23a752ff.js +0 -1
- package/out/_next/static/chunks/344-b64f09e670634ed1.js +0 -1
- package/out/_next/static/chunks/429-aab52070cad2884b.js +0 -3
- package/out/_next/static/chunks/593-97393b59cba3d429.js +0 -1
- package/out/_next/static/chunks/907-0f84907d31c989bf.js +0 -1
- /package/out/_next/static/{31Ud_23VczjR1uJs9oG2p → bwDsGgVrRudGF6BhMNf48}/_ssgManifest.js +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlankSlate,
|
|
3
|
+
BlankSlateActions,
|
|
4
|
+
BlankSlateDescription,
|
|
3
5
|
BlankSlateIcon,
|
|
6
|
+
BlankSlateTitle,
|
|
4
7
|
Box,
|
|
5
|
-
Button,
|
|
6
8
|
Dialog,
|
|
7
9
|
DialogActionButton,
|
|
8
10
|
DialogActions,
|
|
@@ -12,9 +14,7 @@ import {
|
|
|
12
14
|
DialogHeader,
|
|
13
15
|
FileDropZone,
|
|
14
16
|
FileUploadButton,
|
|
15
|
-
ProgressCircle,
|
|
16
17
|
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";
|
|
@@ -27,45 +27,48 @@ import { addAiFeedback } from "@/features/aiFeedback";
|
|
|
27
27
|
import { useOnboarding } from "@/features/onboarding/useOnboarding";
|
|
28
28
|
import { useAutoSync } from "@/features/sync/AutoSyncProvider";
|
|
29
29
|
import { useExperimentVariant } from "@/hooks/useExperimentVariant";
|
|
30
|
-
import { FigmaIcon } from "@/icons/FigmaIcon";
|
|
31
30
|
import { managerClient } from "@/managerClient";
|
|
32
31
|
import useSliceMachineActions from "@/modules/useSliceMachineActions";
|
|
33
32
|
|
|
34
33
|
import { Slice, SliceCard } from "./SliceCard";
|
|
35
34
|
|
|
36
|
-
const clipboardDataSchema = z.object({
|
|
37
|
-
__type: z.literal("figma-to-prismic/clipboard-data"),
|
|
38
|
-
name: z.string(),
|
|
39
|
-
image: z.string().startsWith("data:image/"),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
35
|
const IMAGE_UPLOAD_LIMIT = 10;
|
|
43
36
|
|
|
44
37
|
interface CreateSliceFromImageModalProps {
|
|
45
38
|
open: boolean;
|
|
46
39
|
location: "custom_type" | "page_type" | "slices";
|
|
47
|
-
onSuccess: (args: {
|
|
40
|
+
onSuccess: (args: {
|
|
41
|
+
slices: {
|
|
42
|
+
model: SharedSlice;
|
|
43
|
+
langSmithUrl?: string;
|
|
44
|
+
}[];
|
|
45
|
+
library: string;
|
|
46
|
+
}) => void;
|
|
48
47
|
onClose: () => void;
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
const clipboardDataSchema = z.object({
|
|
51
|
+
__type: z.literal("figma-to-prismic/clipboard-data"),
|
|
52
|
+
name: z.string(),
|
|
53
|
+
image: z.string().startsWith("data:image/"),
|
|
54
|
+
});
|
|
55
|
+
|
|
51
56
|
export function CreateSliceFromImageModal(
|
|
52
57
|
props: CreateSliceFromImageModalProps,
|
|
53
58
|
) {
|
|
54
|
-
const { open, location,
|
|
59
|
+
const { open, location, onSuccess, onClose } = props;
|
|
55
60
|
const [slices, setSlices] = useState<Slice[]>([]);
|
|
56
|
-
const [
|
|
57
|
-
|
|
61
|
+
const [isCreatingSlices, setIsCreatingSlices] = useState(false);
|
|
58
62
|
const { syncChanges } = useAutoSync();
|
|
59
63
|
const { createSliceSuccess } = useSliceMachineActions();
|
|
60
64
|
const { completeStep } = useOnboarding();
|
|
61
|
-
const
|
|
62
|
-
|
|
65
|
+
const isFigmaEnabled = useIsFigmaEnabled();
|
|
66
|
+
|
|
63
67
|
/**
|
|
64
68
|
* Keeps track of the current instance id.
|
|
65
69
|
* When the modal is closed, the id is reset.
|
|
66
70
|
*/
|
|
67
71
|
const id = useRef(crypto.randomUUID());
|
|
68
|
-
const isFigmaEnabled = useIsFigmaEnabled();
|
|
69
72
|
|
|
70
73
|
useHotkeys(
|
|
71
74
|
["meta+v", "ctrl+v"],
|
|
@@ -76,10 +79,6 @@ export function CreateSliceFromImageModal(
|
|
|
76
79
|
{ enabled: open && isFigmaEnabled },
|
|
77
80
|
);
|
|
78
81
|
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
return () => void cancelActiveRequests();
|
|
81
|
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
82
|
-
|
|
83
82
|
const setSlice = (args: {
|
|
84
83
|
index: number;
|
|
85
84
|
slice: (prevSlice: Slice) => Slice;
|
|
@@ -88,9 +87,14 @@ export function CreateSliceFromImageModal(
|
|
|
88
87
|
setSlices((slices) => slices.map((s, i) => (i === index ? slice(s) : s)));
|
|
89
88
|
};
|
|
90
89
|
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
90
|
+
const onOpenChange = (open: boolean) => {
|
|
91
|
+
if (open || isCreatingSlices) return;
|
|
92
|
+
onClose();
|
|
93
|
+
id.current = crypto.randomUUID();
|
|
94
|
+
setSlices([]);
|
|
95
|
+
};
|
|
93
96
|
|
|
97
|
+
const onImagesSelected = (images: File[]) => {
|
|
94
98
|
if (images.length > IMAGE_UPLOAD_LIMIT) {
|
|
95
99
|
toast.error(
|
|
96
100
|
`You can only upload ${IMAGE_UPLOAD_LIMIT} images at a time.`,
|
|
@@ -98,22 +102,169 @@ export function CreateSliceFromImageModal(
|
|
|
98
102
|
return;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
),
|
|
111
|
-
|
|
105
|
+
setSlices(
|
|
106
|
+
images.map((image) => ({
|
|
107
|
+
source: "upload",
|
|
108
|
+
status: "uploading",
|
|
109
|
+
image,
|
|
110
|
+
})),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
images.forEach((image, index) =>
|
|
114
|
+
uploadImage({ index, image, source: "upload" }),
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const uploadImage = (args: {
|
|
119
|
+
index: number;
|
|
120
|
+
image: File;
|
|
121
|
+
source: "upload" | "figma";
|
|
122
|
+
}) => {
|
|
123
|
+
const { index, image, source } = args;
|
|
124
|
+
const currentId = id.current;
|
|
125
|
+
|
|
126
|
+
setSlice({
|
|
127
|
+
index,
|
|
128
|
+
slice: (prevSlice) => ({
|
|
129
|
+
...prevSlice,
|
|
130
|
+
status: "uploading",
|
|
131
|
+
source,
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
getImageUrl({ image }).then(
|
|
136
|
+
(imageUrl) => {
|
|
137
|
+
if (currentId !== id.current) return;
|
|
138
|
+
void inferSlice({ index, imageUrl, source });
|
|
139
|
+
},
|
|
140
|
+
() => {
|
|
141
|
+
if (currentId !== id.current) return;
|
|
142
|
+
setSlice({
|
|
143
|
+
index,
|
|
144
|
+
slice: (prevSlice) => ({
|
|
145
|
+
...prevSlice,
|
|
146
|
+
status: "uploadError",
|
|
147
|
+
onRetry: () => uploadImage({ index, image, source }),
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const existingSlices = useExistingSlices({ open });
|
|
155
|
+
|
|
156
|
+
const inferSlice = async (args: {
|
|
157
|
+
index: number;
|
|
158
|
+
imageUrl: string;
|
|
159
|
+
source: "upload" | "figma";
|
|
160
|
+
}) => {
|
|
161
|
+
const { index, imageUrl, source } = args;
|
|
162
|
+
const currentId = id.current;
|
|
112
163
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
164
|
+
const libraryID = await getLibraryID();
|
|
165
|
+
|
|
166
|
+
setSlice({
|
|
167
|
+
index,
|
|
168
|
+
slice: (prevSlice) => ({
|
|
169
|
+
...prevSlice,
|
|
170
|
+
status: "generating",
|
|
171
|
+
thumbnailUrl: imageUrl,
|
|
172
|
+
}),
|
|
116
173
|
});
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const inferResult = await managerClient.customTypes.inferSlice({
|
|
177
|
+
imageUrl,
|
|
178
|
+
source,
|
|
179
|
+
libraryID,
|
|
180
|
+
});
|
|
181
|
+
if (currentId !== id.current) return;
|
|
182
|
+
|
|
183
|
+
const model = sliceWithoutConflicts({
|
|
184
|
+
existingSlices: existingSlices.current,
|
|
185
|
+
newSlices: slices,
|
|
186
|
+
slice: inferResult.slice,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
setSlices((prevSlices) =>
|
|
190
|
+
prevSlices.map((prevSlice, i) =>
|
|
191
|
+
i === index
|
|
192
|
+
? {
|
|
193
|
+
...prevSlice,
|
|
194
|
+
status: "success",
|
|
195
|
+
thumbnailUrl: imageUrl,
|
|
196
|
+
langSmithUrl: inferResult.langSmithUrl,
|
|
197
|
+
model,
|
|
198
|
+
}
|
|
199
|
+
: prevSlice,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
} catch {
|
|
203
|
+
if (currentId !== id.current) return;
|
|
204
|
+
setSlice({
|
|
205
|
+
index,
|
|
206
|
+
slice: (prevSlice) => ({
|
|
207
|
+
...prevSlice,
|
|
208
|
+
status: "generateError",
|
|
209
|
+
thumbnailUrl: imageUrl,
|
|
210
|
+
onRetry: () => void inferSlice({ index, imageUrl, source }),
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const onSubmit = () => {
|
|
217
|
+
const newSlices = slices.reduce<NewSlice[]>((acc, slice) => {
|
|
218
|
+
if (slice.status === "success" && slice.source === "upload") {
|
|
219
|
+
acc.push(slice);
|
|
220
|
+
}
|
|
221
|
+
return acc;
|
|
222
|
+
}, []);
|
|
223
|
+
if (!newSlices.length) return;
|
|
224
|
+
|
|
225
|
+
const currentId = id.current;
|
|
226
|
+
setIsCreatingSlices(true);
|
|
227
|
+
addSlices(newSlices)
|
|
228
|
+
.then(async ({ slices, library }) => {
|
|
229
|
+
if (currentId !== id.current) return;
|
|
230
|
+
|
|
231
|
+
const serverState = await getState();
|
|
232
|
+
createSliceSuccess(serverState.libraries);
|
|
233
|
+
syncChanges();
|
|
234
|
+
|
|
235
|
+
onSuccess({ slices, library });
|
|
236
|
+
|
|
237
|
+
setIsCreatingSlices(false);
|
|
238
|
+
id.current = crypto.randomUUID();
|
|
239
|
+
setSlices([]);
|
|
240
|
+
|
|
241
|
+
void completeStep("createSlice");
|
|
242
|
+
|
|
243
|
+
for (const { model, langSmithUrl } of slices) {
|
|
244
|
+
void telemetry.track({
|
|
245
|
+
event: "slice:created",
|
|
246
|
+
id: model.id,
|
|
247
|
+
name: model.name,
|
|
248
|
+
library,
|
|
249
|
+
location,
|
|
250
|
+
mode: "ai",
|
|
251
|
+
langSmithUrl,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
addAiFeedback({
|
|
255
|
+
type: "model",
|
|
256
|
+
library,
|
|
257
|
+
sliceId: model.id,
|
|
258
|
+
variationId: model.variations[0].id,
|
|
259
|
+
langSmithUrl,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
.catch(() => {
|
|
264
|
+
if (currentId !== id.current) return;
|
|
265
|
+
setIsCreatingSlices(false);
|
|
266
|
+
toast.error("An unexpected error happened while adding slices.");
|
|
267
|
+
});
|
|
117
268
|
};
|
|
118
269
|
|
|
119
270
|
const handlePaste = async () => {
|
|
@@ -227,7 +378,7 @@ export function CreateSliceFromImageModal(
|
|
|
227
378
|
]);
|
|
228
379
|
|
|
229
380
|
// Start uploading the new image
|
|
230
|
-
void uploadImage({ index: newIndex, imageData, source: "figma" });
|
|
381
|
+
void uploadImage({ index: newIndex, image: imageData, source: "figma" });
|
|
231
382
|
|
|
232
383
|
toast.success(`Pasted ${imageName}${success ? " from Figma" : ""}`);
|
|
233
384
|
} catch (error) {
|
|
@@ -238,421 +389,60 @@ export function CreateSliceFromImageModal(
|
|
|
238
389
|
}
|
|
239
390
|
};
|
|
240
391
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const { index, imageData, source } = args;
|
|
247
|
-
const currentId = id.current;
|
|
248
|
-
|
|
249
|
-
setSlice({
|
|
250
|
-
index,
|
|
251
|
-
slice: (prevSlice) => ({
|
|
252
|
-
...prevSlice,
|
|
253
|
-
status: "uploading",
|
|
254
|
-
image: imageData,
|
|
255
|
-
source,
|
|
256
|
-
}),
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
const imageUrl = await getImageUrl({ image: imageData });
|
|
261
|
-
if (currentId !== id.current) return;
|
|
262
|
-
|
|
263
|
-
setSlice({
|
|
264
|
-
index,
|
|
265
|
-
slice: (prevSlice) => ({
|
|
266
|
-
...prevSlice,
|
|
267
|
-
status: "pending",
|
|
268
|
-
thumbnailUrl: imageUrl,
|
|
269
|
-
}),
|
|
270
|
-
});
|
|
271
|
-
} catch {
|
|
272
|
-
if (currentId !== id.current) return;
|
|
273
|
-
setSlice({
|
|
274
|
-
index,
|
|
275
|
-
slice: (prevSlice) => ({
|
|
276
|
-
...prevSlice,
|
|
277
|
-
status: "uploadError",
|
|
278
|
-
onRetry: () => void uploadImage({ index, imageData, source }),
|
|
279
|
-
}),
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const inferSlice = async (args: {
|
|
285
|
-
index: number;
|
|
286
|
-
imageUrl: string;
|
|
287
|
-
libraryID: string;
|
|
288
|
-
source: "figma" | "upload";
|
|
289
|
-
}) => {
|
|
290
|
-
const { index, imageUrl, libraryID, source } = args;
|
|
291
|
-
let currentId = id.current;
|
|
292
|
-
|
|
293
|
-
const requestId = crypto.randomUUID();
|
|
294
|
-
|
|
295
|
-
setSlice({
|
|
296
|
-
index,
|
|
297
|
-
slice: (prevSlice) => ({
|
|
298
|
-
...prevSlice,
|
|
299
|
-
status: "generating",
|
|
300
|
-
thumbnailUrl: imageUrl,
|
|
301
|
-
requestId,
|
|
302
|
-
}),
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
const inferResult = await managerClient.customTypes.inferSlice({
|
|
307
|
-
source,
|
|
308
|
-
libraryID,
|
|
309
|
-
imageUrl,
|
|
310
|
-
requestId,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
if (currentId !== id.current) return;
|
|
314
|
-
|
|
315
|
-
const model = sliceWithoutConflicts({
|
|
316
|
-
existingSlices: existingSlices.current,
|
|
317
|
-
newSlices: slices,
|
|
318
|
-
slice: inferResult.slice,
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
setSlices((prevSlices) => {
|
|
322
|
-
return prevSlices.map((prevSlice, i) => {
|
|
323
|
-
if (i !== index) return prevSlice;
|
|
324
|
-
return {
|
|
325
|
-
...prevSlice,
|
|
326
|
-
status: "success",
|
|
327
|
-
thumbnailUrl: imageUrl,
|
|
328
|
-
model,
|
|
329
|
-
langSmithUrl: inferResult.langSmithUrl,
|
|
330
|
-
};
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
if (source === "upload") {
|
|
335
|
-
currentId = id.current;
|
|
336
|
-
const currentSlice = slices[index];
|
|
337
|
-
|
|
338
|
-
const { errors } = await managerClient.slices.createSlice({
|
|
339
|
-
libraryID,
|
|
340
|
-
model: model,
|
|
341
|
-
});
|
|
342
|
-
if (errors.length) {
|
|
343
|
-
throw new Error(`Failed to create slice ${model.id}.`);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
await managerClient.slices.updateSliceScreenshot({
|
|
347
|
-
libraryID,
|
|
348
|
-
sliceID: model.id,
|
|
349
|
-
variationID: model.variations[0].id,
|
|
350
|
-
data: currentSlice.image,
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
if (currentId !== id.current) return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
void completeStep("createSlice");
|
|
357
|
-
|
|
358
|
-
void telemetry.track({
|
|
359
|
-
event: "slice:created",
|
|
360
|
-
id: model.id,
|
|
361
|
-
name: model.name,
|
|
362
|
-
library: libraryID,
|
|
363
|
-
location,
|
|
364
|
-
mode: "ai",
|
|
365
|
-
langSmithUrl: inferResult.langSmithUrl,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
addAiFeedback({
|
|
369
|
-
type: "model",
|
|
370
|
-
library: libraryID,
|
|
371
|
-
sliceId: model.id,
|
|
372
|
-
variationId: model.variations[0].id,
|
|
373
|
-
langSmithUrl: inferResult.langSmithUrl,
|
|
374
|
-
});
|
|
375
|
-
} catch (error) {
|
|
376
|
-
if (currentId !== id.current) return;
|
|
377
|
-
|
|
378
|
-
setSlice({
|
|
379
|
-
index,
|
|
380
|
-
slice: (prevSlice) => ({
|
|
381
|
-
...prevSlice,
|
|
382
|
-
status:
|
|
383
|
-
error instanceof Error && error.name === "AbortError"
|
|
384
|
-
? "cancelled"
|
|
385
|
-
: "generateError",
|
|
386
|
-
thumbnailUrl: imageUrl,
|
|
387
|
-
onRetry: () => {
|
|
388
|
-
void inferSlice({ index, imageUrl, libraryID, source });
|
|
389
|
-
},
|
|
390
|
-
}),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
const generatePendingSlices = () => {
|
|
396
|
-
if (libraryID === undefined) return;
|
|
397
|
-
|
|
398
|
-
slices.forEach((slice, index) => {
|
|
399
|
-
if (slice.status === "pending") {
|
|
400
|
-
void inferSlice({
|
|
401
|
-
index,
|
|
402
|
-
libraryID,
|
|
403
|
-
imageUrl: slice.thumbnailUrl,
|
|
404
|
-
source: slice.source,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const cancelActiveRequests = async () => {
|
|
411
|
-
const cancelableIds = slices.flatMap((slice) =>
|
|
412
|
-
slice.status === "generating" ? [slice.requestId] : [],
|
|
413
|
-
);
|
|
414
|
-
if (cancelableIds.length === 0) return;
|
|
415
|
-
|
|
416
|
-
await Promise.all(
|
|
417
|
-
cancelableIds.map((requestId) => {
|
|
418
|
-
return managerClient.customTypes.cancelInferSlice({ requestId });
|
|
419
|
-
}),
|
|
420
|
-
);
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
const closeModal = (args?: { cancelActiveRequests?: boolean }) => {
|
|
424
|
-
if (loadingSliceCount > 0) return;
|
|
425
|
-
if (args?.cancelActiveRequests ?? true) {
|
|
426
|
-
void cancelActiveRequests();
|
|
427
|
-
}
|
|
428
|
-
onClose();
|
|
429
|
-
id.current = crypto.randomUUID();
|
|
430
|
-
setTimeout(() => setSlices([]), 250); // wait for the modal fade animation
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
const onSubmit = async () => {
|
|
434
|
-
try {
|
|
435
|
-
setIsSubmitting(true);
|
|
436
|
-
if (libraryID === undefined) return;
|
|
437
|
-
|
|
438
|
-
const serverState = await getState();
|
|
439
|
-
createSliceSuccess(serverState.libraries);
|
|
440
|
-
syncChanges();
|
|
441
|
-
|
|
442
|
-
onSuccess({
|
|
443
|
-
slices: slices.flatMap((slice) =>
|
|
444
|
-
slice.status === "success" ? slice.model : [],
|
|
445
|
-
),
|
|
446
|
-
library: libraryID,
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
closeModal({ cancelActiveRequests: false });
|
|
450
|
-
} finally {
|
|
451
|
-
setIsSubmitting(false);
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
const generatingSliceCount = slices.filter((slice) => {
|
|
456
|
-
return slice.status === "generating";
|
|
457
|
-
}).length;
|
|
458
|
-
|
|
459
|
-
const uploadingSliceCount = slices.filter((slice) => {
|
|
460
|
-
return slice.status === "uploading";
|
|
461
|
-
}).length;
|
|
462
|
-
|
|
463
|
-
const loadingSliceCount = generatingSliceCount + uploadingSliceCount;
|
|
464
|
-
|
|
465
|
-
const pendingSliceCount = slices.filter((slice) => {
|
|
466
|
-
return slice.status === "pending";
|
|
467
|
-
}).length;
|
|
468
|
-
|
|
469
|
-
const completedSliceCount = slices.filter((slice) => {
|
|
470
|
-
return slice.status === "success";
|
|
471
|
-
}).length;
|
|
472
|
-
|
|
473
|
-
const hasTriggeredGeneration = slices.some((slice) => {
|
|
474
|
-
return slice.status === "generating" || slice.status === "success";
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
const generateSliceCount = loadingSliceCount + pendingSliceCount;
|
|
392
|
+
const areSlicesLoading = slices.some(
|
|
393
|
+
(slice) => slice.status === "uploading" || slice.status === "generating",
|
|
394
|
+
);
|
|
395
|
+
const readySlices = slices.filter((slice) => slice.status === "success");
|
|
396
|
+
const someSlicesReady = readySlices.length > 0;
|
|
478
397
|
|
|
479
398
|
return (
|
|
480
|
-
<Dialog open={open} onOpenChange={
|
|
481
|
-
<DialogHeader title="Generate
|
|
399
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
400
|
+
<DialogHeader title="Generate from image" />
|
|
482
401
|
<DialogContent gap={0}>
|
|
483
402
|
<DialogDescription hidden>
|
|
484
403
|
Upload images to generate slices with AI
|
|
485
404
|
</DialogDescription>
|
|
486
|
-
{
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
flexDirection="column"
|
|
495
|
-
>
|
|
496
|
-
{isFigmaEnabled && (
|
|
497
|
-
<Box
|
|
498
|
-
display="flex"
|
|
499
|
-
gap={16}
|
|
500
|
-
alignItems="center"
|
|
501
|
-
backgroundColor="grey2"
|
|
502
|
-
padding={16}
|
|
503
|
-
borderRadius={12}
|
|
504
|
-
>
|
|
505
|
-
<Box
|
|
506
|
-
display="flex"
|
|
507
|
-
gap={8}
|
|
508
|
-
alignItems="center"
|
|
509
|
-
flexGrow={1}
|
|
510
|
-
>
|
|
511
|
-
<Box
|
|
512
|
-
width={48}
|
|
513
|
-
height={48}
|
|
514
|
-
backgroundColor="grey12"
|
|
515
|
-
borderRadius="100%"
|
|
516
|
-
display="flex"
|
|
517
|
-
alignItems="center"
|
|
518
|
-
justifyContent="center"
|
|
519
|
-
>
|
|
520
|
-
<FigmaIcon variant="original" height={25} />
|
|
521
|
-
</Box>
|
|
522
|
-
<Box display="flex" flexDirection="column" flexGrow={1}>
|
|
523
|
-
<Text variant="bold">Want to work faster?</Text>
|
|
524
|
-
<Text variant="small" color="grey11">
|
|
525
|
-
Copy frames from Figma with the Slice Machine plugin
|
|
526
|
-
and paste them here.
|
|
527
|
-
</Text>
|
|
528
|
-
</Box>
|
|
529
|
-
</Box>
|
|
530
|
-
<Button
|
|
531
|
-
endIcon="arrowForward"
|
|
532
|
-
color="indigo"
|
|
533
|
-
onClick={() =>
|
|
534
|
-
window.open(
|
|
535
|
-
"https://www.figma.com/community/plugin/TODO",
|
|
536
|
-
"_blank",
|
|
537
|
-
)
|
|
538
|
-
}
|
|
539
|
-
sx={{ marginRight: 8 }}
|
|
540
|
-
invisible
|
|
541
|
-
>
|
|
542
|
-
Install plugin
|
|
543
|
-
</Button>
|
|
544
|
-
</Box>
|
|
545
|
-
)}
|
|
546
|
-
<FileDropZone
|
|
405
|
+
{slices.length === 0 ? (
|
|
406
|
+
<Box padding={16} height="100%">
|
|
407
|
+
<FileDropZone
|
|
408
|
+
onFilesSelected={onImagesSelected}
|
|
409
|
+
assetType="image"
|
|
410
|
+
maxFiles={IMAGE_UPLOAD_LIMIT}
|
|
411
|
+
overlay={
|
|
412
|
+
<UploadBlankSlate
|
|
547
413
|
onFilesSelected={onImagesSelected}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
droppingFiles
|
|
555
|
-
/>
|
|
556
|
-
}
|
|
557
|
-
>
|
|
558
|
-
<UploadBlankSlate
|
|
559
|
-
onFilesSelected={onImagesSelected}
|
|
560
|
-
onPaste={() => void handlePaste()}
|
|
561
|
-
/>
|
|
562
|
-
</FileDropZone>
|
|
563
|
-
</Box>
|
|
564
|
-
) : (
|
|
565
|
-
<>
|
|
566
|
-
<Box
|
|
567
|
-
display="flex"
|
|
568
|
-
alignItems="center"
|
|
569
|
-
justifyContent="space-between"
|
|
570
|
-
padding={16}
|
|
571
|
-
>
|
|
572
|
-
<Text variant="h3">Design</Text>
|
|
573
|
-
<FileUploadButton
|
|
574
|
-
size="medium"
|
|
575
|
-
color="grey"
|
|
576
|
-
onFilesSelected={onImagesSelected}
|
|
577
|
-
startIcon="attachFile"
|
|
578
|
-
disabled={hasTriggeredGeneration}
|
|
579
|
-
>
|
|
580
|
-
Add images
|
|
581
|
-
</FileUploadButton>
|
|
582
|
-
</Box>
|
|
583
|
-
<ScrollArea stableScrollbar={false}>
|
|
584
|
-
<Box
|
|
585
|
-
display="grid"
|
|
586
|
-
gridTemplateColumns="1fr 1fr"
|
|
587
|
-
gap={16}
|
|
588
|
-
padding={16}
|
|
589
|
-
>
|
|
590
|
-
{slices.map((slice, index) => (
|
|
591
|
-
<SliceCard slice={slice} key={`slice-${index}`} />
|
|
592
|
-
))}
|
|
593
|
-
</Box>
|
|
594
|
-
</ScrollArea>
|
|
595
|
-
</>
|
|
596
|
-
)}
|
|
597
|
-
<DialogActions>
|
|
598
|
-
{generatingSliceCount > 0 ? (
|
|
599
|
-
<DialogCancelButton
|
|
600
|
-
onClick={() => void cancelActiveRequests()}
|
|
601
|
-
size="medium"
|
|
602
|
-
sx={{ marginRight: 8 }}
|
|
603
|
-
invisible
|
|
604
|
-
>
|
|
605
|
-
Cancel
|
|
606
|
-
</DialogCancelButton>
|
|
607
|
-
) : (
|
|
608
|
-
<DialogCancelButton
|
|
609
|
-
onClick={() => closeModal()}
|
|
610
|
-
size="medium"
|
|
611
|
-
sx={{ marginRight: 8 }}
|
|
612
|
-
invisible
|
|
613
|
-
>
|
|
614
|
-
Close
|
|
615
|
-
</DialogCancelButton>
|
|
616
|
-
)}
|
|
617
|
-
{completedSliceCount === 0 || loadingSliceCount > 0 ? (
|
|
618
|
-
<DialogActionButton
|
|
619
|
-
color="purple"
|
|
620
|
-
startIcon="autoFixHigh"
|
|
621
|
-
onClick={() => void generatePendingSlices()}
|
|
622
|
-
disabled={
|
|
623
|
-
hasTriggeredGeneration ||
|
|
624
|
-
loadingSliceCount > 0 ||
|
|
625
|
-
pendingSliceCount === 0
|
|
626
|
-
}
|
|
627
|
-
loading={loadingSliceCount > 0}
|
|
628
|
-
size="medium"
|
|
629
|
-
>
|
|
630
|
-
Generate{" "}
|
|
631
|
-
{generateSliceCount > 0 ? `(${generateSliceCount}) ` : ""}
|
|
632
|
-
{generateSliceCount === 1 ? "Slice" : "Slices"}
|
|
633
|
-
</DialogActionButton>
|
|
634
|
-
) : (
|
|
635
|
-
<DialogActionButton
|
|
636
|
-
color="purple"
|
|
637
|
-
onClick={() => void onSubmit()}
|
|
638
|
-
loading={isSubmitting}
|
|
639
|
-
size="medium"
|
|
640
|
-
>
|
|
641
|
-
{getSubmitButtonLabel(location, completedSliceCount)}
|
|
642
|
-
</DialogActionButton>
|
|
643
|
-
)}
|
|
644
|
-
</DialogActions>
|
|
645
|
-
</>
|
|
646
|
-
) : (
|
|
647
|
-
<Box
|
|
648
|
-
display="flex"
|
|
649
|
-
justifyContent="center"
|
|
650
|
-
alignItems="center"
|
|
651
|
-
height="100%"
|
|
652
|
-
>
|
|
653
|
-
<ProgressCircle color="purple9" />
|
|
414
|
+
droppingFiles
|
|
415
|
+
/>
|
|
416
|
+
}
|
|
417
|
+
>
|
|
418
|
+
<UploadBlankSlate onFilesSelected={onImagesSelected} />
|
|
419
|
+
</FileDropZone>
|
|
654
420
|
</Box>
|
|
421
|
+
) : (
|
|
422
|
+
<ScrollArea stableScrollbar={false}>
|
|
423
|
+
<Box
|
|
424
|
+
display="grid"
|
|
425
|
+
gridTemplateColumns="1fr 1fr"
|
|
426
|
+
gap={16}
|
|
427
|
+
padding={16}
|
|
428
|
+
>
|
|
429
|
+
{slices.map((slice, index) => (
|
|
430
|
+
<SliceCard slice={slice} key={`slice-${index}`} />
|
|
431
|
+
))}
|
|
432
|
+
</Box>
|
|
433
|
+
</ScrollArea>
|
|
655
434
|
)}
|
|
435
|
+
|
|
436
|
+
<DialogActions>
|
|
437
|
+
<DialogCancelButton disabled={isCreatingSlices} />
|
|
438
|
+
<DialogActionButton
|
|
439
|
+
disabled={!someSlicesReady || areSlicesLoading}
|
|
440
|
+
loading={isCreatingSlices}
|
|
441
|
+
onClick={onSubmit}
|
|
442
|
+
>
|
|
443
|
+
{getSubmitButtonLabel(location)} ({readySlices.length})
|
|
444
|
+
</DialogActionButton>
|
|
445
|
+
</DialogActions>
|
|
656
446
|
</DialogContent>
|
|
657
447
|
</Dialog>
|
|
658
448
|
);
|
|
@@ -661,10 +451,8 @@ export function CreateSliceFromImageModal(
|
|
|
661
451
|
function UploadBlankSlate(props: {
|
|
662
452
|
droppingFiles?: boolean;
|
|
663
453
|
onFilesSelected: (files: File[]) => void;
|
|
664
|
-
onPaste: () => void;
|
|
665
454
|
}) {
|
|
666
|
-
const { droppingFiles = false, onFilesSelected
|
|
667
|
-
const isFigmaEnabled = useIsFigmaEnabled();
|
|
455
|
+
const { droppingFiles = false, onFilesSelected } = props;
|
|
668
456
|
|
|
669
457
|
return (
|
|
670
458
|
<Box
|
|
@@ -675,72 +463,27 @@ function UploadBlankSlate(props: {
|
|
|
675
463
|
border
|
|
676
464
|
borderStyle="dashed"
|
|
677
465
|
borderColor={droppingFiles ? "purple9" : "grey6"}
|
|
678
|
-
borderRadius={12}
|
|
679
|
-
flexGrow={1}
|
|
680
466
|
>
|
|
681
467
|
<BlankSlate>
|
|
682
|
-
<
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
468
|
+
<BlankSlateIcon
|
|
469
|
+
lineColor="purple11"
|
|
470
|
+
backgroundColor="purple5"
|
|
471
|
+
name="cloudUpload"
|
|
472
|
+
size="large"
|
|
473
|
+
/>
|
|
474
|
+
<BlankSlateTitle>Upload your design images.</BlankSlateTitle>
|
|
475
|
+
<BlankSlateDescription>
|
|
476
|
+
Once uploaded, you can generate slices automatically using AI.
|
|
477
|
+
</BlankSlateDescription>
|
|
478
|
+
<BlankSlateActions>
|
|
479
|
+
<FileUploadButton
|
|
480
|
+
startIcon="attachFile"
|
|
481
|
+
onFilesSelected={onFilesSelected}
|
|
482
|
+
color="grey"
|
|
694
483
|
>
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
<Text variant="small" color="grey11">
|
|
699
|
-
Upload your design images or paste them directly from Figma.
|
|
700
|
-
</Text>
|
|
701
|
-
</>
|
|
702
|
-
) : (
|
|
703
|
-
<>
|
|
704
|
-
<Text>Upload your design images.</Text>
|
|
705
|
-
<Text variant="small" color="grey11">
|
|
706
|
-
Once uploaded, you can generate slices automatically using AI.
|
|
707
|
-
</Text>
|
|
708
|
-
</>
|
|
709
|
-
)}
|
|
710
|
-
</Box>
|
|
711
|
-
<Box display="flex" alignItems="center" gap={16}>
|
|
712
|
-
{isFigmaEnabled ? (
|
|
713
|
-
<>
|
|
714
|
-
<Button
|
|
715
|
-
size="small"
|
|
716
|
-
renderStartIcon={() => (
|
|
717
|
-
<FigmaIcon variant="original" height={16} />
|
|
718
|
-
)}
|
|
719
|
-
color="grey"
|
|
720
|
-
onClick={onPaste}
|
|
721
|
-
>
|
|
722
|
-
Paste from Figma
|
|
723
|
-
</Button>
|
|
724
|
-
<FileUploadButton
|
|
725
|
-
size="small"
|
|
726
|
-
onFilesSelected={onFilesSelected}
|
|
727
|
-
color="purple"
|
|
728
|
-
invisible
|
|
729
|
-
>
|
|
730
|
-
Add images
|
|
731
|
-
</FileUploadButton>
|
|
732
|
-
</>
|
|
733
|
-
) : (
|
|
734
|
-
<FileUploadButton
|
|
735
|
-
startIcon="attachFile"
|
|
736
|
-
onFilesSelected={onFilesSelected}
|
|
737
|
-
color="grey"
|
|
738
|
-
>
|
|
739
|
-
Add images
|
|
740
|
-
</FileUploadButton>
|
|
741
|
-
)}
|
|
742
|
-
</Box>
|
|
743
|
-
</Box>
|
|
484
|
+
Add images
|
|
485
|
+
</FileUploadButton>
|
|
486
|
+
</BlankSlateActions>
|
|
744
487
|
</BlankSlate>
|
|
745
488
|
</Box>
|
|
746
489
|
);
|
|
@@ -763,6 +506,12 @@ async function getImageUrl({ image }: { image: File }) {
|
|
|
763
506
|
return url;
|
|
764
507
|
}
|
|
765
508
|
|
|
509
|
+
type NewSlice = {
|
|
510
|
+
image: File;
|
|
511
|
+
model: SharedSlice;
|
|
512
|
+
langSmithUrl?: string;
|
|
513
|
+
};
|
|
514
|
+
|
|
766
515
|
/**
|
|
767
516
|
* Keeps track of the existing slices in the project.
|
|
768
517
|
* Re-fetches them when the modal is opened.
|
|
@@ -834,42 +583,68 @@ function sliceWithoutConflicts({
|
|
|
834
583
|
};
|
|
835
584
|
}
|
|
836
585
|
|
|
837
|
-
function
|
|
838
|
-
|
|
586
|
+
async function addSlices(newSlices: NewSlice[]) {
|
|
587
|
+
// use the first library
|
|
588
|
+
const { libraries = [] } =
|
|
589
|
+
await managerClient.project.getSliceMachineConfig();
|
|
590
|
+
const library = libraries[0];
|
|
591
|
+
if (!library) {
|
|
592
|
+
throw new Error("No library found in the config.");
|
|
593
|
+
}
|
|
839
594
|
|
|
840
|
-
|
|
841
|
-
managerClient.
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
})
|
|
850
|
-
.catch(() => {
|
|
851
|
-
throw new Error("Could not get library ID from the config.");
|
|
852
|
-
});
|
|
853
|
-
}, []);
|
|
595
|
+
for (const { model } of newSlices) {
|
|
596
|
+
const { errors } = await managerClient.slices.createSlice({
|
|
597
|
+
libraryID: library,
|
|
598
|
+
model,
|
|
599
|
+
});
|
|
600
|
+
if (errors.length) {
|
|
601
|
+
throw new Error(`Failed to create slice ${model.id}.`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
854
604
|
|
|
855
|
-
|
|
856
|
-
|
|
605
|
+
// for each added slice, set the variation screenshot
|
|
606
|
+
const slices = await Promise.all(
|
|
607
|
+
newSlices.map(async ({ model, image, langSmithUrl }) => {
|
|
608
|
+
await managerClient.slices.updateSliceScreenshot({
|
|
609
|
+
libraryID: library,
|
|
610
|
+
sliceID: model.id,
|
|
611
|
+
variationID: model.variations[0].id,
|
|
612
|
+
data: image,
|
|
613
|
+
});
|
|
614
|
+
return {
|
|
615
|
+
model,
|
|
616
|
+
langSmithUrl,
|
|
617
|
+
};
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
857
620
|
|
|
858
|
-
|
|
859
|
-
const experiment = useExperimentVariant("llm-proxy-access");
|
|
860
|
-
return experiment?.value === "on";
|
|
621
|
+
return { library, slices };
|
|
861
622
|
}
|
|
862
623
|
|
|
863
624
|
const getSubmitButtonLabel = (
|
|
864
625
|
location: "custom_type" | "page_type" | "slices",
|
|
865
|
-
completedSliceCount: number,
|
|
866
626
|
) => {
|
|
867
627
|
switch (location) {
|
|
868
628
|
case "custom_type":
|
|
869
|
-
return
|
|
629
|
+
return "Add to type";
|
|
870
630
|
case "page_type":
|
|
871
|
-
return
|
|
631
|
+
return "Add to page";
|
|
872
632
|
case "slices":
|
|
873
|
-
return "
|
|
633
|
+
return "Add to slices";
|
|
874
634
|
}
|
|
875
635
|
};
|
|
636
|
+
|
|
637
|
+
function useIsFigmaEnabled() {
|
|
638
|
+
const experiment = useExperimentVariant("llm-proxy-access");
|
|
639
|
+
return experiment?.value === "on";
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function getLibraryID() {
|
|
643
|
+
return managerClient.project.getSliceMachineConfig().then((smConfig) => {
|
|
644
|
+
const libraryID = smConfig?.libraries?.[0];
|
|
645
|
+
if (libraryID === undefined) {
|
|
646
|
+
throw new Error("No library found in the config.");
|
|
647
|
+
}
|
|
648
|
+
return libraryID;
|
|
649
|
+
});
|
|
650
|
+
}
|