slice-machine-ui 2.19.2-alpha.jp-figma-to-prismic.2 → 2.19.2-alpha.jp-figma-to-prismic.4

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 (25) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/{j0_D1z-ZN75hJO-JvwC0X → J2YngfgTVf7j_xF1hSI57}/_buildManifest.js +1 -1
  3. package/out/_next/static/chunks/20-169231cb23a752ff.js +1 -0
  4. package/out/_next/static/chunks/{489-ce3053e1d81ade83.js → 489-b4b2ce029ea444c1.js} +1 -1
  5. package/out/_next/static/chunks/907-fc580cc28042182f.js +1 -0
  6. package/out/_next/static/chunks/pages/{_app-b73cf0344465689d.js → _app-9abe37b33f3ca7c2.js} +1 -1
  7. package/out/changelog.html +1 -1
  8. package/out/changes.html +1 -1
  9. package/out/custom-types/[customTypeId].html +1 -1
  10. package/out/custom-types.html +1 -1
  11. package/out/index.html +1 -1
  12. package/out/labs.html +1 -1
  13. package/out/page-types/[pageTypeId].html +1 -1
  14. package/out/slices/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  15. package/out/slices/[lib]/[sliceName]/[variation].html +1 -1
  16. package/out/slices.html +1 -1
  17. package/package.json +3 -3
  18. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/CreateSliceFromImageModal.tsx +405 -237
  19. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +23 -4
  20. package/src/icons/FigmaIcon.tsx +16 -6
  21. package/src/legacy/lib/builders/CustomTypeBuilder/SliceZone/index.tsx +1 -1
  22. package/test/src/modules/__fixtures__/serverState.ts +0 -1
  23. package/out/_next/static/chunks/20-4cb8941c8aafa019.js +0 -1
  24. package/out/_next/static/chunks/907-88dafe5c1e80dead.js +0 -1
  25. /package/out/_next/static/{j0_D1z-ZN75hJO-JvwC0X → J2YngfgTVf7j_xF1hSI57}/_ssgManifest.js +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  Box,
5
5
  Button,
6
6
  Dialog,
7
+ DialogActionButton,
7
8
  DialogActions,
8
9
  DialogCancelButton,
9
10
  DialogContent,
@@ -11,6 +12,7 @@ import {
11
12
  DialogHeader,
12
13
  FileDropZone,
13
14
  FileUploadButton,
15
+ ProgressCircle,
14
16
  ScrollArea,
15
17
  Text,
16
18
  } from "@prismicio/editor-ui";
@@ -24,6 +26,7 @@ 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";
27
30
  import { FigmaIcon } from "@/icons/FigmaIcon";
28
31
  import { managerClient } from "@/managerClient";
29
32
  import useSliceMachineActions from "@/modules/useSliceMachineActions";
@@ -41,31 +44,27 @@ const IMAGE_UPLOAD_LIMIT = 10;
41
44
  interface CreateSliceFromImageModalProps {
42
45
  open: boolean;
43
46
  location: "custom_type" | "page_type" | "slices";
44
- onSuccess: (args: {
45
- slices: {
46
- model: SharedSlice;
47
- langSmithUrl?: string;
48
- }[];
49
- library: string;
50
- }) => void;
47
+ onSuccess: (args: { slices: SharedSlice[]; library: string }) => void;
51
48
  onClose: () => void;
52
49
  }
53
50
 
