slice-machine-ui 2.19.2-beta.2 → 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.
Files changed (34) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{489-ce3053e1d81ade83.js → 489-32281540712d98bb.js} +1 -1
  3. package/out/_next/static/chunks/593-7ffd1197c3405ef8.js +1 -0
  4. package/out/_next/static/chunks/633-275b9968b5aaa920.js +1 -0
  5. package/out/_next/static/chunks/882-48d61b2fabee28d8.js +1 -0
  6. package/out/_next/static/chunks/pages/{_app-83c4f5b209504941.js → _app-9cc2da8ff60c3087.js} +1 -1
  7. package/out/_next/static/chunks/pages/{changes-7f24b37f5bf872ae.js → changes-e66094f57453cf9c.js} +1 -1
  8. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-1b47424a37b49dff.js → [customTypeId]-6d613b67e6967ae5.js} +1 -1
  9. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-385f933c203e8b16.js → [pageTypeId]-40207b66190e3fcd.js} +1 -1
  10. package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-08cdeefc96106c0c.js → [variation]-22ffa2561ac66557.js} +1 -1
  11. package/out/_next/static/chunks/pages/{slices-bedcb854fbdca8cd.js → slices-a9b4d6d022cfcd88.js} +1 -1
  12. package/out/_next/static/uC4K1_hgf7dK4FL02cbUH/_buildManifest.js +1 -0
  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 +428 -240
  25. package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +49 -12
  26. package/src/icons/FigmaIcon.tsx +33 -34
  27. package/src/icons/FigmaIconSquare.tsx +45 -0
  28. package/src/legacy/components/ScreenshotChangesModal/index.tsx +2 -2
  29. package/src/legacy/lib/builders/CustomTypeBuilder/SliceZone/index.tsx +1 -1
  30. package/out/_next/static/4ZbwQOH1s2JwSCJTqy_83/_buildManifest.js +0 -1
  31. package/out/_next/static/chunks/34-28725deef8b874b1.js +0 -1
  32. package/out/_next/static/chunks/658-8231c0b729e0124a.js +0 -1
  33. package/out/_next/static/chunks/907-bf4215e6fc238ea0.js +0 -1
  34. /package/out/_next/static/{4ZbwQOH1s2JwSCJTqy_83 → 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,7 +12,9 @@ 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";
@@ -27,6 +27,7 @@ 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";
30
31
  import { managerClient } from "@/managerClient";
31
32
  import useSliceMachineActions from "@/modules/useSliceMachineActions";
32
33
 
@@ -37,13 +38,7 @@ const IMAGE_UPLOAD_LIMIT = 10;
37
38
  interface CreateSliceFromImageModalProps {
38
39
  open: boolean;
39
40
  location: "custom_type" | "page_type" | "slices";
40
- onSuccess: (args: {
41
- slices: {
42
- model: SharedSlice;
43
- langSmithUrl?: string;
44
- }[];
45
- library: string;
46
- }) => void;
41
+ onSuccess: (args: { slices: SharedSlice[]; library: string }) => void;
47
42
  onClose: () => void;
48
43
  }
49
44
 
@@ -58,11 +53,14 @@ export function CreateSliceFromImageModal(
58
53
  ) {
59
54
  const { open, location, onSuccess, onClose } = props;
60
55
  const [slices, setSlices] = useState<Slice[]>([]);
61
- const [isCreatingSlices, setIsCreatingSlices] = useState(false);
56
+ const [isSubmitting, setIsSubmitting] = useState(false);
57
+
62
58
  const { syncChanges } = useAutoSync();
63
59
  const { createSliceSuccess } = useSliceMachineActions();
64
60
  const { completeStep } = useOnboarding();
61
+ const existingSlices = useExistingSlices({ open });
65
62
  const isFigmaEnabled = useIsFigmaEnabled();
63
+ const { libraryID, isLoading: isLoadingLibraryID } = useLibraryID();
66
64
 
67
65
  /**
68
66
  * Keeps track of the current instance id.
@@ -87,14 +85,9 @@ export function CreateSliceFromImageModal(
87
85
  setSlices((slices) => slices.map((s, i) => (i === index ? slice(s) : s)));
88
86
  };
89
87
 
90
- const onOpenChange = (open: boolean) => {
91
- if (open || isCreatingSlices) return;
92
- onClose();
93
- id.current = crypto.randomUUID();
94
- setSlices([]);
95
- };
96
-
97
88
  const onImagesSelected = (images: File[]) => {
89
+ if (hasTriggeredGeneration) return;
90
+
98
91
  if (images.length > IMAGE_UPLOAD_LIMIT) {
99
92
  toast.error(
100
93
  `You can only upload ${IMAGE_UPLOAD_LIMIT} images at a time.`,
@@ -102,23 +95,28 @@ export function CreateSliceFromImageModal(
102
95
  return;
103
96
  }
104
97
 
105
- setSlices(
106
- images.map((image) => ({
107
- source: "upload",
108
- status: "uploading",
109
- image,
110
- })),
111
- );
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
+ ]);
112
109
 
113
- images.forEach((image, index) =>
114
- uploadImage({ index, image, source: "upload" }),
115
- );
110
+ images.forEach((image, relativeIndex) => {
111
+ const index = startIndex + relativeIndex;
112
+ void uploadImage({ index, image, source: "upload" });
113
+ });
116
114
  };
117
115
 
118
- const uploadImage = (args: {
116
+ const uploadImage = async (args: {
119
117
  index: number;
120
118
  image: File;
121
- source: "upload" | "figma";
119
+ source: "figma" | "upload";
122
120
  }) => {
123
121
  const { index, image, source } = args;
124
122
  const currentId = id.current;
@@ -128,40 +126,45 @@ export function CreateSliceFromImageModal(
128
126
  slice: (prevSlice) => ({
129
127
  ...prevSlice,
130
128
  status: "uploading",
129
+ image,
131
130
  source,
132
131
  }),
133
132
  });
134
133
 
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
- };
134
+ try {
135
+ const imageUrl = await getImageUrl({ image });
136
+ if (currentId !== id.current) return;
153
137
 
154
- const existingSlices = useExistingSlices({ open });
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
+ }
157
+ };
155
158
 
156
159
  const inferSlice = async (args: {
157
160
  index: number;
158
161
  imageUrl: string;
159
- source: "upload" | "figma";
162
+ source: "figma" | "upload";
160
163
  }) => {
161
- const { index, imageUrl, source } = args;
162
- const currentId = id.current;
164
+ if (libraryID === undefined) return;
163
165
 
164
- const libraryID = await getLibraryID();
166
+ const { index, imageUrl, source } = args;
167
+ let currentId = id.current;
165
168
 
166
169
  setSlice({
167
170
  index,
@@ -174,10 +177,11 @@ export function CreateSliceFromImageModal(
174
177
 
175
178
  try {
176
179
  const inferResult = await managerClient.customTypes.inferSlice({
177
- imageUrl,
178
180
  source,
179
181
  libraryID,
182
+ imageUrl,
180
183
  });
184
+
181
185
  if (currentId !== id.current) return;
182
186
 
183
187
  const model = sliceWithoutConflicts({
@@ -186,85 +190,143 @@ export function CreateSliceFromImageModal(
186
190
  slice: inferResult.slice,
187
191
  });
188
192
 
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 {
193
+ setSlices((prevSlices) => {
194
+ return prevSlices.map((prevSlice, i) => {
195
+ if (i !== index) return prevSlice;
196
+ return {
197
+ ...prevSlice,
198
+ status: "success",
199
+ thumbnailUrl: imageUrl,
200
+ model,
201
+ langSmithUrl: inferResult.langSmithUrl,
202
+ };
203
+ });
204
+ });
205
+
206
+ if (source === "upload") {
207
+ currentId = id.current;
208
+ const currentSlice = slices[index];
209
+
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}.`);
216
+ }
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
+ if (currentId !== id.current) return;
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,
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) {
203
248
  if (currentId !== id.current) return;
249
+
204
250
  setSlice({
205
251
  index,
206
252
  slice: (prevSlice) => ({
207
253
  ...prevSlice,
208
254
  status: "generateError",
209
255
  thumbnailUrl: imageUrl,
210
- onRetry: () => void inferSlice({ index, imageUrl, source }),
256
+ onRetry: () => {
257
+ void inferSlice({ index, imageUrl, source });
258
+ },
211
259
  }),
212
260
  });
213
261
  }
214
262
  };
215
263
 
216
- const onSubmit = () => {
217
- const newSlices = slices.reduce<NewSlice[]>((acc, slice) => {
218
- if (slice.status === "success" && slice.source === "upload") {
219
- acc.push(slice);
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
+ });
220
274
  }
221
- return acc;
222
- }, []);
223
- if (!newSlices.length) return;
275
+ });
276
+ };
224
277
 
225
- const currentId = id.current;
226
- setIsCreatingSlices(true);
227
- addSlices(newSlices)
228
- .then(async ({ slices, library }) => {
229
- if (currentId !== id.current) return;
278
+ const generatingSliceCount = slices.filter((slice) => {
279
+ return slice.status === "generating";
280
+ }).length;
230
281
 
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.");
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,
267
324
  });
325
+
326
+ closeModal();
327
+ } finally {
328
+ setIsSubmitting(false);
329
+ }
268
330
  };
269
331
 
270
332
  const handlePaste = async () => {
@@ -362,7 +424,7 @@ export function CreateSliceFromImageModal(
362
424
  }
363
425
 
364
426
  // Create File object from blob and append to existing slices
365
- const imageData = new File([imageBlob], imageName, {
427
+ const image = new File([imageBlob], imageName, {
366
428
  type: imageBlob.type,
367
429
  });
368
430
  const newIndex = currentSliceCount;
@@ -373,12 +435,12 @@ export function CreateSliceFromImageModal(
373
435
  {
374
436
  source: "figma",
375
437
  status: "uploading",
376
- image: imageData,
438
+ image,
377
439
  },
378
440
  ]);
379
441
 
380
442
  // Start uploading the new image
381
- void uploadImage({ index: newIndex, image: imageData, source: "figma" });
443
+ void uploadImage({ index: newIndex, image, source: "figma" });
382
444
 
383
445
  toast.success(`Pasted ${imageName}${success ? " from Figma" : ""}`);
384
446
  } catch (error) {
@@ -389,60 +451,173 @@ export function CreateSliceFromImageModal(
389
451
  }
390
452
  };
391
453
 
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;
397
-
398
454
  return (
399
- <Dialog open={open} onOpenChange={onOpenChange}>
400
- <DialogHeader title="Generate from image" />
455
+ <Dialog open={open} onOpenChange={(open) => !open && closeModal()}>
456
+ <DialogHeader title="Generate with AI" />
401
457
  <DialogContent gap={0}>
402
458
  <DialogDescription hidden>
403
459
  Upload images to generate slices with AI
404
460
  </DialogDescription>
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
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
413
522
  onFilesSelected={onImagesSelected}
414
- droppingFiles
415
- />
416
- }
417
- >
418
- <UploadBlankSlate onFilesSelected={onImagesSelected} />
419
- </FileDropZone>
420
- </Box>
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
+ </>
421
611
  ) : (
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>
434
- )}
435
-
436
- <DialogActions>
437
- <DialogCancelButton disabled={isCreatingSlices} />
438
- <DialogActionButton
439
- disabled={!someSlicesReady || areSlicesLoading}
440
- loading={isCreatingSlices}
441
- onClick={onSubmit}
612
+ <Box
613
+ display="flex"
614
+ justifyContent="center"
615
+ alignItems="center"
616
+ height="100%"
442
617
  >
443
- {getSubmitButtonLabel(location)} ({readySlices.length})
444
- </DialogActionButton>
445
- </DialogActions>
618
+ <ProgressCircle color="purple9" />
619
+ </Box>
620
+ )}
446
621
  </DialogContent>
447
622
  </Dialog>
448
623
  );
@@ -451,8 +626,10 @@ export function CreateSliceFromImageModal(
451
626
  function UploadBlankSlate(props: {
452
627
  droppingFiles?: boolean;
453
628
  onFilesSelected: (files: File[]) => void;
629
+ onPaste: () => void;
454
630
  }) {
455
- const { droppingFiles = false, onFilesSelected } = props;
631
+ const { droppingFiles = false, onFilesSelected, onPaste } = props;
632
+ const isFigmaEnabled = useIsFigmaEnabled();
456
633
 
457
634
  return (
458
635
  <Box
@@ -463,27 +640,70 @@ function UploadBlankSlate(props: {
463
640
  border
464
641
  borderStyle="dashed"
465
642
  borderColor={droppingFiles ? "purple9" : "grey6"}
643
+ borderRadius={12}
644
+ flexGrow={1}
466
645
  >
467
646
  <BlankSlate>
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"
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"
483
659
  >
484
- Add images
485
- </FileUploadButton>
486
- </BlankSlateActions>
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>
487
707
  </BlankSlate>
488
708
  </Box>
489
709
  );
@@ -506,12 +726,6 @@ async function getImageUrl({ image }: { image: File }) {
506
726
  return url;
507
727
  }
508
728
 
509
- type NewSlice = {
510
- image: File;
511
- model: SharedSlice;
512
- langSmithUrl?: string;
513
- };
514
-
515
729
  /**
516
730
  * Keeps track of the existing slices in the project.
517
731
  * Re-fetches them when the modal is opened.
@@ -583,54 +797,17 @@ function sliceWithoutConflicts({
583
797
  };
584
798
  }
585
799
 
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
- }
594
-
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
- }
604
-
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
- );
620
-
621
- return { library, slices };
622
- }
623
-
624
800
  const getSubmitButtonLabel = (
625
801
  location: "custom_type" | "page_type" | "slices",
802
+ completedSliceCount: number,
626
803
  ) => {
627
804
  switch (location) {
628
805
  case "custom_type":
629
- return "Add to type";
806
+ return `Add to type (${completedSliceCount})`;
630
807
  case "page_type":
631
- return "Add to page";
808
+ return `Add to page (${completedSliceCount})`;
632
809
  case "slices":
633
- return "Add to slices";
810
+ return "Done";
634
811
  }
635
812
  };
636
813
 
@@ -639,12 +816,23 @@ function useIsFigmaEnabled() {
639
816
  return experiment?.value === "on";
640
817
  }
641
818
 
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
- });
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 };
650
838
  }