slice-machine-ui 2.19.2-beta.2 → 2.19.2-beta.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.
- package/out/404.html +1 -1
- package/out/_next/static/LhgS5bFIDwBpgBpbMYvj5/_buildManifest.js +1 -0
- package/out/_next/static/chunks/{489-ce3053e1d81ade83.js → 489-32281540712d98bb.js} +1 -1
- package/out/_next/static/chunks/593-7ffd1197c3405ef8.js +1 -0
- package/out/_next/static/chunks/633-6ef64d197d323bf7.js +1 -0
- package/out/_next/static/chunks/882-48d61b2fabee28d8.js +1 -0
- package/out/_next/static/chunks/pages/{_app-83c4f5b209504941.js → _app-0f3a7a06eafa1943.js} +1 -1
- package/out/_next/static/chunks/pages/{changes-7f24b37f5bf872ae.js → changes-e66094f57453cf9c.js} +1 -1
- package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-1b47424a37b49dff.js → [customTypeId]-6d613b67e6967ae5.js} +1 -1
- package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-385f933c203e8b16.js → [pageTypeId]-40207b66190e3fcd.js} +1 -1
- package/out/_next/static/chunks/pages/slices/[lib]/[sliceName]/{[variation]-08cdeefc96106c0c.js → [variation]-22ffa2561ac66557.js} +1 -1
- package/out/_next/static/chunks/pages/{slices-bedcb854fbdca8cd.js → slices-a9b4d6d022cfcd88.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 +525 -241
- package/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/SliceCard.tsx +59 -15
- package/src/icons/FigmaIcon.tsx +33 -34
- package/src/icons/FigmaIconSquare.tsx +45 -0
- package/src/legacy/components/ScreenshotChangesModal/index.tsx +2 -2
- package/src/legacy/lib/builders/CustomTypeBuilder/SliceZone/index.tsx +1 -1
- package/out/_next/static/4ZbwQOH1s2JwSCJTqy_83/_buildManifest.js +0 -1
- package/out/_next/static/chunks/34-28725deef8b874b1.js +0 -1
- package/out/_next/static/chunks/658-8231c0b729e0124a.js +0 -1
- package/out/_next/static/chunks/907-bf4215e6fc238ea0.js +0 -1
- /package/out/_next/static/{4ZbwQOH1s2JwSCJTqy_83 → LhgS5bFIDwBpgBpbMYvj5}/_ssgManifest.js +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { useStableCallback } from "@prismicio/editor-support/React";
|
|
1
2
|
import {
|
|
2
3
|
BlankSlate,
|
|
3
|
-
BlankSlateActions,
|
|
4
|
-
BlankSlateDescription,
|
|
5
4
|
BlankSlateIcon,
|
|
6
|
-
BlankSlateTitle,
|
|
7
5
|
Box,
|
|
6
|
+
Button,
|
|
8
7
|
Dialog,
|
|
9
8
|
DialogActionButton,
|
|
10
9
|
DialogActions,
|
|
@@ -14,9 +13,12 @@ import {
|
|
|
14
13
|
DialogHeader,
|
|
15
14
|
FileDropZone,
|
|
16
15
|
FileUploadButton,
|
|
16
|
+
ProgressCircle,
|
|
17
17
|
ScrollArea,
|
|
18
|
+
Text,
|
|
18
19
|
} from "@prismicio/editor-ui";
|
|
19
20
|
import { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
|
|
21
|
+
import { useRouter } from "next/router";
|
|
20
22
|
import { useEffect, useRef, useState } from "react";
|
|
21
23
|
import { useHotkeys } from "react-hotkeys-hook";
|
|
22
24
|
import { toast } from "react-toastify";
|
|
@@ -27,6 +29,7 @@ import { addAiFeedback } from "@/features/aiFeedback";
|
|
|
27
29
|
import { useOnboarding } from "@/features/onboarding/useOnboarding";
|
|
28
30
|
import { useAutoSync } from "@/features/sync/AutoSyncProvider";
|
|
29
31
|
import { useExperimentVariant } from "@/hooks/useExperimentVariant";
|
|
32
|
+
import { FigmaIcon } from "@/icons/FigmaIcon";
|
|
30
33
|
import { managerClient } from "@/managerClient";
|
|
31
34
|
import useSliceMachineActions from "@/modules/useSliceMachineActions";
|
|
32
35
|
|
|
@@ -37,13 +40,7 @@ const IMAGE_UPLOAD_LIMIT = 10;
|
|
|
37
40
|
interface CreateSliceFromImageModalProps {
|
|
38
41
|
open: boolean;
|
|
39
42
|
location: "custom_type" | "page_type" | "slices";
|
|
40
|
-
onSuccess: (args: {
|
|
41
|
-
slices: {
|
|
42
|
-
model: SharedSlice;
|
|
43
|
-
langSmithUrl?: string;
|
|
44
|
-
}[];
|
|
45
|
-
library: string;
|
|
46
|
-
}) => void;
|
|
43
|
+
onSuccess: (args: { slices: SharedSlice[]; library: string }) => void;
|
|
47
44
|
onClose: () => void;
|
|
48
45
|
}
|
|
49
46
|
|
|
@@ -57,12 +54,21 @@ export function CreateSliceFromImageModal(
|
|
|
57
54
|
props: CreateSliceFromImageModalProps,
|
|
58
55
|
) {
|
|
59
56
|
const { open, location, onSuccess, onClose } = props;
|
|
57
|
+
const router = useRouter();
|
|
58
|
+
|
|
60
59
|
const [slices, setSlices] = useState<Slice[]>([]);
|
|
61
|
-
const [
|
|
60
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
61
|
+
const [showCancelConfirmation, setShowCancelConfirmation] = useState(false);
|
|
62
|
+
|
|
62
63
|
const { syncChanges } = useAutoSync();
|
|
63
64
|
const { createSliceSuccess } = useSliceMachineActions();
|
|
64
65
|
const { completeStep } = useOnboarding();
|
|
66
|
+
const existingSlices = useExistingSlices({ open });
|
|
65
67
|
const isFigmaEnabled = useIsFigmaEnabled();
|
|
68
|
+
const { libraryID, isLoading: isLoadingLibraryID } = useLibraryID();
|
|
69
|
+
const stableCancelGeneratingRequests = useStableCallback(
|
|
70
|
+
cancelGeneratingRequests,
|
|
71
|
+
);
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
74
|
* Keeps track of the current instance id.
|
|
@@ -74,11 +80,29 @@ export function CreateSliceFromImageModal(
|
|
|
74
80
|
["meta+v", "ctrl+v"],
|
|
75
81
|
(event) => {
|
|
76
82
|
event.preventDefault();
|
|
77
|
-
void
|
|
83
|
+
void onPaste();
|
|
78
84
|
},
|
|
79
85
|
{ enabled: open && isFigmaEnabled },
|
|
80
86
|
);
|
|
81
87
|
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!slices.some((slice) => slice.status === "generating")) return;
|
|
90
|
+
|
|
91
|
+
const onBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
92
|
+
stableCancelGeneratingRequests();
|
|
93
|
+
const message = "Your current generating slices will be cancelled.";
|
|
94
|
+
event.returnValue = message;
|
|
95
|
+
return message;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
router.events.on("routeChangeStart", stableCancelGeneratingRequests);
|
|
99
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
100
|
+
return () => {
|
|
101
|
+
router.events.off("routeChangeStart", stableCancelGeneratingRequests);
|
|
102
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
103
|
+
};
|
|
104
|
+
}, [slices, router.events, stableCancelGeneratingRequests]);
|
|
105
|
+
|
|
82
106
|
const setSlice = (args: {
|
|
83
107
|
index: number;
|
|
84
108
|
slice: (prevSlice: Slice) => Slice;
|
|
@@ -87,14 +111,9 @@ export function CreateSliceFromImageModal(
|
|
|
87
111
|
setSlices((slices) => slices.map((s, i) => (i === index ? slice(s) : s)));
|
|
88
112
|
};
|
|
89
113
|
|
|
90
|
-
const onOpenChange = (open: boolean) => {
|
|
91
|
-
if (open || isCreatingSlices) return;
|
|
92
|
-
onClose();
|
|
93
|
-
id.current = crypto.randomUUID();
|
|
94
|
-
setSlices([]);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
114
|
const onImagesSelected = (images: File[]) => {
|
|
115
|
+
if (hasTriggeredGeneration) return;
|
|
116
|
+
|
|
98
117
|
if (images.length > IMAGE_UPLOAD_LIMIT) {
|
|
99
118
|
toast.error(
|
|
100
119
|
`You can only upload ${IMAGE_UPLOAD_LIMIT} images at a time.`,
|
|
@@ -102,23 +121,28 @@ export function CreateSliceFromImageModal(
|
|
|
102
121
|
return;
|
|
103
122
|
}
|
|
104
123
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
image
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
const startIndex = slices.length;
|
|
125
|
+
setSlices((prevSlices) => [
|
|
126
|
+
...prevSlices,
|
|
127
|
+
...images.map(
|
|
128
|
+
(image): Slice => ({
|
|
129
|
+
source: "upload",
|
|
130
|
+
status: "uploading",
|
|
131
|
+
image,
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
]);
|
|
112
135
|
|
|
113
|
-
images.forEach((image,
|
|
114
|
-
|
|
115
|
-
|
|
136
|
+
images.forEach((image, relativeIndex) => {
|
|
137
|
+
const index = startIndex + relativeIndex;
|
|
138
|
+
void uploadImage({ index, image, source: "upload" });
|
|
139
|
+
});
|
|
116
140
|
};
|
|
117
141
|
|
|
118
|
-
const uploadImage = (args: {
|
|
142
|
+
const uploadImage = async (args: {
|
|
119
143
|
index: number;
|
|
120
144
|
image: File;
|
|
121
|
-
source: "
|
|
145
|
+
source: "figma" | "upload";
|
|
122
146
|
}) => {
|
|
123
147
|
const { index, image, source } = args;
|
|
124
148
|
const currentId = id.current;
|
|
@@ -128,40 +152,47 @@ export function CreateSliceFromImageModal(
|
|
|
128
152
|
slice: (prevSlice) => ({
|
|
129
153
|
...prevSlice,
|
|
130
154
|
status: "uploading",
|
|
155
|
+
image,
|
|
131
156
|
source,
|
|
132
157
|
}),
|
|
133
158
|
});
|
|
134
159
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
};
|
|
160
|
+
try {
|
|
161
|
+
const imageUrl = await getImageUrl({ image });
|
|
162
|
+
if (currentId !== id.current) return;
|
|
153
163
|
|
|
154
|
-
|
|
164
|
+
setSlice({
|
|
165
|
+
index,
|
|
166
|
+
slice: (prevSlice) => ({
|
|
167
|
+
...prevSlice,
|
|
168
|
+
status: "pending",
|
|
169
|
+
thumbnailUrl: imageUrl,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
} catch {
|
|
173
|
+
if (currentId !== id.current) return;
|
|
174
|
+
setSlice({
|
|
175
|
+
index,
|
|
176
|
+
slice: (prevSlice) => ({
|
|
177
|
+
...prevSlice,
|
|
178
|
+
status: "uploadError",
|
|
179
|
+
onRetry: () => void uploadImage({ index, image, source }),
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
155
184
|
|
|
156
185
|
const inferSlice = async (args: {
|
|
157
186
|
index: number;
|
|
158
187
|
imageUrl: string;
|
|
159
|
-
source: "
|
|
188
|
+
source: "figma" | "upload";
|
|
160
189
|
}) => {
|
|
190
|
+
if (libraryID === undefined) return;
|
|
191
|
+
|
|
161
192
|
const { index, imageUrl, source } = args;
|
|
162
|
-
|
|
193
|
+
let currentId = id.current;
|
|
163
194
|
|
|
164
|
-
const
|
|
195
|
+
const requestId = crypto.randomUUID();
|
|
165
196
|
|
|
166
197
|
setSlice({
|
|
167
198
|
index,
|
|
@@ -169,15 +200,18 @@ export function CreateSliceFromImageModal(
|
|
|
169
200
|
...prevSlice,
|
|
170
201
|
status: "generating",
|
|
171
202
|
thumbnailUrl: imageUrl,
|
|
203
|
+
requestId,
|
|
172
204
|
}),
|
|
173
205
|
});
|
|
174
206
|
|
|
175
207
|
try {
|
|
176
208
|
const inferResult = await managerClient.customTypes.inferSlice({
|
|
177
|
-
imageUrl,
|
|
178
209
|
source,
|
|
179
210
|
libraryID,
|
|
211
|
+
imageUrl,
|
|
212
|
+
requestId,
|
|
180
213
|
});
|
|
214
|
+
|
|
181
215
|
if (currentId !== id.current) return;
|
|
182
216
|
|
|
183
217
|
const model = sliceWithoutConflicts({
|
|
@@ -186,88 +220,156 @@ export function CreateSliceFromImageModal(
|
|
|
186
220
|
slice: inferResult.slice,
|
|
187
221
|
});
|
|
188
222
|
|
|
189
|
-
setSlices((prevSlices) =>
|
|
190
|
-
prevSlices.map((prevSlice, i) =>
|
|
191
|
-
i
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
223
|
+
setSlices((prevSlices) => {
|
|
224
|
+
return prevSlices.map((prevSlice, i) => {
|
|
225
|
+
if (i !== index) return prevSlice;
|
|
226
|
+
return {
|
|
227
|
+
...prevSlice,
|
|
228
|
+
status: "success",
|
|
229
|
+
thumbnailUrl: imageUrl,
|
|
230
|
+
model,
|
|
231
|
+
langSmithUrl: inferResult.langSmithUrl,
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (source === "upload") {
|
|
237
|
+
currentId = id.current;
|
|
238
|
+
const currentSlice = slices[index];
|
|
239
|
+
|
|
240
|
+
const { errors } = await managerClient.slices.createSlice({
|
|
241
|
+
libraryID,
|
|
242
|
+
model: model,
|
|
243
|
+
});
|
|
244
|
+
if (errors.length) {
|
|
245
|
+
throw new Error(`Failed to create slice ${model.id}.`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await managerClient.slices.updateSliceScreenshot({
|
|
249
|
+
libraryID,
|
|
250
|
+
sliceID: model.id,
|
|
251
|
+
variationID: model.variations[0].id,
|
|
252
|
+
data: currentSlice.image,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (currentId !== id.current) return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void completeStep("createSlice");
|
|
259
|
+
|
|
260
|
+
void telemetry.track({
|
|
261
|
+
event: "slice:created",
|
|
262
|
+
id: model.id,
|
|
263
|
+
name: model.name,
|
|
264
|
+
library: libraryID,
|
|
265
|
+
location,
|
|
266
|
+
mode: "ai",
|
|
267
|
+
langSmithUrl: inferResult.langSmithUrl,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
addAiFeedback({
|
|
271
|
+
type: "model",
|
|
272
|
+
library: libraryID,
|
|
273
|
+
sliceId: model.id,
|
|
274
|
+
variationId: model.variations[0].id,
|
|
275
|
+
langSmithUrl: inferResult.langSmithUrl,
|
|
276
|
+
});
|
|
277
|
+
} catch (error) {
|
|
203
278
|
if (currentId !== id.current) return;
|
|
279
|
+
|
|
204
280
|
setSlice({
|
|
205
281
|
index,
|
|
206
282
|
slice: (prevSlice) => ({
|
|
207
283
|
...prevSlice,
|
|
208
|
-
status:
|
|
284
|
+
status:
|
|
285
|
+
error instanceof Error && error.name === "AbortError"
|
|
286
|
+
? "cancelled"
|
|
287
|
+
: "generateError",
|
|
209
288
|
thumbnailUrl: imageUrl,
|
|
210
|
-
onRetry: () =>
|
|
289
|
+
onRetry: () => {
|
|
290
|
+
void inferSlice({ index, imageUrl, source });
|
|
291
|
+
},
|
|
211
292
|
}),
|
|
212
293
|
});
|
|
213
294
|
}
|
|
214
295
|
};
|
|
215
296
|
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
297
|
+
const generatePendingSlices = () => {
|
|
298
|
+
if (libraryID === undefined) return;
|
|
299
|
+
|
|
300
|
+
slices.forEach((slice, index) => {
|
|
301
|
+
if (slice.status === "pending") {
|
|
302
|
+
void inferSlice({
|
|
303
|
+
index,
|
|
304
|
+
imageUrl: slice.thumbnailUrl,
|
|
305
|
+
source: slice.source,
|
|
306
|
+
});
|
|
220
307
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!newSlices.length) return;
|
|
308
|
+
});
|
|
309
|
+
};
|
|
224
310
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
311
|
+
const totals = slices.reduce(
|
|
312
|
+
(result, slice) => {
|
|
313
|
+
if (slice.status === "generating") {
|
|
314
|
+
result.generating++;
|
|
315
|
+
} else if (slice.status === "uploading") {
|
|
316
|
+
result.uploading++;
|
|
317
|
+
} else if (slice.status === "pending") {
|
|
318
|
+
result.pending++;
|
|
319
|
+
} else if (slice.status === "success") {
|
|
320
|
+
result.completed++;
|
|
321
|
+
}
|
|
322
|
+
result.loading = result.generating + result.uploading;
|
|
230
323
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
324
|
+
/** Total count for the generate button.
|
|
325
|
+
* Avoids resetting to zero when switching status for better UX. */
|
|
326
|
+
result.generate = result.loading + result.pending;
|
|
327
|
+
|
|
328
|
+
return result;
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
generating: 0,
|
|
332
|
+
uploading: 0,
|
|
333
|
+
pending: 0,
|
|
334
|
+
completed: 0,
|
|
335
|
+
loading: 0,
|
|
336
|
+
generate: 0,
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const hasTriggeredGeneration = totals.generating > 0 || totals.completed > 0;
|
|
341
|
+
|
|
342
|
+
const closeModal = () => {
|
|
343
|
+
if (totals.loading > 0) return;
|
|
344
|
+
onClose();
|
|
345
|
+
id.current = crypto.randomUUID();
|
|
346
|
+
setTimeout(() => setSlices([]), 250); // wait for the modal fade animation
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const onSubmit = async () => {
|
|
350
|
+
if (libraryID === undefined) return;
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
setIsSubmitting(true);
|
|
354
|
+
|
|
355
|
+
const serverState = await getState();
|
|
356
|
+
createSliceSuccess(serverState.libraries);
|
|
357
|
+
syncChanges();
|
|
358
|
+
|
|
359
|
+
onSuccess({
|
|
360
|
+
slices: slices.flatMap((slice) =>
|
|
361
|
+
slice.status === "success" ? slice.model : [],
|
|
362
|
+
),
|
|
363
|
+
library: libraryID,
|
|
267
364
|
});
|
|
365
|
+
|
|
366
|
+
closeModal();
|
|
367
|
+
} finally {
|
|
368
|
+
setIsSubmitting(false);
|
|
369
|
+
}
|
|
268
370
|
};
|
|
269
371
|
|
|
270
|
-
const
|
|
372
|
+
const onPaste = async () => {
|
|
271
373
|
if (
|
|
272
374
|
!open ||
|
|
273
375
|
!isFigmaEnabled ||
|
|
@@ -362,7 +464,7 @@ export function CreateSliceFromImageModal(
|
|
|
362
464
|
}
|
|
363
465
|
|
|
364
466
|
// Create File object from blob and append to existing slices
|
|
365
|
-
const
|
|
467
|
+
const image = new File([imageBlob], imageName, {
|
|
366
468
|
type: imageBlob.type,
|
|
367
469
|
});
|
|
368
470
|
const newIndex = currentSliceCount;
|
|
@@ -373,12 +475,12 @@ export function CreateSliceFromImageModal(
|
|
|
373
475
|
{
|
|
374
476
|
source: "figma",
|
|
375
477
|
status: "uploading",
|
|
376
|
-
image
|
|
478
|
+
image,
|
|
377
479
|
},
|
|
378
480
|
]);
|
|
379
481
|
|
|
380
482
|
// Start uploading the new image
|
|
381
|
-
void uploadImage({ index: newIndex, image
|
|
483
|
+
void uploadImage({ index: newIndex, image, source: "figma" });
|
|
382
484
|
|
|
383
485
|
toast.success(`Pasted ${imageName}${success ? " from Figma" : ""}`);
|
|
384
486
|
} catch (error) {
|
|
@@ -389,61 +491,230 @@ export function CreateSliceFromImageModal(
|
|
|
389
491
|
}
|
|
390
492
|
};
|
|
391
493
|
|
|
392
|
-
|
|
393
|
-
(slice) =>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
494
|
+
function cancelGeneratingRequests() {
|
|
495
|
+
const cancelableIds = slices.flatMap((slice) => {
|
|
496
|
+
return slice.status === "generating" ? [slice.requestId] : [];
|
|
497
|
+
});
|
|
498
|
+
if (cancelableIds.length === 0) return;
|
|
499
|
+
|
|
500
|
+
cancelableIds.forEach((requestId) => {
|
|
501
|
+
void managerClient.customTypes.cancelInferSlice({ requestId });
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const onCancelConfirm = () => {
|
|
506
|
+
setShowCancelConfirmation(false);
|
|
507
|
+
cancelGeneratingRequests();
|
|
508
|
+
};
|
|
397
509
|
|
|
398
510
|
return (
|
|
399
|
-
<Dialog open={open} onOpenChange={
|
|
400
|
-
<DialogHeader title="Generate
|
|
511
|
+
<Dialog open={open} onOpenChange={(open) => !open && closeModal()}>
|
|
512
|
+
<DialogHeader title="Generate with AI" />
|
|
401
513
|
<DialogContent gap={0}>
|
|
402
514
|
<DialogDescription hidden>
|
|
403
515
|
Upload images to generate slices with AI
|
|
404
516
|
</DialogDescription>
|
|
405
|
-
{
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
517
|
+
{!isLoadingLibraryID ? (
|
|
518
|
+
<>
|
|
519
|
+
{slices.length === 0 ? (
|
|
520
|
+
<Box
|
|
521
|
+
padding={16}
|
|
522
|
+
height="100%"
|
|
523
|
+
gap={16}
|
|
524
|
+
display="flex"
|
|
525
|
+
flexDirection="column"
|
|
526
|
+
>
|
|
527
|
+
{isFigmaEnabled && (
|
|
528
|
+
<Box
|
|
529
|
+
display="flex"
|
|
530
|
+
gap={16}
|
|
531
|
+
alignItems="center"
|
|
532
|
+
backgroundColor="grey2"
|
|
533
|
+
padding={16}
|
|
534
|
+
borderRadius={12}
|
|
535
|
+
>
|
|
536
|
+
<Box
|
|
537
|
+
display="flex"
|
|
538
|
+
gap={8}
|
|
539
|
+
alignItems="center"
|
|
540
|
+
flexGrow={1}
|
|
541
|
+
>
|
|
542
|
+
<Box
|
|
543
|
+
width={48}
|
|
544
|
+
height={48}
|
|
545
|
+
backgroundColor="grey12"
|
|
546
|
+
borderRadius="100%"
|
|
547
|
+
display="flex"
|
|
548
|
+
alignItems="center"
|
|
549
|
+
justifyContent="center"
|
|
550
|
+
>
|
|
551
|
+
<FigmaIcon height={25} />
|
|
552
|
+
</Box>
|
|
553
|
+
<Box display="flex" flexDirection="column" flexGrow={1}>
|
|
554
|
+
<Text variant="bold">Want to work faster?</Text>
|
|
555
|
+
<Text variant="small" color="grey11">
|
|
556
|
+
Copy frames from Figma with the Slice Machine plugin
|
|
557
|
+
and paste them here.
|
|
558
|
+
</Text>
|
|
559
|
+
</Box>
|
|
560
|
+
</Box>
|
|
561
|
+
<Button
|
|
562
|
+
endIcon="arrowForward"
|
|
563
|
+
color="indigo"
|
|
564
|
+
onClick={() =>
|
|
565
|
+
window.open(
|
|
566
|
+
"https://www.figma.com/community/plugin/1567955296461153730/figma-to-slice",
|
|
567
|
+
"_blank",
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
sx={{ marginRight: 8 }}
|
|
571
|
+
invisible
|
|
572
|
+
>
|
|
573
|
+
Install plugin
|
|
574
|
+
</Button>
|
|
575
|
+
</Box>
|
|
576
|
+
)}
|
|
577
|
+
<FileDropZone
|
|
413
578
|
onFilesSelected={onImagesSelected}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
579
|
+
assetType="image"
|
|
580
|
+
maxFiles={IMAGE_UPLOAD_LIMIT}
|
|
581
|
+
overlay={
|
|
582
|
+
<UploadBlankSlate
|
|
583
|
+
onFilesSelected={onImagesSelected}
|
|
584
|
+
onPaste={() => void onPaste()}
|
|
585
|
+
droppingFiles
|
|
586
|
+
/>
|
|
587
|
+
}
|
|
588
|
+
>
|
|
589
|
+
<UploadBlankSlate
|
|
590
|
+
onFilesSelected={onImagesSelected}
|
|
591
|
+
onPaste={() => void onPaste()}
|
|
592
|
+
/>
|
|
593
|
+
</FileDropZone>
|
|
594
|
+
</Box>
|
|
595
|
+
) : (
|
|
596
|
+
<>
|
|
597
|
+
<Box
|
|
598
|
+
display="flex"
|
|
599
|
+
alignItems="center"
|
|
600
|
+
justifyContent="space-between"
|
|
601
|
+
padding={16}
|
|
602
|
+
>
|
|
603
|
+
<Text variant="h3">Design</Text>
|
|
604
|
+
<FileUploadButton
|
|
605
|
+
size="medium"
|
|
606
|
+
color="grey"
|
|
607
|
+
onFilesSelected={onImagesSelected}
|
|
608
|
+
startIcon="attachFile"
|
|
609
|
+
disabled={hasTriggeredGeneration}
|
|
610
|
+
>
|
|
611
|
+
Add images
|
|
612
|
+
</FileUploadButton>
|
|
613
|
+
</Box>
|
|
614
|
+
<ScrollArea stableScrollbar={false}>
|
|
615
|
+
<Box
|
|
616
|
+
display="grid"
|
|
617
|
+
gridTemplateColumns="1fr 1fr"
|
|
618
|
+
gap={16}
|
|
619
|
+
padding={16}
|
|
620
|
+
>
|
|
621
|
+
{slices.map((slice, index) => (
|
|
622
|
+
<SliceCard slice={slice} key={`slice-${index}`} />
|
|
623
|
+
))}
|
|
624
|
+
</Box>
|
|
625
|
+
</ScrollArea>
|
|
626
|
+
</>
|
|
627
|
+
)}
|
|
628
|
+
<DialogActions>
|
|
629
|
+
{totals.generating > 0 ? (
|
|
630
|
+
<DialogCancelButton
|
|
631
|
+
onClick={() => setShowCancelConfirmation(true)}
|
|
632
|
+
size="medium"
|
|
633
|
+
sx={{ marginRight: 8 }}
|
|
634
|
+
invisible
|
|
635
|
+
>
|
|
636
|
+
Cancel
|
|
637
|
+
</DialogCancelButton>
|
|
638
|
+
) : (
|
|
639
|
+
<DialogCancelButton
|
|
640
|
+
onClick={() => closeModal()}
|
|
641
|
+
size="medium"
|
|
642
|
+
sx={{ marginRight: 8 }}
|
|
643
|
+
invisible
|
|
644
|
+
>
|
|
645
|
+
Close
|
|
646
|
+
</DialogCancelButton>
|
|
647
|
+
)}
|
|
648
|
+
{totals.completed === 0 || totals.loading > 0 ? (
|
|
649
|
+
<DialogActionButton
|
|
650
|
+
color="purple"
|
|
651
|
+
startIcon="autoFixHigh"
|
|
652
|
+
onClick={generatePendingSlices}
|
|
653
|
+
disabled={
|
|
654
|
+
hasTriggeredGeneration ||
|
|
655
|
+
totals.loading > 0 ||
|
|
656
|
+
totals.pending === 0
|
|
657
|
+
}
|
|
658
|
+
loading={totals.loading > 0}
|
|
659
|
+
size="medium"
|
|
660
|
+
>
|
|
661
|
+
Generate {totals.generate > 0 ? `(${totals.generate}) ` : ""}
|
|
662
|
+
{totals.generate === 1 ? "Slice" : "Slices"}
|
|
663
|
+
</DialogActionButton>
|
|
664
|
+
) : (
|
|
665
|
+
<DialogActionButton
|
|
666
|
+
color="purple"
|
|
667
|
+
onClick={() => void onSubmit()}
|
|
668
|
+
loading={isSubmitting}
|
|
669
|
+
size="medium"
|
|
670
|
+
>
|
|
671
|
+
{getSubmitButtonLabel(location, totals.completed)}
|
|
672
|
+
</DialogActionButton>
|
|
673
|
+
)}
|
|
674
|
+
</DialogActions>
|
|
675
|
+
</>
|
|
421
676
|
) : (
|
|
422
|
-
<
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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}
|
|
677
|
+
<Box
|
|
678
|
+
display="flex"
|
|
679
|
+
justifyContent="center"
|
|
680
|
+
alignItems="center"
|
|
681
|
+
height="100%"
|
|
442
682
|
>
|
|
443
|
-
|
|
444
|
-
</
|
|
445
|
-
|
|
683
|
+
<ProgressCircle color="purple9" />
|
|
684
|
+
</Box>
|
|
685
|
+
)}
|
|
446
686
|
</DialogContent>
|
|
687
|
+
<Dialog
|
|
688
|
+
size="small"
|
|
689
|
+
open={showCancelConfirmation}
|
|
690
|
+
onOpenChange={setShowCancelConfirmation}
|
|
691
|
+
>
|
|
692
|
+
<DialogHeader title="Cancel generation" />
|
|
693
|
+
<DialogContent>
|
|
694
|
+
<DialogDescription>
|
|
695
|
+
<Box display="flex" flexDirection="column" padding={{ inline: 16 }}>
|
|
696
|
+
<Text variant="bold">
|
|
697
|
+
Are you sure you want to cancel the generation for all slices?
|
|
698
|
+
</Text>
|
|
699
|
+
</Box>
|
|
700
|
+
</DialogDescription>
|
|
701
|
+
<DialogActions>
|
|
702
|
+
<DialogCancelButton
|
|
703
|
+
onClick={() => setShowCancelConfirmation(false)}
|
|
704
|
+
size="small"
|
|
705
|
+
>
|
|
706
|
+
Keep generating
|
|
707
|
+
</DialogCancelButton>
|
|
708
|
+
<DialogActionButton
|
|
709
|
+
color="tomato"
|
|
710
|
+
onClick={onCancelConfirm}
|
|
711
|
+
size="small"
|
|
712
|
+
>
|
|
713
|
+
Confirm
|
|
714
|
+
</DialogActionButton>
|
|
715
|
+
</DialogActions>
|
|
716
|
+
</DialogContent>
|
|
717
|
+
</Dialog>
|
|
447
718
|
</Dialog>
|
|
448
719
|
);
|
|
449
720
|
}
|
|
@@ -451,8 +722,10 @@ export function CreateSliceFromImageModal(
|
|
|
451
722
|
function UploadBlankSlate(props: {
|
|
452
723
|
droppingFiles?: boolean;
|
|
453
724
|
onFilesSelected: (files: File[]) => void;
|
|
725
|
+
onPaste: () => void;
|
|
454
726
|
}) {
|
|
455
|
-
const { droppingFiles = false, onFilesSelected } = props;
|
|
727
|
+
const { droppingFiles = false, onFilesSelected, onPaste } = props;
|
|
728
|
+
const isFigmaEnabled = useIsFigmaEnabled();
|
|
456
729
|
|
|
457
730
|
return (
|
|
458
731
|
<Box
|
|
@@ -463,27 +736,70 @@ function UploadBlankSlate(props: {
|
|
|
463
736
|
border
|
|
464
737
|
borderStyle="dashed"
|
|
465
738
|
borderColor={droppingFiles ? "purple9" : "grey6"}
|
|
739
|
+
borderRadius={12}
|
|
740
|
+
flexGrow={1}
|
|
466
741
|
>
|
|
467
742
|
<BlankSlate>
|
|
468
|
-
<
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
startIcon="attachFile"
|
|
481
|
-
onFilesSelected={onFilesSelected}
|
|
482
|
-
color="grey"
|
|
743
|
+
<Box display="flex" flexDirection="column" gap={16} alignItems="center">
|
|
744
|
+
<BlankSlateIcon
|
|
745
|
+
lineColor="purple11"
|
|
746
|
+
backgroundColor="purple5"
|
|
747
|
+
name="cloudUpload"
|
|
748
|
+
size="large"
|
|
749
|
+
/>
|
|
750
|
+
<Box
|
|
751
|
+
display="flex"
|
|
752
|
+
flexDirection="column"
|
|
753
|
+
gap={4}
|
|
754
|
+
alignItems="center"
|
|
483
755
|
>
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
756
|
+
{isFigmaEnabled ? (
|
|
757
|
+
<>
|
|
758
|
+
<Text>Generate slices from your designs</Text>
|
|
759
|
+
<Text variant="small" color="grey11">
|
|
760
|
+
Upload your design images or paste them directly from Figma.
|
|
761
|
+
</Text>
|
|
762
|
+
</>
|
|
763
|
+
) : (
|
|
764
|
+
<>
|
|
765
|
+
<Text>Upload your design images.</Text>
|
|
766
|
+
<Text variant="small" color="grey11">
|
|
767
|
+
Once uploaded, you can generate slices automatically using AI.
|
|
768
|
+
</Text>
|
|
769
|
+
</>
|
|
770
|
+
)}
|
|
771
|
+
</Box>
|
|
772
|
+
<Box display="flex" alignItems="center" gap={16}>
|
|
773
|
+
{isFigmaEnabled ? (
|
|
774
|
+
<>
|
|
775
|
+
<Button
|
|
776
|
+
size="small"
|
|
777
|
+
renderStartIcon={() => <FigmaIcon height={16} />}
|
|
778
|
+
color="grey"
|
|
779
|
+
onClick={onPaste}
|
|
780
|
+
>
|
|
781
|
+
Paste from Figma
|
|
782
|
+
</Button>
|
|
783
|
+
<FileUploadButton
|
|
784
|
+
size="small"
|
|
785
|
+
onFilesSelected={onFilesSelected}
|
|
786
|
+
color="purple"
|
|
787
|
+
invisible
|
|
788
|
+
>
|
|
789
|
+
Add images
|
|
790
|
+
</FileUploadButton>
|
|
791
|
+
</>
|
|
792
|
+
) : (
|
|
793
|
+
<FileUploadButton
|
|
794
|
+
startIcon="attachFile"
|
|
795
|
+
onFilesSelected={onFilesSelected}
|
|
796
|
+
color="grey"
|
|
797
|
+
>
|
|
798
|
+
Add images
|
|
799
|
+
</FileUploadButton>
|
|
800
|
+
)}
|
|
801
|
+
</Box>
|
|
802
|
+
</Box>
|
|
487
803
|
</BlankSlate>
|
|
488
804
|
</Box>
|
|
489
805
|
);
|
|
@@ -506,12 +822,6 @@ async function getImageUrl({ image }: { image: File }) {
|
|
|
506
822
|
return url;
|
|
507
823
|
}
|
|
508
824
|
|
|
509
|
-
type NewSlice = {
|
|
510
|
-
image: File;
|
|
511
|
-
model: SharedSlice;
|
|
512
|
-
langSmithUrl?: string;
|
|
513
|
-
};
|
|
514
|
-
|
|
515
825
|
/**
|
|
516
826
|
* Keeps track of the existing slices in the project.
|
|
517
827
|
* Re-fetches them when the modal is opened.
|
|
@@ -583,54 +893,17 @@ function sliceWithoutConflicts({
|
|
|
583
893
|
};
|
|
584
894
|
}
|
|
585
895
|
|
|
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
896
|
const getSubmitButtonLabel = (
|
|
625
897
|
location: "custom_type" | "page_type" | "slices",
|
|
898
|
+
completedSliceCount: number,
|
|
626
899
|
) => {
|
|
627
900
|
switch (location) {
|
|
628
901
|
case "custom_type":
|
|
629
|
-
return
|
|
902
|
+
return `Add to type (${completedSliceCount})`;
|
|
630
903
|
case "page_type":
|
|
631
|
-
return
|
|
904
|
+
return `Add to page (${completedSliceCount})`;
|
|
632
905
|
case "slices":
|
|
633
|
-
return "
|
|
906
|
+
return "Done";
|
|
634
907
|
}
|
|
635
908
|
};
|
|
636
909
|
|
|
@@ -639,12 +912,23 @@ function useIsFigmaEnabled() {
|
|
|
639
912
|
return experiment?.value === "on";
|
|
640
913
|
}
|
|
641
914
|
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
915
|
+
function useLibraryID() {
|
|
916
|
+
const [libraryID, setLibraryID] = useState<string | undefined>();
|
|
917
|
+
|
|
918
|
+
useEffect(() => {
|
|
919
|
+
managerClient.project
|
|
920
|
+
.getSliceMachineConfig()
|
|
921
|
+
.then((smConfig) => {
|
|
922
|
+
const libraryID = smConfig?.libraries?.[0];
|
|
923
|
+
if (libraryID === undefined) {
|
|
924
|
+
throw new Error("No library found in the config.");
|
|
925
|
+
}
|
|
926
|
+
setLibraryID(libraryID);
|
|
927
|
+
})
|
|
928
|
+
.catch(() => {
|
|
929
|
+
throw new Error("Could not get library ID from the config.");
|
|
930
|
+
});
|
|
931
|
+
}, []);
|
|
932
|
+
|
|
933
|
+
return { libraryID, isLoading: libraryID === undefined };
|
|
650
934
|
}
|