@slicemachine/manager 0.24.4-alpha.xru-kong-obelix-services.3 → 0.24.4-alpha.xru-slice-generation-ai-poc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
+ import fs from "node:fs";
1
2
  import * as t from "io-ts";
3
+ import OpenAI from "openai";
2
4
  import * as prismicCustomTypesClient from "@prismicio/custom-types-client";
3
5
  import { SharedSliceContent } from "@prismicio/types-internal/lib/content";
4
6
  import { SliceComparator } from "@prismicio/types-internal/lib/customtypes/diff";
@@ -20,6 +22,8 @@ import {
20
22
  SliceRenameHookData,
21
23
  SliceUpdateHook,
22
24
  } from "@slicemachine/plugin-kit";
25
+ import path from "node:path";
26
+ import { ChatCompletionMessageParam } from "openai/resources";
23
27
 
24
28
  import { DecodeError } from "../../lib/DecodeError";
25
29
  import { assertPluginsInitialized } from "../../lib/assertPluginsInitialized";
@@ -36,6 +40,11 @@ import { API_ENDPOINTS } from "../../constants/API_ENDPOINTS";
36
40
  import { UnauthenticatedError, UnauthorizedError } from "../../errors";
37
41
 
38
42
  import { BaseManager } from "../BaseManager";
43
+ import {
44
+ BedrockRuntimeClient,
45
+ ConverseCommand,
46
+ Message,
47
+ } from "@aws-sdk/client-bedrock-runtime";
39
48
 
