slice-machine-ui 2.19.2-alpha.jp-figma-to-prismic.6 → 2.19.2-alpha.jp-figma-to-slice-1.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.
Files changed (33) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/34-28725deef8b874b1.js +1 -0
  3. package/out/_next/static/chunks/{489-2e83dd8ae83fd5ed.js → 489-ce3053e1d81ade83.js} +1 -1
  4. package/out/_next/static/chunks/658-8231c0b729e0124a.js +1 -0
  5. package/out/_next/static/chunks/907-bf4215e6fc238ea0.js +1 -0
  6. package/out/_next/static/chunks/pages/{_app-bb6f32b7db48868d.js → _app-3484b0abc3128d5b.js} +1 -1
  7. package/out/_next/static/chunks/pages/{changes-b3f45dfeb5dc08f0.js → changes-7f24b37f5bf872ae.js} +1 -1
  8. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-02278526092bcf5c.js → [customTypeId]-1b47424a37b49dff.js} +1 -1
  9. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-4d99de1b52de7c9b.js → [pageTypeId]-385f933c203e8b16.js} +1 -1
  10. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-330e5d545d9f6269.js → [variation]-08cdeefc96106c0c.js} +1 -1
  11. package/out/_next/static/chunks/pages/{slices-d6873e0fb6a46bb8.js → slices-bedcb854fbdca8cd.js} +1 -1
  12. package/out/_next/static/{ZbGFjanapT9xiyrij9b5m → jHu5WYtoQglBKwG60Lmu4}/_buildManifest.js +1 -1
  13. package/out/changelog.html +1 -1
  14. package/out/changes.html +1 -1
  15. package/out/custom-types/[customTypeId].html +1 -1
  16. package/out/custom-types.html +1 -1
  17. package/out/index.html +1 -1
  18. package/out/labs.html +1 -1
  19. package/out/page-types/[pageTypeId].html +1 -1
  20. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  21. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  22. package/out/slices.html +1 -1
  23. package/package.json +3 -3
  24. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/CreateSliceFromImageModal.tsx +310 -535
  25. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +14 -57
  26. package/src/icons/FigmaIcon.tsx +34 -88
  27. package/src/legacy/components/ScreenshotChangesModal/index.tsx +1 -1
  28. package/src/legacy/lib/builders/CustomTypeBuilder/SliceZone/index.tsx +2 -1
  29. package/src/pages/slices.tsx +1 -0
  30. package/out/_next/static/chunks/20-169231cb23a752ff.js +0 -1
  31. package/out/_next/static/chunks/593-97393b59cba3d429.js +0 -1
  32. package/out/_next/static/chunks/907-0f84907d31c989bf.js +0 -1
  33. /package/out/_next/static/{ZbGFjanapT9xiyrij9b5m → jHu5WYtoQglBKwG60Lmu4}/_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: { slices: SharedSlice[]; library: string }) => void;
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, onClose, onSuccess } = props;
59
+ const { open, location, onSuccess, onClose } = props;
55
60
  const [slices, setSlices] = useState<Slice[]>([]);
56
- const [isSubmitting, setIsSubmitting] = useState(false);
57
-
61
+ const [isCreatingSlices, setIsCreatingSlices] = useState(false);
58
62
  const { syncChanges } = useAutoSync();
59
63
  const { createSliceSuccess } = useSliceMachineActions();
60
64
  const { completeStep } = useOnboarding();
61
- const existingSlices = useExistingSlices({ open });
62
- const { libraryID, isLoading: isLoadingLibraryID } = useLibraryID();
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 onImagesSelected = (images: File[]) => {
92
- if (hasTriggeredGeneration) return;
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
- const startIndex = slices.length;
102
- setSlices((prevSlices) => [
103
- ...prevSlices,
104
- ...images.map(
105
- (image): Slice => ({
106
- source: "upload",
107
- status: "uploading",
108
- image,
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
- images.forEach((imageData, relativeIndex) => {
114
- const index = startIndex + relativeIndex;
115
- void uploadImage({ index, imageData, source: "upload" });
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 uploadImage = async (args: {
242
- index: number;
243
- imageData: File;
244
- source: "figma" | "upload";
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={(open) => !open && closeModal()}>
481
- <DialogHeader title="Generate with AI" />
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
- {!isLoadingLibraryID ? (
487
- <>
488
- {slices.length === 0 ? (
489
- <Box
490
- padding={16}
491
- height="100%"
492
- gap={16}
493
- display="flex"
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
- assetType="image"
549
- maxFiles={IMAGE_UPLOAD_LIMIT}
550
- overlay={
551
- <UploadBlankSlate
552
- onFilesSelected={onImagesSelected}
553
- onPaste={() => void handlePaste()}
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, onPaste } = props;
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
- <Box display="flex" flexDirection="column" gap={16} alignItems="center">
683
- <BlankSlateIcon
684
- lineColor="purple11"
685
- backgroundColor="purple5"
686
- name="cloudUpload"
687
- size="large"
688
- />
689
- <Box
690
- display="flex"
691
- flexDirection="column"
692
- gap={4}
693
- alignItems="center"
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
- {isFigmaEnabled ? (
696
- <>
697
- <Text>Generate slices from your designs</Text>
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 useLibraryID() {
838
- const [libraryID, setLibraryID] = useState<string | undefined>();
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
- useEffect(() => {
841
- managerClient.project
842
- .getSliceMachineConfig()
843
- .then((smConfig) => {
844
- const libraryID = smConfig?.libraries?.[0];
845
- if (libraryID === undefined) {
846
- throw new Error("No library found in the config.");
847
- }
848
- setLibraryID(libraryID);
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
- return { libraryID, isLoading: libraryID === undefined };
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
- function useIsFigmaEnabled() {
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 `Add to type (${completedSliceCount})`;
629
+ return "Add to type";
870
630
  case "page_type":
871
- return `Add to page (${completedSliceCount})`;
631
+ return "Add to page";
872
632
  case "slices":
873
- return "Done";
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
+ }