54
51
  export function CreateSliceFromImageModal(
55
52
  props: CreateSliceFromImageModalProps,
56
53
  ) {
57
- const { open, location, onClose } = props;
54
+ const { open, location, onClose, onSuccess } = props;
58
55
  const [slices, setSlices] = useState<Slice[]>([]);
56
+ const [isSubmitting, setIsSubmitting] = useState(false);
59
57
  const { syncChanges } = useAutoSync();
60
58
  const { createSliceSuccess } = useSliceMachineActions();
61
59
  const { completeStep } = useOnboarding();
62
60
  const existingSlices = useExistingSlices({ open });
63
-
61
+ const { libraryID, isLoading: isLoadingLibraryID } = useLibraryID();
64
62
  /**
65
63
  * Keeps track of the current instance id.
66
64
  * When the modal is closed, the id is reset.
67
65
  */
68
66
  const id = useRef(crypto.randomUUID());
67
+ const isFigmaEnabled = useIsFigmaEnabled();
69
68
 
70
69
  useHotkeys(
71
70
  ["meta+v", "ctrl+v"],
@@ -73,15 +72,9 @@ export function CreateSliceFromImageModal(
73
72
  event.preventDefault();
74
73
  void handlePaste();
75
74
  },
76
- { enabled: open },
75
+ { enabled: open && isFigmaEnabled },
77
76
  );
78
77
 
79
- useEffect(() => {
80
- if (slices.every((slice) => slice.status === "success")) {
81
- void onAllComplete();
82
- }
83
- }, [slices]); // eslint-disable-line react-hooks/exhaustive-deps
84
-
85
78
  const setSlice = (args: {
86
79
  index: number;
87
80
  slice: (prevSlice: Slice) => Slice;
@@ -98,6 +91,8 @@ export function CreateSliceFromImageModal(
98
91
  };
99
92
 
100
93
  const onImagesSelected = (images: File[]) => {
94
+ if (hasTriggeredGeneration) return;
95
+
101
96
  if (images.length > IMAGE_UPLOAD_LIMIT) {
102
97
  toast.error(
103
98
  `You can only upload ${IMAGE_UPLOAD_LIMIT} images at a time.`,
@@ -105,22 +100,39 @@ export function CreateSliceFromImageModal(
105
100
  return;
106
101
  }
107
102
 
108
- setSlices(
109
- images.map((image) => ({
110
- status: "uploading",
111
- source: "upload",
112
- image,
113
- })),
114
- );
103
+ const startIndex = slices.length;
104
+ setSlices((prevSlices) => [
105
+ ...prevSlices,
106
+ ...images.map(
107
+ (image): Slice => ({
108
+ source: "upload",
109
+ status: "uploading",
110
+ image,
111
+ }),
112
+ ),
113
+ ]);
115
114
 
116
- images.forEach((imageData, index) => {
117
- void generateSlice({ index, imageData, source: "upload" });
115
+ images.forEach((imageData, relativeIndex) => {
116
+ const index = startIndex + relativeIndex;
117
+ void uploadImage({ index, imageData, source: "upload" });
118
118
  });
119
119
  };
120
120
 
121
121
  const handlePaste = async () => {
122
- // Limit just to one paste at a time for now
123
- if (!open || slices.length > 0) return;
122
+ if (
123
+ !open ||
124
+ !isFigmaEnabled ||
125
+ // For now we only support one Figma slice at a time
126
+ slices.some((slice) => slice.source === "figma")
127
+ ) {
128
+ return;
129
+ }
130
+
131
+ // Don't allow pasting while uploads or generation are in progress
132
+ const isLoading = slices.some(
133
+ (slice) => slice.status === "uploading" || slice.status === "generating",
134
+ );
135
+ if (isLoading) return;
124
136
 
125
137
  const supportsClipboardRead =
126
138
  typeof navigator.clipboard?.read === "function";
@@ -217,7 +229,7 @@ export function CreateSliceFromImageModal(
217
229
  ]);
218
230
 
219
231
  // Start uploading the new image
220
- void generateSlice({ index: newIndex, imageData, source: "figma" });
232
+ void uploadImage({ index: newIndex, imageData, source: "figma" });
221
233
 
222
234
  toast.success(`Pasted ${imageName}${success ? " from Figma" : ""}`);
223
235
  } catch (error) {
@@ -228,7 +240,7 @@ export function CreateSliceFromImageModal(
228
240
  }
229
241
  };
230
242
 
231
- const generateSlice = async (args: {
243
+ const uploadImage = async (args: {
232
244
  index: number;
233
245
  imageData: File;
234
246
  source: "figma" | "upload";
@@ -236,22 +248,28 @@ export function CreateSliceFromImageModal(
236
248
  const { index, imageData, source } = args;
237
249
  const currentId = id.current;
238
250
 
239
- const smConfig = await managerClient.project.getSliceMachineConfig();
240
- const libraryID = smConfig?.libraries?.[0];
241
- if (libraryID === undefined) {
242
- throw new Error("No library found in the config.");
243
- }
244
-
245
251
  setSlice({
246
252
  index,
247
- slice: (prevSlice) => ({ ...prevSlice, status: "uploading" }),
253
+ slice: (prevSlice) => ({
254
+ ...prevSlice,
255
+ status: "uploading",
256
+ image: imageData,
257
+ source,
258
+ }),
248
259
  });
249
260
 
250
261
  try {
251
262
  const imageUrl = await getImageUrl({ image: imageData });
252
263
  if (currentId !== id.current) return;
253
264
 
254
- void inferSlice({ index, imageUrl, libraryID, source });
265
+ setSlice({
266
+ index,
267
+ slice: (prevSlice) => ({
268
+ ...prevSlice,
269
+ status: "pending",
270
+ thumbnailUrl: imageUrl,
271
+ }),
272
+ });
255
273
  } catch {
256
274
  if (currentId !== id.current) return;
257
275
  setSlice({
@@ -259,12 +277,28 @@ export function CreateSliceFromImageModal(
259
277
  slice: (prevSlice) => ({
260
278
  ...prevSlice,
261
279
  status: "uploadError",
262
- onRetry: () => void generateSlice({ index, imageData, source }),
280
+ onRetry: () => void uploadImage({ index, imageData, source }),
263
281
  }),
264
282
  });
265
283
  }
266
284
  };
267
285
 
286
+ const generateAllPendingSlices = () => {
287
+ if (libraryID === undefined) return;
288
+
289
+ // Generate all pending slices simultaneously
290
+ slices.forEach((slice, index) => {
291
+ if (slice.status === "pending") {
292
+ void inferSlice({
293
+ index,
294
+ libraryID,
295
+ imageUrl: slice.thumbnailUrl,
296
+ source: slice.source,
297
+ });
298
+ }
299
+ });
300
+ };
301
+
268
302
  const inferSlice = async (args: {
269
303
  index: number;
270
304
  imageUrl: string;
@@ -272,7 +306,7 @@ export function CreateSliceFromImageModal(
272
306
  source: "figma" | "upload";
273
307
  }) => {
274
308
  const { index, imageUrl, libraryID, source } = args;
275
- const currentId = id.current;
309
+ let currentId = id.current;
276
310
 
277
311
  setSlice({
278
312
  index,
@@ -292,6 +326,13 @@ export function CreateSliceFromImageModal(
292
326
 
293
327
  if (currentId !== id.current) return;
294
328
 
329
+ const resolvedModel = sliceWithoutConflicts({
330
+ existingSlices: existingSlices.current,
331
+ newSlices: slices,
332
+ slice: inferResult.slice,
333
+ });
334
+
335
+ // Update slice state with success
295
336
  setSlices((prevSlices) => {
296
337
  return prevSlices.map((prevSlice, i) => {
297
338
  if (i !== index) return prevSlice;
@@ -299,16 +340,57 @@ export function CreateSliceFromImageModal(
299
340
  ...prevSlice,
300
341
  status: "success",
301
342
  thumbnailUrl: imageUrl,
302
- model: sliceWithoutConflicts({
303
- existingSlices: existingSlices.current,
304
- newSlices: slices,
305
- slice: inferResult.slice,
306
- }),
343
+ model: resolvedModel,
344
+ langSmithUrl: inferResult.langSmithUrl,
307
345
  };
308
346
  });
309
347
  });
348
+
349
+ if (source === "upload") {
350
+ currentId = id.current;
351
+ const currentSlice = slices[index];
352
+
353
+ const { errors } = await managerClient.slices.createSlice({
354
+ libraryID,
355
+ model: resolvedModel,
356
+ });
357
+ if (errors.length) {
358
+ throw new Error(`Failed to create slice ${resolvedModel.id}.`);
359
+ }
360
+
361
+ // Set the variation screenshot
362
+ await managerClient.slices.updateSliceScreenshot({
363
+ libraryID,
364
+ sliceID: resolvedModel.id,
365
+ variationID: resolvedModel.variations[0].id,
366
+ data: currentSlice.image,
367
+ });
368
+
369
+ if (currentId !== id.current) return;
370
+ }
371
+
372
+ void completeStep("createSlice");
373
+
374
+ void telemetry.track({
375
+ event: "slice:created",
376
+ id: resolvedModel.id,
377
+ name: resolvedModel.name,
378
+ library: libraryID,
379
+ location,
380
+ mode: "ai",
381
+ langSmithUrl: inferResult.langSmithUrl,
382
+ });
383
+
384
+ addAiFeedback({
385
+ type: "model",
386
+ library: libraryID,
387
+ sliceId: resolvedModel.id,
388
+ variationId: resolvedModel.variations[0].id,
389
+ langSmithUrl: inferResult.langSmithUrl,
390
+ });
310
391
  } catch {
311
392
  if (currentId !== id.current) return;
393
+
312
394
  setSlice({
313
395
  index,
314
396
  slice: (prevSlice) => ({
@@ -323,163 +405,230 @@ export function CreateSliceFromImageModal(
323
405
  }
324
406
  };
325
407
 
326
- const onAllComplete = async () => {
327
- const newSlices = slices.reduce<NewSlice[]>((acc, slice) => {
328
- if (slice.status === "success") {
329
- acc.push(slice);
330
- }
331
- return acc;
332
- }, []);
408
+ const resetState = () => {
409
+ id.current = crypto.randomUUID();
410
+ setSlices([]);
411
+ };
333
412
 
334
- if (!newSlices.length) return;
413
+ const handleClose = () => {
414
+ resetState();
415
+ onClose();
416
+ };
335
417
 
336
- const currentId = id.current;
418
+ const onSubmit = async () => {
337
419
  try {
338
- // Only the slices generated from uploaded images need this step
339
- const { slices, library } = await addSlices(
340
- newSlices.filter((slice) => slice.source === "upload"),
341
- );
342
- if (currentId !== id.current) return;
343
-
344
- id.current = crypto.randomUUID();
420
+ setIsSubmitting(true);
421
+ if (libraryID === undefined) return;
345
422
 
346
423
  const serverState = await getState();
347
424
  createSliceSuccess(serverState.libraries);
348
425
  syncChanges();
349
426
 
350
- void completeStep("createSlice");
351
-
352
- for (const { model, langSmithUrl } of slices) {
353
- void telemetry.track({
354
- event: "slice:created",
355
- id: model.id,
356
- name: model.name,
357
- library,
358
- location,
359
- mode: "ai",
360
- langSmithUrl,
361
- });
362
-
363
- addAiFeedback({
364
- type: "model",
365
- library,
366
- sliceId: model.id,
367
- variationId: model.variations[0].id,
368
- langSmithUrl,
369
- });
370
- }
427
+ onSuccess({
428
+ slices: slices.flatMap((slice) =>
429
+ slice.status === "success" ? slice.model : [],
430
+ ),
431
+ library: libraryID,
432
+ });
371
433
 
372
- toast.success(
373
- `${newSlices.length} new slice${
374
- newSlices.length > 1 ? "s" : ""
375
- } successfully generated.`,
376
- );
377
- } catch {
378
- if (currentId !== id.current) return;
379
- toast.error("An unexpected error happened while adding slices.");
434
+ resetState();
435
+ } finally {
436
+ setIsSubmitting(false);
380
437
  }
381
438
  };
382
439
 
383
- const areSlicesLoading = slices.some(
384
- (slice) => slice.status === "uploading" || slice.status === "generating",
385
- );
440
+ const loadingSliceCount = slices.filter((slice) => {
441
+ return slice.status === "uploading" || slice.status === "generating";
442
+ }).length;
443
+
444
+ const pendingSliceCount = slices.filter((slice) => {
445
+ return slice.status === "pending";
446
+ }).length;
447
+
448
+ const hasTriggeredGeneration = slices.some((slice) => {
449
+ return slice.status === "generating" || slice.status === "success";
450
+ });
451
+
452
+ const completedSliceCount = slices.filter((slice) => {
453
+ return slice.status === "success";
454
+ }).length;
386
455
 
456
+ const generateSliceCount = loadingSliceCount + pendingSliceCount;
457
+
458
+ console.log({
459
+ slices,
460
+ generateSliceCount,
461
+ loadingSliceCount,
462
+ pendingSliceCount,
463
+ });
387
464
  return (
388
- <Dialog open={open} onOpenChange={onOpenChange}>
389
- <DialogHeader title="Generate from image" />
465
+ <Dialog
466
+ open={open}
467
+ onOpenChange={loadingSliceCount > 0 ? undefined : onOpenChange}
468
+ >
469
+ <DialogHeader title="Generate with AI" />
390
470
  <DialogContent gap={0}>
391
471
  <DialogDescription hidden>
392
472
  Upload images to generate slices with AI
393
473
  </DialogDescription>
394
- {slices.length === 0 ? (
395
- <Box
396
- padding={16}
397
- height="100%"
398
- gap={16}
399
- display="flex"
400
- flexDirection="column"
401
- >
402
- <Box
403
- display="flex"
404
- gap={16}
405
- alignItems="center"
406
- backgroundColor="grey2"
407
- padding={16}
408
- borderRadius={12}
409
- >
410
- <Box display="flex" gap={8} alignItems="center" flexGrow={1}>
474
+ {!isLoadingLibraryID ? (
475
+ <>
476
+ {slices.length === 0 ? (
477
+ <Box
478
+ padding={16}
479
+ height="100%"
480
+ gap={16}
481
+ display="flex"
482
+ flexDirection="column"
483
+ >
484
+ {isFigmaEnabled && (
485
+ <Box
486
+ display="flex"
487
+ gap={16}
488
+ alignItems="center"
489
+ backgroundColor="grey2"
490
+ padding={16}
491
+ borderRadius={12}
492
+ >
493
+ <Box
494
+ display="flex"
495
+ gap={8}
496
+ alignItems="center"
497
+ flexGrow={1}
498
+ >
499
+ <Box
500
+ width={48}
501
+ height={48}
502
+ backgroundColor="grey12"
503
+ borderRadius="100%"
504
+ display="flex"
505
+ alignItems="center"
506
+ justifyContent="center"
507
+ >
508
+ <FigmaIcon variant="original" height={25} />
509
+ </Box>
510
+ <Box display="flex" flexDirection="column" flexGrow={1}>
511
+ <Text variant="bold">Want to work faster?</Text>
512
+ <Text variant="small" color="grey11">
513
+ Copy frames from Figma with the Slice Machine plugin
514
+ and paste them here.
515
+ </Text>
516
+ </Box>
517
+ </Box>
518
+ <Button
519
+ endIcon="arrowForward"
520
+ color="indigo"
521
+ onClick={() =>
522
+ window.open(
523
+ "https://www.figma.com/community/plugin/TODO",
524
+ "_blank",
525
+ )
526
+ }
527
+ sx={{ marginRight: 8 }}
528
+ invisible
529
+ >
530
+ Install plugin
531
+ </Button>
532
+ </Box>
533
+ )}
534
+ <FileDropZone
535
+ onFilesSelected={onImagesSelected}
536
+ assetType="image"
537
+ maxFiles={IMAGE_UPLOAD_LIMIT}
538
+ overlay={
539
+ <UploadBlankSlate
540
+ onFilesSelected={onImagesSelected}
541
+ onPaste={() => void handlePaste()}
542
+ droppingFiles
543
+ />
544
+ }
545
+ >
546
+ <UploadBlankSlate
547
+ onFilesSelected={onImagesSelected}
548
+ onPaste={() => void handlePaste()}
549
+ />
550
+ </FileDropZone>
551
+ </Box>
552
+ ) : (
553
+ <>
411
554
  <Box
412
- width={48}
413
- height={48}
414
- backgroundColor="grey12"
415
- borderRadius="100%"
416
555
  display="flex"
417
556
  alignItems="center"
418
- justifyContent="center"
557
+ justifyContent="space-between"
558
+ padding={16}
419
559
  >
420
- <FigmaIcon variant="original" height={25} />
560
+ <Text variant="h3">Design</Text>
561
+ <FileUploadButton
562
+ size="medium"
563
+ color="grey"
564
+ onFilesSelected={onImagesSelected}
565
+ startIcon="attachFile"
566
+ disabled={hasTriggeredGeneration}
567
+ >
568
+ Add images
569
+ </FileUploadButton>
421
570
  </Box>
422
- <Box display="flex" flexDirection="column" flexGrow={1}>
423
- <Text variant="bold">Want to work faster?</Text>
424
- <Text variant="small" color="grey11">
425
- Copy frames from Figma with the Slice Machine plugin and
426
- paste them here.
427
- </Text>
428
- </Box>
429
- </Box>
430
- <Button
431
- endIcon="arrowForward"
432
- color="indigo"
433
- onClick={() =>
434
- window.open(
435
- "https://www.figma.com/community/plugin/TODO",
436
- "_blank",
437
- )
438
- }
439
- sx={{ marginRight: 8 }}
440
- invisible
571
+ <ScrollArea stableScrollbar={false}>
572
+ <Box
573
+ display="grid"
574
+ gridTemplateColumns="1fr 1fr"
575
+ gap={16}
576
+ padding={16}
577
+ >
578
+ {slices.map((slice, index) => (
579
+ <SliceCard slice={slice} key={`slice-${index}`} />
580
+ ))}
581
+ </Box>
582
+ </ScrollArea>
583
+ </>
584
+ )}
585
+ <DialogActions>
586
+ <DialogCancelButton
587
+ size="medium"
588
+ onClick={handleClose}
589
+ disabled={loadingSliceCount > 0}
441
590
  >
442
- Install plugin
443
- </Button>
444
- </Box>
445
- <FileDropZone
446
- onFilesSelected={onImagesSelected}
447
- assetType="image"
448
- maxFiles={IMAGE_UPLOAD_LIMIT}
449
- overlay={
450
- <UploadBlankSlate
451
- onFilesSelected={onImagesSelected}
452
- onPaste={() => void handlePaste()}
453
- droppingFiles
454
- />
455
- }
456
- >
457
- <UploadBlankSlate
458
- onFilesSelected={onImagesSelected}
459
- onPaste={() => void handlePaste()}
460
- />
461
- </FileDropZone>
462
- </Box>
591
+ Close
592
+ </DialogCancelButton>
593
+ {completedSliceCount === 0 ? (
594
+ <DialogActionButton
595
+ color="purple"
596
+ startIcon="autoFixHigh"
597
+ onClick={() => void generateAllPendingSlices()}
598
+ disabled={
599
+ hasTriggeredGeneration ||
600
+ loadingSliceCount > 0 ||
601
+ pendingSliceCount === 0
602
+ }
603
+ loading={loadingSliceCount > 0}
604
+ size="medium"
605
+ >
606
+ Generate{" "}
607
+ {generateSliceCount > 0 ? `(${generateSliceCount}) ` : ""}
608
+ {generateSliceCount === 1 ? "Slice" : "Slices"}
609
+ </DialogActionButton>
610
+ ) : (
611
+ <DialogActionButton
612
+ color="purple"
613
+ onClick={() => void onSubmit()}
614
+ loading={isSubmitting}
615
+ size="medium"
616
+ >
617
+ {getSubmitButtonLabel(location, completedSliceCount)}
618
+ </DialogActionButton>
619
+ )}
620
+ </DialogActions>
621
+ </>
463
622
  ) : (
464
- <ScrollArea stableScrollbar={false}>
465
- <Box
466
- display="grid"
467
- gridTemplateColumns="1fr 1fr"
468
- gap={16}
469
- padding={16}
470
- >
471
- {slices.map((slice, index) => (
472
- <SliceCard slice={slice} key={`slice-${index}`} />
473
- ))}
474
- </Box>
475
- </ScrollArea>
623
+ <Box
624
+ display="flex"
625
+ justifyContent="center"
626
+ alignItems="center"
627
+ height="100%"
628
+ >
629
+ <ProgressCircle color="purple9" />
630
+ </Box>
476
631
  )}
477
-
478
- <DialogActions>
479
- <DialogCancelButton disabled={areSlicesLoading}>
480
- Close
481
- </DialogCancelButton>
482
- </DialogActions>
483
632
  </DialogContent>
484
633
  </Dialog>
485
634
  );
@@ -491,6 +640,7 @@ function UploadBlankSlate(props: {
491
640
  onPaste: () => void;
492
641
  }) {
493
642
  const { droppingFiles = false, onFilesSelected, onPaste } = props;
643
+ const isFigmaEnabled = useIsFigmaEnabled();
494
644
 
495
645
  return (
496
646
  <Box
@@ -518,30 +668,53 @@ function UploadBlankSlate(props: {
518
668
  gap={4}
519
669
  alignItems="center"
520
670
  >
521
- <Text>Generate slices from your designs</Text>
522
- <Text variant="small" color="grey11">
523
- Upload your design images or paste them directly from Figma.
524
- </Text>
671
+ {isFigmaEnabled ? (
672
+ <>
673
+ <Text>Generate slices from your designs</Text>
674
+ <Text variant="small" color="grey11">
675
+ Upload your design images or paste them directly from Figma.
676
+ </Text>
677
+ </>
678
+ ) : (
679
+ <>
680
+ <Text>Upload your design images.</Text>
681
+ <Text variant="small" color="grey11">
682
+ Once uploaded, you can generate slices automatically using AI.
683
+ </Text>
684
+ </>
685
+ )}
525
686
  </Box>
526
687
  <Box display="flex" alignItems="center" gap={16}>
527
- <Button
528
- size="small"
529
- renderStartIcon={() => (
530
- <FigmaIcon variant="original" height={16} />
531
- )}
532
- color="grey"
533
- onClick={onPaste}
534
- >
535
- Paste from Figma
536
- </Button>
537
- <FileUploadButton
538
- size="small"
539
- onFilesSelected={onFilesSelected}
540
- color="purple"
541
- invisible
542
- >
543
- Add images
544
- </FileUploadButton>
688
+ {isFigmaEnabled ? (
689
+ <>
690
+ <Button
691
+ size="small"
692
+ renderStartIcon={() => (
693
+ <FigmaIcon variant="original" height={16} />
694
+ )}
695
+ color="grey"
696
+ onClick={onPaste}
697
+ >
698
+ Paste from Figma
699
+ </Button>
700
+ <FileUploadButton
701
+ size="small"
702
+ onFilesSelected={onFilesSelected}
703
+ color="purple"
704
+ invisible
705
+ >
706
+ Add images
707
+ </FileUploadButton>
708
+ </>
709
+ ) : (
710
+ <FileUploadButton
711
+ startIcon="attachFile"
712
+ onFilesSelected={onFilesSelected}
713
+ color="grey"
714
+ >
715
+ Add images
716
+ </FileUploadButton>
717
+ )}
545
718
  </Box>
546
719
  </Box>
547
720
  </BlankSlate>
@@ -566,13 +739,6 @@ async function getImageUrl({ image }: { image: File }) {
566
739
  return url;
567
740
  }
568
741
 
569
- type NewSlice = {
570
- image: File;
571
- model: SharedSlice;
572
- langSmithUrl?: string;
573
- source: "figma" | "upload";
574
- };
575
-
576
742
  /**
577
743
  * Keeps track of the existing slices in the project.
578
744
  * Re-fetches them when the modal is opened.
@@ -644,40 +810,42 @@ function sliceWithoutConflicts({
644
810
  };
645
811
  }
646
812
 
647
- async function addSlices(newSlices: NewSlice[]) {
648
- // use the first library
649
- const { libraries = [] } =
650
- await managerClient.project.getSliceMachineConfig();
651
- const library = libraries[0];
652
- if (!library) {
653
- throw new Error("No library found in the config.");
654
- }
655
-
656
- for (const { model } of newSlices) {
657
- const { errors } = await managerClient.slices.createSlice({
658
- libraryID: library,
659
- model,
660
- });
661
- if (errors.length) {
662
- throw new Error(`Failed to create slice ${model.id}.`);
663
- }
664
- }
813
+ function useLibraryID() {
814
+ const [libraryID, setLibraryID] = useState<string | undefined>();
665
815
 
666
- // for each added slice, set the variation screenshot
667
- const slices = await Promise.all(
668
- newSlices.map(async ({ model, image, langSmithUrl }) => {
669
- await managerClient.slices.updateSliceScreenshot({
670
- libraryID: library,
671
- sliceID: model.id,
672
- variationID: model.variations[0].id,
673
- data: image,
816
+ useEffect(() => {
817
+ managerClient.project
818
+ .getSliceMachineConfig()
819
+ .then((smConfig) => {
820
+ const libraryID = smConfig?.libraries?.[0];
821
+ if (libraryID === undefined) {
822
+ throw new Error("No library found in the config.");
823
+ }
824
+ setLibraryID(libraryID);
825
+ })
826
+ .catch(() => {
827
+ throw new Error("Could not get library ID from the config.");
674
828
  });
675
- return {
676
- model,
677
- langSmithUrl,
678
- };
679
- }),
680
- );
829
+ }, []);
830
+
831
+ return { libraryID, isLoading: libraryID === undefined };
832
+ }
681
833
 
682
- return { library, slices };
834
+ function useIsFigmaEnabled() {
835
+ const experiment = useExperimentVariant("llm-proxy-access");
836
+ return experiment?.value === "on";
683
837
  }
838
+
839
+ const getSubmitButtonLabel = (
840
+ location: "custom_type" | "page_type" | "slices",
841
+ completedSliceCount: number,
842
+ ) => {
843
+ switch (location) {
844
+ case "custom_type":
845
+ return `Add to type (${completedSliceCount})`;
846
+ case "page_type":
847
+ return `Add to page (${completedSliceCount})`;
848
+ case "slices":
849
+ return "Done";
850
+ }
851
+ };