40
49
  type SlicesManagerReadSliceLibraryReturnType = {
41
50
  sliceIDs: string[];
@@ -207,6 +216,24 @@ type SliceMachineManagerConvertLegacySliceToSharedSliceReturnType = {
207
216
  errors: (DecodeError | HookError)[];
208
217
  };
209
218
 
219
+ type SliceMachineManagerGenerateSliceArgs = {
220
+ libraryID: string;
221
+ slice: SharedSlice;
222
+ imageFile: Uint8Array;
223
+ };
224
+
225
+ type SliceMachineManagerGenerateSliceReturnType = {
226
+ slice?: SharedSlice;
227
+ };
228
+
229
+ type SliceMachineManagerGenerateSlicesFromUrlArgs = {
230
+ sliceImages: Uint8Array[];
231
+ };
232
+
233
+ type SliceMachineManagerGenerateSlicesFromUrlReturnType = {
234
+ slices: SharedSlice[];
235
+ };
236
+
210
237
  export class SlicesManager extends BaseManager {
211
238
  async readSliceLibrary(
212
239
  args: SliceLibraryReadHookData,
@@ -325,6 +352,11 @@ export class SlicesManager extends BaseManager {
325
352
  ): Promise<OnlyHookErrors<CallHookReturnType<SliceCreateHook>>> {
326
353
  assertPluginsInitialized(this.sliceMachinePluginRunner);
327
354
 
355
+ console.log(
356
+ `component code to create NOW for ${args.model.name}:`,
357
+ JSON.stringify(args),
358
+ );
359
+
328
360
  const hookResult = await this.sliceMachinePluginRunner.callHook(
329
361
  "slice:create",
330
362
  args,
@@ -1110,4 +1142,1671 @@ export class SlicesManager extends BaseManager {
1110
1142
 
1111
1143
  return { errors: customTypeReadErrors };
1112
1144
  }
1145
+
1146
+ async generateSlice(
1147
+ args: SliceMachineManagerGenerateSliceArgs,
1148
+ ): Promise<SliceMachineManagerGenerateSliceReturnType> {
1149
+ assertPluginsInitialized(this.sliceMachinePluginRunner);
1150
+
1151
+ const INPUT_TOKEN_PRICE = 0.000003;
1152
+ const OUTPUT_TOKEN_PRICE = 0.000015;
1153
+
1154
+ let totalTokens = {
1155
+ modelGeneration: {
1156
+ input: 0,
1157
+ output: 0,
1158
+ total: 0,
1159
+ price: 0,
1160
+ },
1161
+ mocksGeneration: {
1162
+ input: 0,
1163
+ output: 0,
1164
+ total: 0,
1165
+ price: 0,
1166
+ },
1167
+ codeGeneration: {
1168
+ input: 0,
1169
+ output: 0,
1170
+ total: 0,
1171
+ price: 0,
1172
+ },
1173
+ };
1174
+
1175
+ // Validate AWS credentials
1176
+ const AWS_REGION = "us-east-1";
1177
+ const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = process.env;
1178
+ if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) {
1179
+ throw new Error("AWS credentials are not set.");
1180
+ }
1181
+
1182
+ /**
1183
+ * TypeScript schema for the Shared Slice definition.
1184
+ */
1185
+ const SHARED_SLICE_SCHEMA = `
1186
+ /**
1187
+ * Represents a Prismic Slice.
1188
+ * @property {string} type - Should always be "SharedSlice".
1189
+ * @property {string} id - Unique identifier for the slice in snake_case.
1190
+ * @property {string} name - Human-readable name in PascalCase.
1191
+ * @property {string} description - Brief explanation of the slice's purpose.
1192
+ * @property {SliceVariation[]} variations - Array of variations for the slice.
1193
+ */
1194
+ type PrismicSlice = {
1195
+ type: "SharedSlice";
1196
+ id: string;
1197
+ name: string;
1198
+ description: string;
1199
+ variations: SliceVariation[];
1200
+ };
1201
+
1202
+ /**
1203
+ * Represents a variation of a Prismic Slice.
1204
+ */
1205
+ type SliceVariation = {
1206
+ id: string;
1207
+ name: string;
1208
+ description: string;
1209
+ primary: Record<string, PrismicField>;
1210
+ docURL: string;
1211
+ version: string;
1212
+ };
1213
+
1214
+ /**
1215
+ * Union type representing all possible Prismic fields.
1216
+ */
1217
+ type PrismicField =
1218
+ | UIDField
1219
+ | BooleanField
1220
+ | ColorField
1221
+ | DateField
1222
+ | TimestampField
1223
+ | NumberField
1224
+ | KeyTextField
1225
+ | SelectField
1226
+ | StructuredTextField
1227
+ | ImageField
1228
+ | LinkField
1229
+ | GeoPointField
1230
+ | EmbedField
1231
+ | GroupField;
1232
+
1233
+ /* Definitions for each field type follow... */
1234
+
1235
+ /**
1236
+ * Represents a UID Field in Prismic.
1237
+ * @property {"UID"} type - Field type.
1238
+ * @property {Object} config - Configuration object.
1239
+ * @property {string} config.label - Label displayed in the editor.
1240
+ * @property {string} [config.placeholder] - Placeholder text.
1241
+ * @property {string} [config.customregex] - Custom regex for validation.
1242
+ * @property {string} [config.errorMessage] - Error message for invalid input.
1243
+ */
1244
+ type UIDField = {
1245
+ type: "UID";
1246
+ config: {
1247
+ label: string;
1248
+ placeholder?: string;
1249
+ customregex?: string;
1250
+ errorMessage?: string;
1251
+ };
1252
+ };
1253
+
1254
+ /**
1255
+ * Represents a Boolean Field in Prismic.
1256
+ * @property {"Boolean"} type - Field type.
1257
+ * @property {Object} config - Configuration object.
1258
+ * @property {string} config.label - Label displayed in the editor.
1259
+ * @property {boolean} [config.default_value] - Default value (true or false).
1260
+ */
1261
+ type BooleanField = {
1262
+ type: "Boolean";
1263
+ config: {
1264
+ label: string;
1265
+ default_value?: boolean;
1266
+ };
1267
+ };
1268
+
1269
+ /**
1270
+ * Represents a Color Field in Prismic.
1271
+ * @property {"Color"} type - Field type.
1272
+ * @property {Object} config - Configuration object.
1273
+ * @property {string} config.label - Label displayed in the editor.
1274
+ */
1275
+ type ColorField = {
1276
+ type: "Color";
1277
+ config: {
1278
+ label: string;
1279
+ };
1280
+ };
1281
+
1282
+ /**
1283
+ * Represents a Date Field in Prismic.
1284
+ * @property {"Date"} type - Field type.
1285
+ * @property {Object} config - Configuration object.
1286
+ * @property {string} config.label - Label displayed in the editor.
1287
+ */
1288
+ type DateField = {
1289
+ type: "Date";
1290
+ config: {
1291
+ label: string;
1292
+ };
1293
+ };
1294
+
1295
+ /**
1296
+ * Represents a Timestamp Field in Prismic.
1297
+ * @property {"Timestamp"} type - Field type.
1298
+ * @property {Object} config - Configuration object.
1299
+ * @property {string} config.label - Label displayed in the editor.
1300
+ */
1301
+ type TimestampField = {
1302
+ type: "Timestamp";
1303
+ config: {
1304
+ label: string;
1305
+ };
1306
+ };
1307
+
1308
+ /**
1309
+ * Represents a Number Field in Prismic.
1310
+ * @property {"Number"} type - Field type.
1311
+ * @property {Object} config - Configuration object.
1312
+ * @property {string} config.label - Label displayed in the editor.
1313
+ * @property {string} [config.placeholder] - Placeholder text.
1314
+ * @property {number} [config.min] - Minimum allowable value.
1315
+ * @property {number} [config.max] - Maximum allowable value.
1316
+ */
1317
+ type NumberField = {
1318
+ type: "Number";
1319
+ config: {
1320
+ label: string;
1321
+ placeholder?: string;
1322
+ min?: number;
1323
+ max?: number;
1324
+ };
1325
+ };
1326
+
1327
+ /**
1328
+ * Represents a Key Text Field in Prismic.
1329
+ * @property {"Text"} type - Field type.
1330
+ * @property {Object} config - Configuration object.
1331
+ * @property {string} config.label - Label displayed in the editor.
1332
+ * @property {string} [config.placeholder] - Placeholder text.
1333
+ */
1334
+ type KeyTextField = {
1335
+ type: "Text";
1336
+ config: {
1337
+ label: string;
1338
+ placeholder?: string;
1339
+ };
1340
+ };
1341
+
1342
+ /**
1343
+ * Represents a Select Field in Prismic.
1344
+ * @property {"Select"} type - Field type.
1345
+ * @property {Object} config - Configuration object.
1346
+ * @property {string} config.label - Label displayed in the editor.
1347
+ * @property {string[]} config.options - Array of options for the select dropdown.
1348
+ */
1349
+ type SelectField = {
1350
+ type: "Select";
1351
+ config: {
1352
+ label: string;
1353
+ options: string[];
1354
+ };
1355
+ };
1356
+
1357
+ /**
1358
+ * Represents a Structured Text Field in Prismic.
1359
+ * @property {"StructuredText"} type - Field type.
1360
+ * @property {Object} config - Configuration object.
1361
+ * @property {string} config.label - Label displayed in the editor.
1362
+ * @property {string} [config.placeholder] - Placeholder text.
1363
+ * @property {string} [config.single] - A comma-separated list of formatting options that does not allow line breaks. Options: paragraph | preformatted | heading1 | heading2 | heading3 | heading4 | heading5 | heading6 | strong | em | hyperlink | image | embed | list-item | o-list-item | rtl.
1364
+ * @property {string} [config.multi] - A comma-separated list of formatting options, with paragraph breaks allowed. Options: paragraph | preformatted | heading1 | heading2 | heading3 | heading4 | heading5 | heading6 | strong | em | hyperlink | image | embed | list-item | o-list-item | rtl.
1365
+ * @property {boolean} [config.allowTargetBlank] - Allows links to open in a new tab.
1366
+ * @property {string[]} [config.labels] - An array of strings to define labels for custom formatting.
1367
+ * @property {ImageConstraint} [config.imageConstraint] - Constraints for images within the rich text field.
1368
+ */
1369
+ type StructuredTextField = {
1370
+ type: "StructuredText";
1371
+ config: {
1372
+ label: string;
1373
+ placeholder?: string;
1374
+ single?: string;
1375
+ multi?: string;
1376
+ allowTargetBlank?: boolean;
1377
+ labels?: string[];
1378
+ imageConstraint?: ImageConstraint;
1379
+ };
1380
+ };
1381
+
1382
+ /**
1383
+ * Represents constraints for images within a rich text field.
1384
+ * @property {number} [width] - Width constraint in pixels.
1385
+ * @property {number
1386
+ * @property {number} [height] - Height constraint in pixels.
1387
+ */
1388
+ type ImageConstraint = {
1389
+ width?: number;
1390
+ height?: number;
1391
+ };
1392
+
1393
+ /**
1394
+ * Represents an Image Field in Prismic.
1395
+ * @property {"Image"} type - Field type.
1396
+ * @property {Object} config - Configuration object.
1397
+ * @property {string} config.label - Label displayed in the editor.
1398
+ * @property {Object} [config.constraint] - Constraints for the image dimensions.
1399
+ * @property {number} [config.constraint.width] - Width constraint.
1400
+ * @property {number} [config.constraint.height] - Height constraint.
1401
+ * @property {Thumbnail[]} [config.thumbnails] - Array of thumbnail configurations.
1402
+ */
1403
+ type ImageField = {
1404
+ type: "Image";
1405
+ config: {
1406
+ label: string;
1407
+ constraint?: {
1408
+ width?: number;
1409
+ height?: number;
1410
+ };
1411
+ thumbnails?: Thumbnail[];
1412
+ };
1413
+ };
1414
+
1415
+ /**
1416
+ * Represents a Thumbnail configuration for an Image field.
1417
+ * @property {string} name - Name of the thumbnail.
1418
+ * @property {number} [width] - Width of the thumbnail in pixels.
1419
+ * @property {number} [height] - Height of the thumbnail in pixels.
1420
+ */
1421
+ type Thumbnail = {
1422
+ name: string;
1423
+ width?: number;
1424
+ height?: number;
1425
+ };
1426
+
1427
+ /**
1428
+ * Represents a Link Field in Prismic.
1429
+ * @property {"Link"} type - Field type.
1430
+ * @property {Object} config - Configuration object.
1431
+ * @property {string} config.label - Label displayed in the editor.
1432
+ * @property {boolean} config.allowText - Enable the text field for the link.
1433
+ */
1434
+ type LinkField = {
1435
+ type: "Link";
1436
+ config: {
1437
+ label: string;
1438
+ allowText: boolean;
1439
+ };
1440
+ };
1441
+
1442
+ /**
1443
+ * Represents an Embed Field in Prismic.
1444
+ * @property {"Embed"} type - Field type.
1445
+ * @property {Object} config - Configuration object.
1446
+ * @property {string} config.label - Label displayed in the editor.
1447
+ */
1448
+ type EmbedField = {
1449
+ type: "Embed";
1450
+ config: {
1451
+ label: string;
1452
+ };
1453
+ };
1454
+
1455
+ /**
1456
+ * Represents a GeoPoint Field in Prismic.
1457
+ * @property {"GeoPoint"} type - Field type.
1458
+ * @property {Object} config - Configuration object.
1459
+ * @property {string} config.label - Label displayed in the editor.
1460
+ */
1461
+ type GeoPointField = {
1462
+ type: "GeoPoint";
1463
+ config: {
1464
+ label: string;
1465
+ };
1466
+ };
1467
+
1468
+ /**
1469
+ * Represents a Group Field (Repeatable Fields) in Prismic.
1470
+ * @property {"Group"} type - Field type.
1471
+ * @property {Object} config - Configuration object.
1472
+ * @property {string} config.label - Label displayed in the editor.
1473
+ * @property {Record<string, PrismicField>} config.fields - Defines the fields inside the group.
1474
+ */
1475
+ type GroupField = {
1476
+ type: "Group";
1477
+ config: {
1478
+ label: string;
1479
+ fields: Record<string, PrismicField>;
1480
+ };
1481
+ };
1482
+ `;
1483
+
1484
+ /**
1485
+ * Calls the AI to generate the slice model.
1486
+ */
1487
+ async function generateSliceModel(
1488
+ client: BedrockRuntimeClient,
1489
+ existingSlice: SharedSlice,
1490
+ imageFile: Uint8Array,
1491
+ ): Promise<SharedSlice> {
1492
+ const systemPrompt = `
1493
+ You are an expert in Prismic content modeling. Using the image provided, generate a valid Prismic JSON model for the slice described below. Follow these rules precisely:
1494
+ - Use the TypeScript schema provided as your reference.
1495
+ - Place all main content fields under the "primary" object.
1496
+ - Do not create any collections or groups for single-image content (background images should be a single image field).
1497
+ - Ensure that each field has appropriate placeholders, labels, and configurations.
1498
+ - Never generate a Link / Button text field, only the Link / Button field itself is enough. Just enable "allowText" when doing that.
1499
+ - Do not forget any field visible from the image provide in the user prompt.
1500
+ - Ensure to differentiate Prismic fields from just an image with visual inside the image. When that's the case, just add a Prismic image field.
1501
+ - Do not include any decorative fields.
1502
+ - Do not include any extra commentary or formatting.
1503
+
1504
+ !IMPORTANT!:
1505
+ - Only return a valid JSON object representing the full slice model, nothing else before. JSON.parse on your response should not throw an error.
1506
+ - All your response should fit in a single return response.
1507
+
1508
+ Reference Schema:
1509
+ ${SHARED_SLICE_SCHEMA}
1510
+
1511
+ Existing Slice:
1512
+ ${JSON.stringify(existingSlice)}
1513
+ `.trim();
1514
+ const command = new ConverseCommand({
1515
+ modelId: "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
1516
+ system: [{ text: systemPrompt }],
1517
+ messages: [
1518
+ {
1519
+ role: "user",
1520
+ content: [
1521
+ {
1522
+ image: { format: "png", source: { bytes: imageFile } },
1523
+ },
1524
+ ],
1525
+ },
1526
+ ],
1527
+ });
1528
+
1529
+ const response = await client.send(command);
1530
+ console.log("Generated model response:", JSON.stringify(response));
1531
+
1532
+ if (
1533
+ !response.usage ||
1534
+ !response.usage.inputTokens ||
1535
+ !response.usage.outputTokens ||
1536
+ !response.usage.totalTokens
1537
+ ) {
1538
+ throw new Error("No usage data was returned.");
1539
+ }
1540
+ totalTokens.modelGeneration = {
1541
+ input: response.usage.inputTokens,
1542
+ output: response.usage.outputTokens,
1543
+ total: response.usage.totalTokens,
1544
+ price:
1545
+ response.usage.inputTokens * INPUT_TOKEN_PRICE +
1546
+ response.usage.outputTokens * OUTPUT_TOKEN_PRICE,
1547
+ };
1548
+
1549
+ const resultText = response.output?.message?.content?.[0]?.text?.trim();
1550
+ if (!resultText) {
1551
+ throw new Error("No valid slice model was generated.");
1552
+ }
1553
+
1554
+ try {
1555
+ const generatedModel = JSON.parse(resultText);
1556
+ const updatedSlice: SharedSlice = {
1557
+ ...args.slice,
1558
+ variations: generatedModel.variations,
1559
+ };
1560
+
1561
+ return updatedSlice;
1562
+ } catch (error) {
1563
+ throw new Error("Failed to parse AI response for model: " + error);
1564
+ }
1565
+ }
1566
+
1567
+ /**
1568
+ * Calls the AI endpoint to generate mocks.
1569
+ */
1570
+ async function generateSliceMocks(
1571
+ client: BedrockRuntimeClient,
1572
+ imageFile: Uint8Array,
1573
+ existingMocks: SharedSliceContent[],
1574
+ ): Promise<SharedSliceContent[]> {
1575
+ // Build a prompt focused solely on updating the mocks.
1576
+ const systemPrompt = `
1577
+ You are a seasoned frontend engineer with deep expertise in Prismic slices.
1578
+ Your task is to update the provided mocks template based solely on the visible content in the image.
1579
+ Follow these guidelines strictly:
1580
+ - Do not modify the overall structure of the mocks template.
1581
+ - Strictly only update text content.
1582
+ - Do not touch images.
1583
+ - If you see a repetition with a group, you must create the same number of group items that are visible on the image.
1584
+
1585
+ !IMPORTANT!:
1586
+ - Only return a valid JSON object for mocks, nothing else before. JSON.parse on your response should not throw an error.
1587
+ - All your response should fit in a single return response.
1588
+
1589
+ Existing Mocks Template:
1590
+ ${JSON.stringify(existingMocks, null, 2)}
1591
+ `.trim();
1592
+
1593
+ const command = new ConverseCommand({
1594
+ modelId: "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
1595
+ system: [{ text: systemPrompt }],
1596
+ messages: [
1597
+ {
1598
+ role: "user",
1599
+ content: [
1600
+ { image: { format: "png", source: { bytes: imageFile } } },
1601
+ ],
1602
+ },
1603
+ ],
1604
+ });
1605
+
1606
+ const response = await client.send(command);
1607
+ console.log("Generated mocks response:", JSON.stringify(response));
1608
+
1609
+ if (
1610
+ !response.usage ||
1611
+ !response.usage.inputTokens ||
1612
+ !response.usage.outputTokens ||
1613
+ !response.usage.totalTokens
1614
+ ) {
1615
+ throw new Error("No usage data was returned.");
1616
+ }
1617
+ totalTokens.mocksGeneration = {
1618
+ input: response.usage.inputTokens,
1619
+ output: response.usage.outputTokens,
1620
+ total: response.usage.totalTokens,
1621
+ price:
1622
+ response.usage.inputTokens * INPUT_TOKEN_PRICE +
1623
+ response.usage.outputTokens * OUTPUT_TOKEN_PRICE,
1624
+ };
1625
+
1626
+ const resultText = response.output?.message?.content?.[0]?.text?.trim();
1627
+ if (!resultText) {
1628
+ throw new Error("No valid mocks were generated.");
1629
+ }
1630
+
1631
+ try {
1632
+ const updatedMocks = JSON.parse(resultText);
1633
+
1634
+ return updatedMocks;
1635
+ } catch (error) {
1636
+ throw new Error("Failed to parse AI response for mocks: " + error);
1637
+ }
1638
+ }
1639
+
1640
+ /**
1641
+ * Calls the AI endpoint to generate the slice React component.
1642
+ */
1643
+ async function generateSliceComponentCode(
1644
+ client: BedrockRuntimeClient,
1645
+ imageFile: Uint8Array,
1646
+ existingMocks: any,
1647
+ ): Promise<string> {
1648
+ // Build a prompt focused solely on generating the React component code.
1649
+ const systemPrompt = `
1650
+ You are a seasoned frontend engineer with deep expertise in Prismic slices.
1651
+ Your task is to generate a fully isolated React component code for a slice based on the provided image input.
1652
+ Follow these guidelines strictly:
1653
+ - Be self-contained.
1654
+ - Your goal is to make the code visually looks as close as possible to the image from the user input.
1655
+ - Ensure that the color used for the background is the same as the image provide in the user prompt! It's better no background color than a wrong one.
1656
+ - For links, you must use PrismicNextLink and you must just pass the field, PrismicNextLink will handle the display of the link text, don't do it manually.
1657
+ - Respect the padding and margin visible in the image provide in the user prompt.
1658
+ - Respect the fonts size, color, type visible in the image provide in the user prompt.
1659
+ - Respect the colors visible in the image provide in the user prompt.
1660
+ - Respect the position of elements visible in the image provide in the user prompt.
1661
+ - Respect the size of each elements visible in the image provide in the user prompt.
1662
+ - Respect the overall proportions of the slice from the image provide in the user prompt.
1663
+ - Ensure to strictly respect what is defined on the mocks for each fields ID, do not invent or use something not in the mocks.
1664
+ - Ensure to use all fields provided in the mocks.
1665
+ - Use inline <style> (do not use <style jsx>).
1666
+ - Follow the structure provided in the code example below.
1667
+
1668
+ !IMPORTANT!:
1669
+ - Only return a valid JSON object with two keys: "mocks" and "componentCode", nothing else before. JSON.parse on your response should not throw an error.
1670
+ - All your response should fit in a single return response.
1671
+
1672
+ ## Example of a Fully Isolated Slice Component:
1673
+ -----------------------------------------------------------
1674
+ import { type Content } from "@prismicio/client";
1675
+ import { PrismicNextLink, PrismicNextImage } from "@prismicio/next";
1676
+ import { SliceComponentProps, PrismicRichText } from "@prismicio/react";
1677
+
1678
+ export type HeroProps = SliceComponentProps<Content.HeroSlice>;
1679
+
1680
+ const Hero = ({ slice }: HeroProps): JSX.Element => {
1681
+ return (
1682
+ <section
1683
+ data-slice-type={slice.slice_type}
1684
+ data-slice-variation={slice.variation}
1685
+ className="hero"
1686
+ >
1687
+ <div className="hero__content">
1688
+ <div className="hero__image-wrapper">
1689
+ <PrismicNextImage field={slice.primary.image} className="hero__image" />
1690
+ </div>
1691
+ <div className="hero__text">
1692
+ <PrismicRichText field={slice.primary.title} />
1693
+ <PrismicRichText field={slice.primary.description} />
1694
+ <PrismicNextLink field={slice.primary.link} />
1695
+ </div>
1696
+ </div>
1697
+ <style>
1698
+ {\`
1699
+ .hero { display: flex; flex-direction: row; padding: 20px; }
1700
+ .hero__content { width: 100%; }
1701
+ .hero__image-wrapper { flex: 1; }
1702
+ .hero__text { flex: 1; padding-left: 20px; }
1703
+ \`}
1704
+ </style>
1705
+ </section>
1706
+ );
1707
+ };
1708
+
1709
+ export default Hero;
1710
+ -----------------------------------------------------------
1711
+ Existing Mocks Template:
1712
+ ${JSON.stringify(existingMocks, null, 2)}
1713
+ `.trim();
1714
+
1715
+ const command = new ConverseCommand({
1716
+ modelId: "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
1717
+ system: [{ text: systemPrompt }],
1718
+ messages: [
1719
+ {
1720
+ role: "user",
1721
+ content: [
1722
+ { image: { format: "png", source: { bytes: imageFile } } },
1723
+ ],
1724
+ },
1725
+ ],
1726
+ });
1727
+
1728
+ const response = await client.send(command);
1729
+ console.log(
1730
+ "Generated component code response:",
1731
+ JSON.stringify(response),
1732
+ );
1733
+
1734
+ if (
1735
+ !response.usage ||
1736
+ !response.usage.inputTokens ||
1737
+ !response.usage.outputTokens ||
1738
+ !response.usage.totalTokens
1739
+ ) {
1740
+ throw new Error("No usage data was returned.");
1741
+ }
1742
+ totalTokens.codeGeneration = {
1743
+ input: response.usage.inputTokens,
1744
+ output: response.usage.outputTokens,
1745
+ total: response.usage.totalTokens,
1746
+ price:
1747
+ response.usage.inputTokens * INPUT_TOKEN_PRICE +
1748
+ response.usage.outputTokens * OUTPUT_TOKEN_PRICE,
1749
+ };
1750
+
1751
+ const resultText = response.output?.message?.content?.[0]?.text?.trim();
1752
+ if (!resultText) {
1753
+ throw new Error("No valid slice component code was generated.");
1754
+ }
1755
+
1756
+ try {
1757
+ const parsed = JSON.parse(resultText);
1758
+ if (!parsed.componentCode) {
1759
+ throw new Error("Missing key 'componentCode' in AI response.");
1760
+ }
1761
+ return parsed.componentCode;
1762
+ } catch (error) {
1763
+ throw new Error(
1764
+ "Failed to parse AI response for component code: " + error,
1765
+ );
1766
+ }
1767
+ }
1768
+
1769
+ // Initialize AWS Bedrock client.
1770
+ const bedrockClient = new BedrockRuntimeClient({
1771
+ region: AWS_REGION,
1772
+ credentials: {
1773
+ accessKeyId: AWS_ACCESS_KEY_ID,
1774
+ secretAccessKey: AWS_SECRET_ACCESS_KEY,
1775
+ },
1776
+ });
1777
+
1778
+ try {
1779
+ // ----- Q1 scope -----
1780
+
1781
+ // STEP 1: Generate the slice model using the image.
1782
+ console.log("STEP 1: Generate the slice model using the image.");
1783
+ const updatedSlice = await generateSliceModel(
1784
+ bedrockClient,
1785
+ args.slice,
1786
+ args.imageFile,
1787
+ );
1788
+
1789
+ // STEP 2: Persist the updated slice model.
1790
+ console.log("STEP 2: Persist the updated slice model.");
1791
+ await this.updateSlice({
1792
+ libraryID: args.libraryID,
1793
+ model: updatedSlice,
1794
+ });
1795
+
1796
+ // STEP 3: Update the slice screenshot.
1797
+ console.log("STEP 3: Update the slice screenshot.");
1798
+ await this.updateSliceScreenshot({
1799
+ libraryID: args.libraryID,
1800
+ sliceID: updatedSlice.id,
1801
+ variationID: updatedSlice.variations[0].id,
1802
+ data: Buffer.from(args.imageFile),
1803
+ });
1804
+
1805
+ // ----- Q1 scope -----
1806
+
1807
+ // STEP 4: Generate updated mocks.
1808
+ console.log("STEP 4: Generate updated mocks.");
1809
+ const existingMocks = mockSlice({ model: updatedSlice });
1810
+ const updatedMocks = await generateSliceMocks(
1811
+ bedrockClient,
1812
+ args.imageFile,
1813
+ existingMocks,
1814
+ );
1815
+
1816
+ // STEP 5: Generate the isolated slice component code.
1817
+ console.log("STEP 5: Generate updated component code.");
1818
+ const componentCode = await generateSliceComponentCode(
1819
+ bedrockClient,
1820
+ args.imageFile,
1821
+ existingMocks,
1822
+ );
1823
+
1824
+ // STEP 6: Update the slice code.
1825
+ console.log("STEP 6: Update the slice code.");
1826
+ await this.createSlice({
1827
+ libraryID: args.libraryID,
1828
+ model: updatedSlice,
1829
+ componentContents: componentCode,
1830
+ });
1831
+
1832
+ // STEP 7: Persist the generated mocks.
1833
+ console.log("STEP 7: Persist the generated mocks.");
1834
+ await this.updateSliceMocks({
1835
+ libraryID: args.libraryID,
1836
+ sliceID: args.slice.id,
1837
+ mocks: updatedMocks,
1838
+ });
1839
+
1840
+ // Usage
1841
+ console.log("Tokens used:", totalTokens);
1842
+ const totalPrice = Object.values(totalTokens).reduce(
1843
+ (acc, { price }) => acc + price,
1844
+ 0,
1845
+ );
1846
+ console.log("Total price:", totalPrice);
1847
+
1848
+ return { slice: updatedSlice };
1849
+ } catch (error) {
1850
+ console.error("Failed to generate slice:", error);
1851
+ throw new Error("Failed to generate slice: " + error);
1852
+ }
1853
+ }
1854
+
1855
+ async generateSlicesFromUrl(
1856
+ args: SliceMachineManagerGenerateSlicesFromUrlArgs,
1857
+ ): Promise<SliceMachineManagerGenerateSlicesFromUrlReturnType> {
1858
+ assertPluginsInitialized(this.sliceMachinePluginRunner);
1859
+
1860
+ const { OPENAI_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } =
1861
+ process.env;
1862
+
1863
+ if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) {
1864
+ throw new Error("AWS credentials are not set.");
1865
+ }
1866
+ const AWS_REGION = "us-east-1";
1867
+ const bedrockClient = new BedrockRuntimeClient({
1868
+ region: AWS_REGION,
1869
+ credentials: {
1870
+ accessKeyId: AWS_ACCESS_KEY_ID,
1871
+ secretAccessKey: AWS_SECRET_ACCESS_KEY,
1872
+ },
1873
+ });
1874
+
1875
+ if (!OPENAI_API_KEY) {
1876
+ throw new Error("OPENAI_API_KEY is not set.");
1877
+ }
1878
+ const openai = new OpenAI({ apiKey: OPENAI_API_KEY });
1879
+
1880
+ const sliceMachineConfig = await this.project.getSliceMachineConfig();
1881
+ const libraryIDs = sliceMachineConfig.libraries || [];
1882
+ const DEFAULT_LIBRARY_ID = libraryIDs[0];
1883
+
1884
+ let retry: {
1885
+ [key in "MODEL" | "MOCKS" | "CODE" | "APPEARANCE"]: number;
1886
+ }[] = args.sliceImages.map((_) => ({
1887
+ MODEL: 0,
1888
+ MOCKS: 0,
1889
+ CODE: 0,
1890
+ APPEARANCE: 0,
1891
+ }));
1892
+
1893
+ async function callAI<ReturnType extends Record<string, unknown>>({
1894
+ ai,
1895
+ sliceIndex,
1896
+ stepName,
1897
+ systemPrompt,
1898
+ imageFile,
1899
+ textContent,
1900
+ }: {
1901
+ ai: "OPENAI" | "AWS";
1902
+ sliceIndex: number;
1903
+ stepName: "MODEL" | "MOCKS" | "CODE" | "APPEARANCE";
1904
+ systemPrompt: string;
1905
+ imageFile?: Uint8Array;
1906
+ textContent?: string;
1907
+ }): Promise<ReturnType> {
1908
+ let resultText: string | undefined;
1909
+
1910
+ if (ai === "OPENAI") {
1911
+ const messages: Array<ChatCompletionMessageParam> = [
1912
+ { role: "system", content: systemPrompt },
1913
+ ];
1914
+
1915
+ const userContent: Array<
1916
+ | {
1917
+ type: "text";
1918
+ text: string;
1919
+ }
1920
+ | {
1921
+ type: "image_url";
1922
+ image_url: { url: string };
1923
+ }
1924
+ > = [];
1925
+
1926
+ if (imageFile) {
1927
+ userContent.push({
1928
+ type: "image_url",
1929
+ image_url: {
1930
+ url:
1931
+ "data:image/png;base64," +
1932
+ Buffer.from(imageFile).toString("base64"),
1933
+ },
1934
+ });
1935
+ }
1936
+
1937
+ if (textContent) {
1938
+ userContent.push({ type: "text", text: textContent });
1939
+ }
1940
+
1941
+ if (userContent.length > 0) {
1942
+ messages.push({
1943
+ role: "user",
1944
+ content: userContent,
1945
+ });
1946
+ }
1947
+
1948
+ const response = await openai.chat.completions.create({
1949
+ model: "gpt-4o",
1950
+ messages,
1951
+ response_format: { type: "json_object" },
1952
+ });
1953
+
1954
+ console.log(
1955
+ `Generated response for ${stepName} - ${sliceIndex}:`,
1956
+ JSON.stringify(response),
1957
+ );
1958
+
1959
+ resultText = response.choices[0]?.message?.content?.trim();
1960
+ } else if (ai === "AWS") {
1961
+ const messages: Array<Message> = [];
1962
+
1963
+ if (imageFile) {
1964
+ messages.push({
1965
+ role: "user",
1966
+ content: [
1967
+ {
1968
+ image: { format: "png", source: { bytes: imageFile } },
1969
+ },
1970
+ ],
1971
+ });
1972
+ }
1973
+
1974
+ if (textContent) {
1975
+ messages.push({
1976
+ role: "user",
1977
+ content: [
1978
+ {
1979
+ text: textContent,
1980
+ },
1981
+ ],
1982
+ });
1983
+ }
1984
+
1985
+ const command = new ConverseCommand({
1986
+ modelId: "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
1987
+ system: [{ text: systemPrompt }],
1988
+ messages: messages,
1989
+ });
1990
+
1991
+ const response = await bedrockClient.send(command);
1992
+
1993
+ console.log(
1994
+ `Generated response for ${stepName} - ${sliceIndex}:`,
1995
+ JSON.stringify(response),
1996
+ );
1997
+
1998
+ resultText = response.output?.message?.content?.[0]?.text?.trim();
1999
+ }
2000
+
2001
+ async function retryCall(error: string): Promise<ReturnType> {
2002
+ if (retry[sliceIndex][stepName] < 3) {
2003
+ retry[sliceIndex][stepName]++;
2004
+ console.log(
2005
+ `Retrying ${retry[sliceIndex][stepName]} ${stepName} for slice ${sliceIndex}.`,
2006
+ error,
2007
+ );
2008
+
2009
+ return await callAI({
2010
+ ai,
2011
+ sliceIndex,
2012
+ stepName,
2013
+ systemPrompt,
2014
+ imageFile,
2015
+ textContent,
2016
+ });
2017
+ }
2018
+
2019
+ throw new Error(error);
2020
+ }
2021
+
2022
+ if (!resultText) {
2023
+ return await retryCall(
2024
+ `No valid response was generated for ${stepName}.`,
2025
+ );
2026
+ }
2027
+
2028
+ try {
2029
+ return JSON.parse(resultText);
2030
+ } catch (error) {
2031
+ return await retryCall(
2032
+ `Failed to parse AI response for ${stepName}: ` + error,
2033
+ );
2034
+ }
2035
+ }
2036
+
2037
+ /**
2038
+ * TypeScript schema for the Shared Slice definition.
2039
+ */
2040
+ const SHARED_SLICE_SCHEMA = `
2041
+ /**
2042
+ * Represents a Prismic Slice.
2043
+ * @property {string} type - Should always be "SharedSlice".
2044
+ * @property {string} id - Unique identifier for the slice in snake_case.
2045
+ * @property {string} name - Human-readable name in PascalCase.
2046
+ * @property {string} description - Brief explanation of the slice's purpose.
2047
+ * @property {SliceVariation[]} variations - Array of variations for the slice.
2048
+ */
2049
+ type PrismicSlice = {
2050
+ type: "SharedSlice";
2051
+ id: string;
2052
+ name: string;
2053
+ description: string;
2054
+ variations: SliceVariation[];
2055
+ };
2056
+
2057
+ /**
2058
+ * Represents a variation of a Prismic Slice.
2059
+ * TIPS: Never use "items" property, you can see it doesn't exist here!
2060
+ */
2061
+ type SliceVariation = {
2062
+ id: string;
2063
+ name: string;
2064
+ description: string;
2065
+ primary: Record<string, PrismicField>;
2066
+ docURL: string;
2067
+ version: string;
2068
+ };
2069
+
2070
+ /**
2071
+ * Union type representing all possible Prismic fields.
2072
+ */
2073
+ type PrismicField =
2074
+ | UIDField
2075
+ | BooleanField
2076
+ | ColorField
2077
+ | DateField
2078
+ | TimestampField
2079
+ | NumberField
2080
+ | TextField
2081
+ | SelectField
2082
+ | StructuredTextField
2083
+ | ImageField
2084
+ | LinkField
2085
+ | GeoPointField
2086
+ | EmbedField
2087
+ | GroupField;
2088
+
2089
+ /* Definitions for each field type follow... */
2090
+
2091
+ /**
2092
+ * Represents a UID Field in Prismic.
2093
+ * @property {"UID"} type - Field type.
2094
+ * @property {Object} config - Configuration object.
2095
+ * @property {string} config.label - Label displayed in the editor.
2096
+ * @property {string} [config.placeholder] - Placeholder text.
2097
+ * @property {string} [config.customregex] - Custom regex for validation.
2098
+ * @property {string} [config.errorMessage] - Error message for invalid input.
2099
+ */
2100
+ type UIDField = {
2101
+ type: "UID";
2102
+ config: {
2103
+ label: string;
2104
+ placeholder?: string;
2105
+ customregex?: string;
2106
+ errorMessage?: string;
2107
+ };
2108
+ };
2109
+
2110
+ /**
2111
+ * Represents a Boolean Field in Prismic.
2112
+ * @property {"Boolean"} type - Field type.
2113
+ * @property {Object} config - Configuration object.
2114
+ * @property {string} config.label - Label displayed in the editor.
2115
+ * @property {boolean} [config.default_value] - Default value (true or false).
2116
+ */
2117
+ type BooleanField = {
2118
+ type: "Boolean";
2119
+ config: {
2120
+ label: string;
2121
+ default_value?: boolean;
2122
+ };
2123
+ };
2124
+
2125
+ /**
2126
+ * Represents a Color Field in Prismic.
2127
+ * @property {"Color"} type - Field type.
2128
+ * @property {Object} config - Configuration object.
2129
+ * @property {string} config.label - Label displayed in the editor.
2130
+ */
2131
+ type ColorField = {
2132
+ type: "Color";
2133
+ config: {
2134
+ label: string;
2135
+ };
2136
+ };
2137
+
2138
+ /**
2139
+ * Represents a Date Field in Prismic.
2140
+ * @property {"Date"} type - Field type.
2141
+ * @property {Object} config - Configuration object.
2142
+ * @property {string} config.label - Label displayed in the editor.
2143
+ */
2144
+ type DateField = {
2145
+ type: "Date";
2146
+ config: {
2147
+ label: string;
2148
+ };
2149
+ };
2150
+
2151
+ /**
2152
+ * Represents a Timestamp Field in Prismic.
2153
+ * @property {"Timestamp"} type - Field type.
2154
+ * @property {Object} config - Configuration object.
2155
+ * @property {string} config.label - Label displayed in the editor.
2156
+ */
2157
+ type TimestampField = {
2158
+ type: "Timestamp";
2159
+ config: {
2160
+ label: string;
2161
+ };
2162
+ };
2163
+
2164
+ /**
2165
+ * Represents a Number Field in Prismic.
2166
+ * @property {"Number"} type - Field type.
2167
+ * @property {Object} config - Configuration object.
2168
+ * @property {string} config.label - Label displayed in the editor.
2169
+ * @property {string} [config.placeholder] - Placeholder text.
2170
+ * @property {number} [config.min] - Minimum allowable value.
2171
+ * @property {number} [config.max] - Maximum allowable value.
2172
+ */
2173
+ type NumberField = {
2174
+ type: "Number";
2175
+ config: {
2176
+ label: string;
2177
+ placeholder?: string;
2178
+ min?: number;
2179
+ max?: number;
2180
+ };
2181
+ };
2182
+
2183
+ /**
2184
+ * Represents a Text Field in Prismic.
2185
+ * @property {"Text"} type - Field type.
2186
+ * @property {Object} config - Configuration object.
2187
+ * @property {string} config.label - Label displayed in the editor.
2188
+ * @property {string} [config.placeholder] - Placeholder text.
2189
+ */
2190
+ type TextField = {
2191
+ type: "Text";
2192
+ config: {
2193
+ label: string;
2194
+ placeholder?: string;
2195
+ };
2196
+ };
2197
+
2198
+ /**
2199
+ * Represents a Select Field in Prismic.
2200
+ * @property {"Select"} type - Field type.
2201
+ * @property {Object} config - Configuration object.
2202
+ * @property {string} config.label - Label displayed in the editor.
2203
+ * @property {string[]} config.options - Array of options for the select dropdown.
2204
+ */
2205
+ type SelectField = {
2206
+ type: "Select";
2207
+ config: {
2208
+ label: string;
2209
+ options: string[];
2210
+ };
2211
+ };
2212
+
2213
+ /**
2214
+ * Represents a Structured Text Field in Prismic.
2215
+ * @property {"StructuredText"} type - Field type.
2216
+ * @property {Object} config - Configuration object.
2217
+ * @property {string} config.label - Label displayed in the editor.
2218
+ * @property {string} [config.placeholder] - Placeholder text.
2219
+ * @property {string} [config.single] - A comma-separated list of formatting options that does not allow line breaks. Options: paragraph | preformatted | heading1 | heading2 | heading3 | heading4 | heading5 | heading6 | strong | em | hyperlink | image | embed | list-item | o-list-item | rtl.
2220
+ * @property {string} [config.multi] - A comma-separated list of formatting options, with paragraph breaks allowed. Options: paragraph | preformatted | heading1 | heading2 | heading3 | heading4 | heading5 | heading6 | strong | em | hyperlink | image | embed | list-item | o-list-item | rtl.
2221
+ * @property {boolean} [config.allowTargetBlank] - Allows links to open in a new tab.
2222
+ * @property {string[]} [config.labels] - An array of strings to define labels for custom formatting.
2223
+ * @property {ImageConstraint} [config.imageConstraint] - Constraints for images within the rich text field.
2224
+ */
2225
+ type StructuredTextField = {
2226
+ type: "StructuredText";
2227
+ config: {
2228
+ label: string;
2229
+ placeholder?: string;
2230
+ single?: string;
2231
+ multi?: string;
2232
+ allowTargetBlank?: boolean;
2233
+ labels?: string[];
2234
+ imageConstraint?: ImageConstraint;
2235
+ };
2236
+ };
2237
+
2238
+ /**
2239
+ * Represents constraints for images within a rich text field.
2240
+ * @property {number} [width] - Width constraint in pixels.
2241
+ * @property {number
2242
+ * @property {number} [height] - Height constraint in pixels.
2243
+ */
2244
+ type ImageConstraint = {
2245
+ width?: number;
2246
+ height?: number;
2247
+ };
2248
+
2249
+ /**
2250
+ * Represents an Image Field in Prismic.
2251
+ * @property {"Image"} type - Field type.
2252
+ * @property {Object} config - Configuration object.
2253
+ * @property {string} config.label - Label displayed in the editor.
2254
+ * @property {Object} [config.constraint] - Constraints for the image dimensions.
2255
+ * @property {number} [config.constraint.width] - Width constraint.
2256
+ * @property {number} [config.constraint.height] - Height constraint.
2257
+ * @property {Thumbnail[]} [config.thumbnails] - Array of thumbnail configurations.
2258
+ */
2259
+ type ImageField = {
2260
+ type: "Image";
2261
+ config: {
2262
+ label: string;
2263
+ constraint?: {
2264
+ width?: number;
2265
+ height?: number;
2266
+ };
2267
+ thumbnails?: Thumbnail[];
2268
+ };
2269
+ };
2270
+
2271
+ /**
2272
+ * Represents a Thumbnail configuration for an Image field.
2273
+ * @property {string} name - Name of the thumbnail.
2274
+ * @property {number} [width] - Width of the thumbnail in pixels.
2275
+ * @property {number} [height] - Height of the thumbnail in pixels.
2276
+ */
2277
+ type Thumbnail = {
2278
+ name: string;
2279
+ width?: number;
2280
+ height?: number;
2281
+ };
2282
+
2283
+ /**
2284
+ * Represents a Link Field in Prismic.
2285
+ * @property {"Link"} type - Field type.
2286
+ * @property {Object} config - Configuration object.
2287
+ * @property {string} config.label - Label displayed in the editor.
2288
+ * @property {boolean} config.allowText - Enable the text field for the link.
2289
+ */
2290
+ type LinkField = {
2291
+ type: "Link";
2292
+ config: {
2293
+ label: string;
2294
+ allowText: boolean;
2295
+ };
2296
+ };
2297
+
2298
+ /**
2299
+ * Represents an Embed Field in Prismic.
2300
+ * @property {"Embed"} type - Field type.
2301
+ * @property {Object} config - Configuration object.
2302
+ * @property {string} config.label - Label displayed in the editor.
2303
+ */
2304
+ type EmbedField = {
2305
+ type: "Embed";
2306
+ config: {
2307
+ label: string;
2308
+ };
2309
+ };
2310
+
2311
+ /**
2312
+ * Represents a GeoPoint Field in Prismic.
2313
+ * @property {"GeoPoint"} type - Field type.
2314
+ * @property {Object} config - Configuration object.
2315
+ * @property {string} config.label - Label displayed in the editor.
2316
+ */
2317
+ type GeoPointField = {
2318
+ type: "GeoPoint";
2319
+ config: {
2320
+ label: string;
2321
+ };
2322
+ };
2323
+
2324
+ /**
2325
+ * Represents a Group Field (Repeatable Fields) in Prismic.
2326
+ * It CAN NEVER BE PUT INSIDE ANOTHER FIELD.
2327
+ * @property {"Group"} type - Field type.
2328
+ * @property {Object} config - Configuration object.
2329
+ * @property {string} config.label - Label displayed in the editor.
2330
+ * @property {Record<string, PrismicField>} config.fields - Defines the fields inside the group.
2331
+ */
2332
+ type GroupField = {
2333
+ type: "Group";
2334
+ config: {
2335
+ label: string;
2336
+ fields: Record<string, PrismicField>;
2337
+ };
2338
+ };
2339
+ `;
2340
+
2341
+ /**
2342
+ * Default slice model for the SharedSlice.
2343
+ */
2344
+ const DEFAULT_SLICE_MODEL: SharedSlice = {
2345
+ id: "<ID_TO_CHANGE>",
2346
+ type: "SharedSlice",
2347
+ name: "<NAME_TO_CHANGE>",
2348
+ description: "<DESCRIPTION_TO_CHANGE>",
2349
+ variations: [
2350
+ {
2351
+ id: "<VARIATION_ID_TO_CHANGE>",
2352
+ name: "<NAME_TO_CHANGE>",
2353
+ docURL: "...",
2354
+ version: "initial",
2355
+ description: "<DESCRIPTION_TO_CHANGE>",
2356
+ imageUrl: "",
2357
+ },
2358
+ ],
2359
+ };
2360
+
2361
+ /**
2362
+ * Calls the AI to generate the slice model.
2363
+ */
2364
+ async function generateSliceModel(
2365
+ sliceIndex: number,
2366
+ imageFile: Uint8Array,
2367
+ ): Promise<SharedSlice> {
2368
+ const systemPrompt = `
2369
+ You are an **expert in Prismic content modeling**. Using the **image and code provided**, generate a **valid Prismic JSON model** for the slice described below.
2370
+
2371
+ **STRICT MODELING RULES (NO EXCEPTIONS):**
2372
+ - **Use the TypeScript schema provided as your reference**.
2373
+ - **Absolutely all fields must be placed under the "primary" object**.
2374
+ - **Do not create groups or collections for single-image content** (background images must be a single image field).
2375
+ - **Ensure each field has appropriate placeholders, labels, and configurations**.
2376
+ - **Never generate a Link/Button text field—only the Link/Button field itself** with \`"allowText": true\`.
2377
+ - **Include all fields visible in the provided image**, do not forget any field and everything should be covered.
2378
+ - **Repeated fields must always be grouped**:
2379
+ - **Identify when field are part of a group**, when there is a repetition of a field or multiple fields together use a Group field.
2380
+ - **DO NOT** create individually numbered fields like \`feature1\`, \`feature2\`, \`feature3\`. Instead, define a single **Group field** (e.g., \`features\`) and move all repeated items inside it.
2381
+ - **Differentiate Prismic fields from decorative elements:**
2382
+ - If an element in the image is purely visual/decorative, **do not include it in the model**.
2383
+ - Use the **code as the source of truth** to determine what should be a field.
2384
+ - If an element is an image in the code, it **must also be an image field in Prismic** (strict 1:1 mapping).
2385
+ - **Handle repeated content correctly:**
2386
+ - **If an image, text, or link is repeated, always use a Group field**—do not create individual fields.
2387
+ - **If multiple fields are repeated together, they must be inside a single Group field**.
2388
+ - **Strictly forbid nesting of groups:**
2389
+ - **NEVER put a Group inside another Group field**.
2390
+ - **Group fields CANNOT be nested for any reason**—this is **strictly prohibited** even for navigation structures like headers or footers.
2391
+ - **Do not use the "items" field**:
2392
+ - **All repeatable fields must be defined as Group fields under "primary"**.
2393
+ - **"items" must never appear in the final JSON output**.
2394
+ - **Do not create more than one SliceVariation**, only one variation is enough to create the model.
2395
+
2396
+ **STRICT FIELD NAMING & CONTENT RULES:**
2397
+ - **Replace placeholders in the existing slice template** (\`<ID_TO_CHANGE>\`, \`<NAME_TO_CHANGE>\`, etc.).
2398
+ - **Field placeholders must be very short**—do **not** put actual image content inside placeholders.
2399
+ - **Field labels and IDs must define the field's purpose, not its content**.
2400
+ - **Slice name, ID, and description must describe the slice's function, not its content**.
2401
+ - The slice name and ID must be **generic and reusable**, defining what the slice **does**, not what it is used for.
2402
+ - **DO NOT name the slice after a specific topic, content type, or industry. Instead, name it based on its structure and function.
2403
+
2404
+ **STRICT JSON OUTPUT FORMAT (NO MARKDOWN OR EXTRA TEXT):**
2405
+ - **Return ONLY a valid JSON object**—no extra text, comments, or formatting.
2406
+ - **The response must be directly parseable** using \`JSON.parse(output)\`.
2407
+ - **Do not wrap the output in markdown (\`\`\`\`json\`) or any other formatting.**
2408
+
2409
+ **VALIDATION REQUIREMENT:**
2410
+ - Before returning, **validate that \`JSON.parse(output)\` runs without errors**.
2411
+ - If there is **any extra text, markdown, or incorrect structure**, **rewrite the response before returning**.
2412
+
2413
+ **REFERENCE SCHEMA (Follow this exactly):**
2414
+ ${SHARED_SLICE_SCHEMA}
2415
+
2416
+ **EXISTING SLICE TO UPDATE (Modify strictly according to the rules above):**
2417
+ ${JSON.stringify(DEFAULT_SLICE_MODEL)}
2418
+ `.trim();
2419
+
2420
+ const generatedModel = await callAI<SharedSlice>({
2421
+ ai: "OPENAI",
2422
+ sliceIndex,
2423
+ stepName: "MODEL",
2424
+ systemPrompt,
2425
+ imageFile,
2426
+ });
2427
+
2428
+ return generatedModel;
2429
+ }
2430
+
2431
+ /**
2432
+ * Calls the AI endpoint to generate mocks.
2433
+ */
2434
+ async function generateSliceMocks(
2435
+ sliceIndex: number,
2436
+ imageFile: Uint8Array,
2437
+ existingMocks: SharedSliceContent[],
2438
+ ): Promise<SharedSliceContent[]> {
2439
+ const systemPrompt = `
2440
+ You are a **seasoned frontend engineer** with **deep expertise in Prismic slices**.
2441
+ Your task is to **update the provided mocks template** based **only** on the visible text content in the provided image.
2442
+
2443
+ **STRICT UPDATE GUIDELINES:**
2444
+ - **Do no create content, only take visible text from the image.**
2445
+ - **Do not modify the overall structure of the mocks template.**
2446
+ - **Strictly update text content only.**
2447
+ - **Do not touch images or image-related fields.**
2448
+ - **If a repeated item appears in a group, match the exact number of group items seen in the image.**
2449
+ - **Do not modify metadata, field properties, or structure.** This includes:
2450
+ - Do **not** change the \`"key"\` property of links.
2451
+ - Do **not** modify \`StructuredText\` properties such as \`"direction"\`, \`"spans"\`, or \`"type"\`.
2452
+ - Do **not** alter field nesting or object structure.
2453
+ - **For StructuredText fields, maintain all existing structure and properties**—**only replace text content**.
2454
+ - **Ensure that only visible text in the image is updated**—do not generate or assume content.
2455
+ - **Never modify image fields**—image references and properties must remain unchanged.
2456
+
2457
+ **STRICT JSON OUTPUT FORMAT:**
2458
+ - **Return ONLY a valid JSON object**—no extra text, explanations, or formatting.
2459
+ - **The response must be directly parseable** using \`JSON.parse(output)\`.
2460
+ - **Do not wrap the output in markdown (\`\`\`\`json\`) or any other formatting.**
2461
+
2462
+ **VALIDATION REQUIREMENT:**
2463
+ - Before returning, **validate that \`JSON.parse(output)\` runs without errors**.
2464
+ - If there is **any extra text, markdown, or incorrect structure**, **rewrite the response before returning**.
2465
+
2466
+ **EXISTING MOCKS TEMPLATE (To be updated with the visible text from the image only):**
2467
+ ${JSON.stringify(existingMocks)}
2468
+ `.trim();
2469
+
2470
+ const updatedMock = await callAI<SharedSliceContent>({
2471
+ ai: "OPENAI",
2472
+ sliceIndex,
2473
+ stepName: "MOCKS",
2474
+ systemPrompt,
2475
+ imageFile,
2476
+ });
2477
+
2478
+ return [updatedMock];
2479
+ }
2480
+
2481
+ const SLICE_CODE_EXAMPLE = `
2482
+ -----------------------------------------------------------
2483
+ import { FC } from "react";
2484
+ import { Content } from "@prismicio/client";
2485
+ import { SliceComponentProps, PrismicRichText } from "@prismicio/react";
2486
+ import { PrismicNextImage, PrismicNextLink } from "@prismicio/next";
2487
+
2488
+ export type PascalNameToReplaceProps =
2489
+ SliceComponentProps<Content.PascalNameToReplaceSlice>;
2490
+
2491
+ const PascalNameToReplace: FC<PascalNameToReplaceProps> = ({ slice }) => {
2492
+ return (
2493
+ <section
2494
+ data-slice-type={slice.slice_type}
2495
+ data-slice-variation={slice.variation}
2496
+ className="es-bounded es-alternate-grid"
2497
+ >
2498
+ <PrismicNextLink
2499
+ className="es-alternate-grid__button"
2500
+ field={slice.primary.buttonLink}
2501
+ />
2502
+ <div className="es-alternate-grid__content">
2503
+ <PrismicNextImage
2504
+ field={slice.primary.image}
2505
+ className="es-alternate-grid__image"
2506
+ />
2507
+ <div className="es-alternate-grid__primary-content">
2508
+ <div className="es-alternate-grid__primary-content__intro">
2509
+ <p className="es-alternate-grid__primary-content__intro__eyebrow">
2510
+ {slice.primary.eyebrowHeadline}
2511
+ </p>
2512
+ <div className="es-alternate-grid__primary-content__intro__headline">
2513
+ <PrismicRichText field={slice.primary.title} />
2514
+ </div>
2515
+ <div className="es-alternate-grid__primary-content__intro__description">
2516
+ <PrismicRichText field={slice.primary.description} />
2517
+ </div>
2518
+ </div>
2519
+
2520
+ <div className="es-alternate-grid__primary-content__stats">
2521
+ {slice.primary.stats.map((stat, i) => (
2522
+ <div key={\`stat-$\{i + 1\}\`} className="es-alternate-grid__stat">
2523
+ <div className="es-alternate-grid__stat__heading">
2524
+ <PrismicRichText field={stat.title} />
2525
+ </div>
2526
+ <div className="es-alternate-grid__stat__description">
2527
+ <PrismicRichText field={stat.description} />
2528
+ </div>
2529
+ </div>
2530
+ ))}
2531
+ </div>
2532
+ </div>
2533
+ </div>
2534
+ </section>
2535
+ );
2536
+ };
2537
+
2538
+ export default PascalNameToReplace;
2539
+ -----------------------------------------------------------
2540
+ `.trim();
2541
+
2542
+ /**
2543
+ * Calls the AI endpoint to generate the slice React component.
2544
+ */
2545
+ async function generateSliceComponentCode(
2546
+ sliceIndex: number,
2547
+ imageFile: Uint8Array,
2548
+ updatedSlice: SharedSlice,
2549
+ ): Promise<string> {
2550
+ const systemPrompt = `
2551
+ You are a **seasoned frontend engineer** with **deep expertise in Prismic slices**.
2552
+ Your task is to generate a **fully isolated React component** for a Prismic slice, **focusing ONLY on structure (HTML) without styling**.
2553
+
2554
+ **STRICT STRUCTURAL GUIDELINES:**
2555
+ - **Do not include styling.** Focus **100% on correct structure**.
2556
+ - **Be self-contained.** The component must work in isolation.
2557
+ - **Follow the structure provided in the example.** Do not introduce **any variations**.
2558
+ - **Use all fields provided in the model**—do not omit or invent fields.
2559
+ - **Never access a field using** \`<field>.value\`. Always follow the example pattern with just \`<field>\`.
2560
+ - **Ensure correct mapping of field types:**
2561
+ - **StructuredText** → \`PrismicRichText\`
2562
+ - **Image field** → \`PrismicNextImage\`
2563
+ - **Text field** → Standard \`<p>\` element
2564
+ - **Link field** → \`PrismicNextLink\`
2565
+ - **Group field** → Map to the correct structure based on the example.
2566
+ - **Maintain W3C-compliant HTML.** Do not place \`PrismicRichText\` inside \`<h1>\`, \`<p>\`, or other invalid elements.
2567
+
2568
+ **PRISMIC COMPONENT USAGE RULES:**
2569
+ - **Links must use \`PrismicNextLink\`**, passing only the \`field\` (no manual text extraction).
2570
+ - **\`PrismicNextLink\` must never be opened manually**—pass the field directly as in the example.
2571
+ - **\`PrismicRichText\` cannot have a \`style\` prop**.
2572
+ - **Imports must be identical to the provided example**.
2573
+
2574
+ **STRICT JSON OUTPUT FORMAT**
2575
+ - **Return ONLY a valid JSON object** with **one key**: \`"componentCode"\`.
2576
+ - **No markdown (\`\`\`\`json\`), no comments, no text before or after—ONLY pure JSON**.
2577
+ - **The response MUST start with \`{\` and end with \`}\` exactly, do not start with a sentence explaining what you will do.**
2578
+ - **Ensure the output is directly parseable** with \`JSON.parse(output)\`.
2579
+ - **All strings must use double quotes (\`"\`).** Do not use single quotes or template literals.
2580
+ - **Escape all embedded double quotes (\`\"\`) and backslashes (\`\\\`).**
2581
+ - **The output must not contain raw control characters** (newline, tab, etc.); use escape sequences instead.
2582
+
2583
+ **Before returning, VALIDATE that \`JSON.parse(output)\` runs without errors.**
2584
+
2585
+ **EXAMPLE OF A FULLY ISOLATED SLICE COMPONENT (Follow this strictly):**
2586
+ ${SLICE_CODE_EXAMPLE}
2587
+
2588
+ **SLICE MODEL (Use this as the exact reference):**
2589
+ ${JSON.stringify(updatedSlice)}
2590
+ `.trim();
2591
+
2592
+ const parsed = await callAI<{ componentCode: string }>({
2593
+ ai: "AWS",
2594
+ sliceIndex,
2595
+ stepName: "CODE",
2596
+ systemPrompt,
2597
+ imageFile,
2598
+ });
2599
+
2600
+ if (!parsed.componentCode) {
2601
+ throw new Error("Missing key 'componentCode' in AI response.");
2602
+ }
2603
+
2604
+ return parsed.componentCode;
2605
+ }
2606
+
2607
+ async function generateSliceComponentCodeAppearance(
2608
+ sliceIndex: number,
2609
+ imageFile: Uint8Array,
2610
+ componentCode: string,
2611
+ ): Promise<string> {
2612
+ const systemPrompt = `
2613
+ You are a **seasoned frontend engineer** with **deep expertise in Prismic slices**.
2614
+ Your task is to **apply branding (appearance) strictly based on the provided image and code input**.
2615
+ The **branding is CRITICAL**—the slice you create **must perfectly match the visual appearance** of the provided slice image.
2616
+
2617
+ **STRICT GUIDELINES TO FOLLOW (NO EXCEPTIONS):**
2618
+ - **DO NOT** modify the structure of the code—**ONLY apply styling**. Your role is **purely styling-related**.
2619
+ - **NO external dependencies**—use **only inline** styling.
2620
+ - **VISUAL ACCURACY IS MANDATORY**—your goal is to make the output **visually identical** to the provided image.
2621
+
2622
+ **MUST strictly respect the following:**
2623
+ - **Background color** → Must **exactly match** the image. If unsure, **do not apply** any background color.
2624
+ - **Padding & margin** → Must be **pixel-perfect** per the provided image.
2625
+ - **Font size, color, and type** → Must match exactly. If the exact font is unavailable, choose the **closest possible match**.
2626
+ - **Typography precision** → If the font-family does not match, **the output is incorrect**.
2627
+ - **Color accuracy** → Use **ONLY the colors visible** in the provided image.
2628
+ - **Element positioning** → Elements **must be placed exactly** as seen in the provided image.
2629
+ - **Element sizes** → Every element **must match** the provided image in width, height, and proportions.
2630
+ - **Overall proportions** → The slice must maintain **identical proportions** to the provided image.
2631
+ - **Image constraints** → Images **must** maintain their **original aspect ratio**. Use explicit \`width\` and \`height\` constraints with an explicit pixels value. Avoid \`width: auto\`, \`height: auto\`, \`width: 100%\` or \`height: 100%\`.
2632
+ - **Repetitions & layout** → Ensure **consistent styling** across repeated items. The **layout direction (horizontal/vertical)** must match the image.
2633
+ - **Animations** → Handle animations as seen in the image, but **keep them fast and subtle** (avoid long animations).
2634
+
2635
+ **IMPORTANT RULES:**
2636
+ 1. **DO NOT modify any non-styling code**.
2637
+ - **Everything from the first import to the last export must remain unchanged**.
2638
+ - **Only add styling** on top of the existing structure.
2639
+
2640
+ 2. **STRICT JSON OUTPUT FORMAT**
2641
+ - Return a **valid JSON object** with **one key only**: \`"componentCode"\`.
2642
+ - **NO markdown, NO code blocks, NO text before or after**—only **pure JSON**.
2643
+ - The response **must start and end directly with \`{ "componentCode": ... }\`**.
2644
+ - Ensure the output is **directly parseable** using \`JSON.parse(output)\`.
2645
+
2646
+ 3. **INLINE \`<style>\` RULES**
2647
+ - Use **only inline** \`<style>\` tags (not \`<style jsx>\`).
2648
+ - Ensure **all CSS is valid** and matches the image precisely.
2649
+ - **Use backtick inside the \`<style>\` tag like this: <style>{\`...\`}</style>**.
2650
+ - **Do NOT escape the backtick (\`\`\`) inside the \`<style>\` tag**.
2651
+
2652
+ **Before returning, VALIDATE that \`JSON.parse(output)\` runs without errors.**
2653
+
2654
+ **EXISTING CODE (to apply branding on):**
2655
+ ${componentCode}
2656
+ `.trim();
2657
+
2658
+ const parsed = await callAI<{ componentCode: string }>({
2659
+ ai: "AWS",
2660
+ sliceIndex,
2661
+ stepName: "APPEARANCE",
2662
+ systemPrompt,
2663
+ imageFile,
2664
+ });
2665
+
2666
+ if (!parsed.componentCode) {
2667
+ throw new Error("Missing key 'componentCode' in AI response.");
2668
+ }
2669
+
2670
+ return parsed.componentCode;
2671
+ }
2672
+
2673
+ try {
2674
+ // Loop in parallel over each slice image and html code and generate the slice model, mocks and code.
2675
+ const updatedSlices = await Promise.all(
2676
+ args.sliceImages.map(async (sliceImage, index) => {
2677
+ // ----- Q1 scope -----
2678
+
2679
+ console.log(
2680
+ "STEP 1: Generate the slice model using the image for slice:",
2681
+ index,
2682
+ );
2683
+ const updatedSlice = await generateSliceModel(index, sliceImage);
2684
+
2685
+ console.log(
2686
+ "STEP 2: Persist the updated slice model for:",
2687
+ `${index} - ${updatedSlice.name}`,
2688
+ );
2689
+ await this.updateSlice({
2690
+ libraryID: DEFAULT_LIBRARY_ID,
2691
+ model: updatedSlice,
2692
+ });
2693
+
2694
+ console.log(
2695
+ "STEP 3: Update the slice screenshot for:",
2696
+ `${index} - ${updatedSlice.name}`,
2697
+ );
2698
+ await this.updateSliceScreenshot({
2699
+ libraryID: DEFAULT_LIBRARY_ID,
2700
+ sliceID: updatedSlice.id,
2701
+ variationID: updatedSlice.variations[0].id,
2702
+ data: Buffer.from(sliceImage),
2703
+ });
2704
+
2705
+ // ----- END Q1 scope -----
2706
+
2707
+ let updatedMock: SharedSliceContent[];
2708
+ try {
2709
+ console.log(
2710
+ "STEP 4: Generate updated mocks for:",
2711
+ `${index} - ${updatedSlice.name}`,
2712
+ );
2713
+ const existingMocks = mockSlice({ model: updatedSlice });
2714
+ updatedMock = await generateSliceMocks(
2715
+ index,
2716
+ sliceImage,
2717
+ existingMocks,
2718
+ );
2719
+ } catch (error) {
2720
+ console.error(
2721
+ `Failed to generate mocks for ${index} - ${updatedSlice.name}:`,
2722
+ error,
2723
+ );
2724
+ updatedMock = mockSlice({ model: updatedSlice });
2725
+ }
2726
+
2727
+ let componentCode: string | undefined;
2728
+ try {
2729
+ console.log(
2730
+ "STEP 5: Generate the isolated slice component code for:",
2731
+ `${index} - ${updatedSlice.name}`,
2732
+ );
2733
+ const initialCode = await generateSliceComponentCode(
2734
+ index,
2735
+ sliceImage,
2736
+ updatedSlice,
2737
+ );
2738
+
2739
+ console.log(
2740
+ "STEP 6: Generate the branding on the code:",
2741
+ `${index} - ${updatedSlice.name}`,
2742
+ );
2743
+ componentCode = await generateSliceComponentCodeAppearance(
2744
+ index,
2745
+ sliceImage,
2746
+ initialCode,
2747
+ );
2748
+ } catch (error) {
2749
+ console.error(
2750
+ `Failed to generate code for ${index} - ${updatedSlice.name}:`,
2751
+ error,
2752
+ );
2753
+ }
2754
+
2755
+ return { updatedSlice, componentCode, updatedMock };
2756
+ }),
2757
+ );
2758
+
2759
+ // Ensure to wait to have all slices code and mocks before writing on the disk
2760
+ await updatedSlices.forEach(
2761
+ async ({ updatedSlice, componentCode, updatedMock }, index) => {
2762
+ console.log(
2763
+ "STEP 7: Update the slice code for:",
2764
+ `${index} - ${updatedSlice.name}`,
2765
+ );
2766
+ if (componentCode) {
2767
+ const { errors } = await this.createSlice({
2768
+ libraryID: DEFAULT_LIBRARY_ID,
2769
+ model: updatedSlice,
2770
+ componentContents: componentCode,
2771
+ });
2772
+
2773
+ if (errors.length > 0) {
2774
+ console.log(
2775
+ `Errors while updating the slice code for ${index} - ${updatedSlice.name}:`,
2776
+ errors,
2777
+ );
2778
+ await this.createSlice({
2779
+ libraryID: DEFAULT_LIBRARY_ID,
2780
+ model: updatedSlice,
2781
+ });
2782
+ }
2783
+ } else {
2784
+ await this.createSlice({
2785
+ libraryID: DEFAULT_LIBRARY_ID,
2786
+ model: updatedSlice,
2787
+ });
2788
+ }
2789
+
2790
+ console.log(
2791
+ "STEP 8: Persist the generated mocks for:",
2792
+ `${index} - ${updatedSlice.name}`,
2793
+ );
2794
+ await this.updateSliceMocks({
2795
+ libraryID: DEFAULT_LIBRARY_ID,
2796
+ sliceID: updatedSlice.id,
2797
+ mocks: updatedMock,
2798
+ });
2799
+ },
2800
+ );
2801
+
2802
+ console.log("STEP 9: THE END");
2803
+
2804
+ return {
2805
+ slices: updatedSlices.map(({ updatedSlice }) => updatedSlice),
2806
+ };
2807
+ } catch (error) {
2808
+ console.error("Failed to generate slice:", error);
2809
+ throw new Error("Failed to generate slice: " + error);
2810
+ }
2811
+ }
1113
2812
  }