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