@alpaca-editor/core 1.0.4174 → 1.0.4176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/agents-view/AgentsView.js +0 -20
  2. package/dist/agents-view/AgentsView.js.map +1 -1
  3. package/dist/editor/QuickItemSwitcher.js +5 -1
  4. package/dist/editor/QuickItemSwitcher.js.map +1 -1
  5. package/dist/editor/ai/AgentTerminal.js +47 -83
  6. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  7. package/dist/editor/ai/Agents.js +7 -5
  8. package/dist/editor/ai/Agents.js.map +1 -1
  9. package/dist/editor/ai/AiResponseMessage.js +1 -1
  10. package/dist/editor/ai/AiResponseMessage.js.map +1 -1
  11. package/dist/editor/ai/ContextInfoBar.js +30 -64
  12. package/dist/editor/ai/ContextInfoBar.js.map +1 -1
  13. package/dist/editor/ai/useAgentStatus.js +81 -2
  14. package/dist/editor/ai/useAgentStatus.js.map +1 -1
  15. package/dist/editor/client/EditorShell.js +44 -7
  16. package/dist/editor/client/EditorShell.js.map +1 -1
  17. package/dist/editor/client/operations.js +30 -3
  18. package/dist/editor/client/operations.js.map +1 -1
  19. package/dist/editor/control-center/WebSocketMessages.js +0 -1
  20. package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
  21. package/dist/editor/page-viewer/PageViewerFrame.js +14 -3
  22. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  23. package/dist/editor/reviews/commentAi.js +43 -32
  24. package/dist/editor/reviews/commentAi.js.map +1 -1
  25. package/dist/editor/services/agentService.d.ts +8 -4
  26. package/dist/editor/services/agentService.js +30 -53
  27. package/dist/editor/services/agentService.js.map +1 -1
  28. package/dist/editor/services/aiService.d.ts +19 -1
  29. package/dist/editor/services/aiService.js +75 -2
  30. package/dist/editor/services/aiService.js.map +1 -1
  31. package/dist/page-wizard/steps/ContentStep.js +29 -57
  32. package/dist/page-wizard/steps/ContentStep.js.map +1 -1
  33. package/dist/page-wizard/steps/MetaDataStep.js +14 -23
  34. package/dist/page-wizard/steps/MetaDataStep.js.map +1 -1
  35. package/dist/page-wizard/steps/SelectStep.js +12 -42
  36. package/dist/page-wizard/steps/SelectStep.js.map +1 -1
  37. package/dist/revision.d.ts +2 -2
  38. package/dist/revision.js +2 -2
  39. package/dist/styles.css +0 -8
  40. package/package.json +1 -1
  41. package/src/agents-view/AgentsView.tsx +1 -21
  42. package/src/editor/QuickItemSwitcher.tsx +23 -19
  43. package/src/editor/ai/AgentTerminal.tsx +83 -142
  44. package/src/editor/ai/Agents.tsx +9 -6
  45. package/src/editor/ai/AiResponseMessage.tsx +5 -1
  46. package/src/editor/ai/ContextInfoBar.tsx +34 -75
  47. package/src/editor/ai/useAgentStatus.ts +82 -4
  48. package/src/editor/client/EditorShell.tsx +62 -9
  49. package/src/editor/client/operations.ts +42 -4
  50. package/src/editor/control-center/WebSocketMessages.tsx +5 -1
  51. package/src/editor/page-viewer/PageViewerFrame.tsx +15 -2
  52. package/src/editor/reviews/commentAi.ts +48 -36
  53. package/src/editor/services/agentService.ts +38 -55
  54. package/src/editor/services/aiService.ts +104 -3
  55. package/src/page-wizard/steps/ContentStep.tsx +47 -81
  56. package/src/page-wizard/steps/MetaDataStep.tsx +52 -60
  57. package/src/page-wizard/steps/SelectStep.tsx +13 -51
  58. package/src/revision.ts +2 -2
@@ -169,12 +169,16 @@ export type AgentContextData = {
169
169
  name?: string;
170
170
  };
171
171
  }>;
172
- /** @deprecated Use components instead to get page information per component */
173
- componentIds?: string[];
174
172
  field?: {
175
173
  fieldId: string;
176
- itemId: string;
177
- name?: string;
174
+ fieldName?: string;
175
+ item?: {
176
+ id: string;
177
+ language: string;
178
+ version: number;
179
+ path?: string;
180
+ name?: string;
181
+ };
178
182
  };
179
183
  comment?: {
180
184
  id: string;
@@ -842,71 +846,63 @@ export function canonicalizeAgentMetadata(
842
846
  }
843
847
  }
844
848
 
845
- // Process pages (legacy) and items
849
+ // Process items
846
850
  const allPages: any[] = [];
847
- if (Array.isArray(m.pages)) allPages.push(...m.pages); // legacy
848
851
  if (Array.isArray(m.items)) allPages.push(...m.items);
849
852
  for (const ctx of contextSources) {
850
- if (Array.isArray(ctx.pages)) allPages.push(...ctx.pages); // legacy
851
853
  if (Array.isArray(ctx.items)) allPages.push(...ctx.items);
852
854
  }
853
855
  if (allPages.length > 0) {
854
856
  const dedup: any[] = [];
855
857
  const seen = new Set<string>();
856
858
  for (const p of allPages) {
857
- const id = p?.id ?? p?.Id;
859
+ const id = p?.id;
858
860
  if (!id) continue;
859
- const lang = (p?.language ?? p?.Language ?? "").toString();
860
- const ver = (p?.version ?? p?.Version ?? "").toString();
861
+ const lang = (p?.language ?? "").toString();
862
+ const ver = (p?.version ?? "").toString();
861
863
  const key = `${String(id)}-${lang}-${ver}`.toLowerCase();
862
864
  if (seen.has(key)) continue;
863
865
  seen.add(key);
864
866
  dedup.push({
865
867
  id: String(id),
866
- language: p?.language ?? p?.Language,
867
- version: p?.version ?? p?.Version,
868
- name: p?.name ?? p?.Name,
869
- path: p?.path ?? p?.Path,
868
+ language: p?.language,
869
+ version: p?.version,
870
+ name: p?.name,
871
+ path: p?.path,
870
872
  });
871
873
  }
872
874
  m.items = dedup;
873
875
  } else {
874
- // Clear both if no items
875
876
  delete m.items;
876
877
  }
877
878
 
878
- // Delete legacy pages property to avoid sending both
879
- delete m.pages;
880
-
881
- // Process components with page info (new structure)
879
+ // Process components with page info
882
880
  const allComponents: any[] = [];
883
881
  if (Array.isArray(m.components)) allComponents.push(...m.components);
884
- if (Array.isArray(m.Components)) allComponents.push(...m.Components);
885
882
  for (const ctx of contextSources) {
886
883
  if (Array.isArray(ctx.components)) allComponents.push(...ctx.components);
887
- if (Array.isArray(ctx.Components)) allComponents.push(...ctx.Components);
888
884
  }
889
885
  if (allComponents.length > 0) {
890
886
  const dedup: any[] = [];
891
887
  const seen = new Set<string>();
892
888
  for (const c of allComponents) {
893
889
  if (!c) continue;
894
- const componentId = c.componentId ?? c.ComponentId ?? c.id ?? c.Id;
890
+ const componentId = c.componentId;
895
891
  if (!componentId || typeof componentId !== "string") continue;
896
892
  const key = componentId.toLowerCase();
897
893
  if (seen.has(key)) continue;
898
894
  seen.add(key);
899
895
 
900
- const pageItem = c.pageItem ?? c.PageItem;
896
+ const pageItem = c.pageItem;
901
897
  dedup.push({
902
898
  componentId: String(componentId),
903
899
  pageItem: pageItem
904
900
  ? {
905
- id: String(pageItem.id ?? pageItem.Id ?? ""),
906
- language: pageItem.language ?? pageItem.Language,
907
- version: pageItem.version ?? pageItem.Version,
908
- name: pageItem.name ?? pageItem.Name,
909
- path: pageItem.path ?? pageItem.Path,
901
+ id: String(pageItem.id ?? ""),
902
+ language: pageItem.language,
903
+ version: pageItem.version,
904
+ name: pageItem.name,
905
+ path: pageItem.path,
910
906
  }
911
907
  : undefined,
912
908
  });
@@ -914,35 +910,25 @@ export function canonicalizeAgentMetadata(
914
910
  m.components = dedup;
915
911
  }
916
912
 
917
- // Process componentIds (legacy - for backward compatibility)
918
- const allComponentIds: any[] = [];
919
- if (Array.isArray(m.componentIds)) allComponentIds.push(...m.componentIds);
913
+ // Process field - merge/complete from context sources if needed
920
914
  for (const ctx of contextSources) {
921
- if (Array.isArray(ctx.componentIds))
922
- allComponentIds.push(...ctx.componentIds);
923
- }
924
- if (allComponentIds.length > 0) {
925
- const ids = allComponentIds
926
- .map((x) => (typeof x === "string" ? x : (x?.id ?? x?.Id)))
927
- .filter((x): x is string => !!x && typeof x === "string");
928
- m.componentIds = Array.from(new Set(ids));
929
- }
930
-
931
- // Process field
932
- if (!m.field) {
933
- for (const ctx of contextSources) {
934
- if (ctx.field) {
915
+ if (ctx.field) {
916
+ // If field doesn't exist, create it from context
917
+ if (!m.field) {
935
918
  m.field = {
936
- fieldId:
937
- ctx.field.fieldId ??
938
- ctx.field.FieldId ??
939
- ctx.field.id ??
940
- ctx.field.Id,
941
- itemId: ctx.field.itemId ?? ctx.field.ItemId,
942
- name: ctx.field.name ?? ctx.field.Name,
919
+ fieldId: ctx.field.fieldId,
920
+ fieldName: ctx.field.fieldName,
921
+ item: ctx.field.item,
943
922
  };
944
923
  break;
945
924
  }
925
+ // If field exists but is incomplete, fill in missing properties
926
+ if (m.field && !m.field.item && ctx.field.item) {
927
+ m.field.item = ctx.field.item;
928
+ }
929
+ if (m.field && !m.field.fieldName && ctx.field.fieldName) {
930
+ m.field.fieldName = ctx.field.fieldName;
931
+ }
946
932
  }
947
933
  }
948
934
 
@@ -967,9 +953,6 @@ export function canonicalizeAgentMetadata(
967
953
  }
968
954
 
969
955
  m.additionalData = Object.keys(additional).length ? additional : undefined;
970
- if (Object.prototype.hasOwnProperty.call(m, "AdditionalData")) {
971
- delete m.AdditionalData;
972
- }
973
956
 
974
957
  return m as AgentContextData;
975
958
  }
@@ -86,7 +86,8 @@ type Message = {
86
86
 
87
87
  export interface ExecutePromptResponse {
88
88
  editOperations: any[];
89
- messages: Message[];
89
+ content: string;
90
+ messages?: Message[];
90
91
  numInputTokens: number;
91
92
  numOutputTokens: number;
92
93
  numCachedTokens: number;
@@ -184,23 +185,25 @@ export async function executePrompt(
184
185
  };
185
186
 
186
187
  // Use migrated endpoint under agent controller
187
- const endpoint = options.endpoint || "/alpaca/editor/agent/prompt";
188
+ const endpoint = options.endpoint || "/alpaca/editor/page-wizard/prompt";
188
189
 
189
190
  const response = await fetch(endpoint, finalRequestOptions);
190
191
 
191
192
  if (!response.ok) {
192
193
  // Always return rich error format
193
194
  const text = await response.text();
195
+ const errorContent = "There was an error processing your request: " + text;
194
196
  return {
195
197
  messages: [
196
198
  {
197
- content: "There was an error processing your request: " + text,
199
+ content: errorContent,
198
200
  name: "assistant",
199
201
  role: "assistant",
200
202
  id: crypto.randomUUID(),
201
203
  tool_calls: [],
202
204
  },
203
205
  ],
206
+ content: errorContent,
204
207
  editOperations: [],
205
208
  numInputTokens: 0,
206
209
  numOutputTokens: 0,
@@ -330,6 +333,104 @@ export async function executePrompt(
330
333
  return result;
331
334
  }
332
335
 
336
+ /**
337
+ * Helper function to parse JSON content from AI response
338
+ * Strips markdown code blocks if present and parses the JSON
339
+ * Handles incomplete markdown blocks during streaming
340
+ */
341
+ function parseJsonContent<T>(content: string): T {
342
+ let cleanedContent = content;
343
+
344
+ // Strip markdown code blocks if present
345
+ if (cleanedContent.startsWith("```json")) {
346
+ // Remove the opening ```json
347
+ cleanedContent = cleanedContent.substring(7).trim();
348
+ // Remove the closing ``` if present
349
+ if (cleanedContent.endsWith("```")) {
350
+ cleanedContent = cleanedContent
351
+ .substring(0, cleanedContent.length - 3)
352
+ .trim();
353
+ }
354
+ } else if (cleanedContent.startsWith("```")) {
355
+ // Remove the opening ```
356
+ cleanedContent = cleanedContent.substring(3).trim();
357
+ // Remove the closing ``` if present
358
+ if (cleanedContent.endsWith("```")) {
359
+ cleanedContent = cleanedContent
360
+ .substring(0, cleanedContent.length - 3)
361
+ .trim();
362
+ }
363
+ }
364
+
365
+ // Apply JsonCleaner to handle incomplete JSON during streaming
366
+ const cleanupResult = JsonCleaner.cleanupJson(cleanedContent);
367
+ console.log("CLEANED CONTENT: ", cleanupResult.cleanedJson);
368
+
369
+ return JSON.parse(cleanupResult.cleanedJson) as T;
370
+ }
371
+
372
+ /**
373
+ * Executes a prompt and returns the parsed JSON result
374
+ * @param messages - The messages to send
375
+ * @param context - The AI context
376
+ * @param options - Execution options
377
+ * @param requestOptions - Additional request options
378
+ * @param callback - Optional callback for streaming responses
379
+ * @param stream - Whether to stream the response
380
+ * @returns The parsed JSON object from the response content
381
+ * @throws Error if the response cannot be parsed or is not in the expected format
382
+ */
383
+ export async function executePromptWithJsonResult<T = any>(
384
+ messages: Message[],
385
+ context:
386
+ | AiContext
387
+ | {
388
+ editContext: EditContextType;
389
+ createAiContext: ({ editContext }: { editContext: any }) => AiContext;
390
+ },
391
+ options: ExecutePromptOptions = {},
392
+ requestOptions?: RequestInit,
393
+ callback?: (parsedJson: T) => void,
394
+ stream?: boolean,
395
+ ): Promise<T> {
396
+ // Wrap the callback to parse JSON before passing to the original callback
397
+ const wrappedCallback = callback
398
+ ? (response: any) => {
399
+ if (!response || !response.content) {
400
+ return;
401
+ }
402
+
403
+ try {
404
+ const parsed = parseJsonContent<T>(response.content);
405
+ callback(parsed);
406
+ } catch (parseError) {
407
+ console.error("Error parsing JSON in callback:", parseError);
408
+ }
409
+ }
410
+ : undefined;
411
+
412
+ const result = await executePrompt(
413
+ messages,
414
+ context,
415
+ options,
416
+ requestOptions,
417
+ wrappedCallback,
418
+ stream,
419
+ );
420
+
421
+ if (!result || !result.content) {
422
+ throw new Error("No content in response");
423
+ }
424
+
425
+ try {
426
+ return parseJsonContent<T>(result.content);
427
+ } catch (parseError) {
428
+ throw new Error(
429
+ `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
430
+ );
431
+ }
432
+ }
433
+
333
434
  // moved to searchService.ts
334
435
 
335
436
  export async function generateImage(
@@ -21,7 +21,10 @@ import { convertPageSchemaToWizardComponents } from "./schema";
21
21
  import { WizardPageModel } from "../PageWizard";
22
22
  import { createWizardAiContext, wipeComponents } from "../service";
23
23
 
24
- import { executePrompt } from "../../editor/services/aiService";
24
+ import {
25
+ executePrompt,
26
+ executePromptWithJsonResult,
27
+ } from "../../editor/services/aiService";
25
28
  import { usePageCreator } from "./usePageCreator";
26
29
  import { useThrottledCallback } from "use-debounce";
27
30
  import { Textarea } from "../../components/ui/textarea";
@@ -152,7 +155,10 @@ export function ContentStep({
152
155
 
153
156
  console.log("PROMPT (selectTargetItem):", promptContent);
154
157
 
155
- const result = await executePrompt(
158
+ const result = await executePromptWithJsonResult<{
159
+ id: string;
160
+ path: string;
161
+ }>(
156
162
  [
157
163
  {
158
164
  content: promptContent,
@@ -169,16 +175,11 @@ export function ContentStep({
169
175
  { signal: abortController.signal },
170
176
  (response) => {
171
177
  try {
172
- const folderResult = JSON.parse(response.content) as {
173
- id: string;
174
- path: string;
175
- };
176
-
177
- if (folderResult.id && folderResult.path) {
178
- setFolderSelectionResult(folderResult);
178
+ if (response.id && response.path) {
179
+ setFolderSelectionResult(response);
179
180
  // Update target folder with the selected folder
180
181
  setTargetFolder({
181
- id: folderResult.id,
182
+ id: response.id,
182
183
  language: language,
183
184
  } as ItemDescriptor);
184
185
  }
@@ -186,23 +187,13 @@ export function ContentStep({
186
187
  },
187
188
  );
188
189
 
189
- if (result && result.messages && result.messages.length > 0) {
190
- const lastMessage = result.messages[result.messages.length - 1];
191
- if (lastMessage && lastMessage.content) {
192
- const folderResult = JSON.parse(lastMessage.content) as {
193
- id: string;
194
- path: string;
195
- };
196
-
197
- if (folderResult.id && folderResult.path) {
198
- setFolderSelectionResult(folderResult);
199
- // Update target folder with the selected folder
200
- setTargetFolder({
201
- id: folderResult.id,
202
- language: language,
203
- } as ItemDescriptor);
204
- }
205
- }
190
+ if (result && result.id && result.path) {
191
+ setFolderSelectionResult(result);
192
+ // Update target folder with the selected folder
193
+ setTargetFolder({
194
+ id: result.id,
195
+ language: language,
196
+ } as ItemDescriptor);
206
197
  }
207
198
  } catch (error) {
208
199
  console.error("Error selecting target folder", error);
@@ -288,38 +279,30 @@ export function ContentStep({
288
279
 
289
280
  console.log("PROMPT (generatePageName):", promptContent);
290
281
 
291
- const result = await executePrompt(
292
- [
293
- {
294
- content: promptContent,
295
- name: "system",
296
- role: "system",
297
- id: crypto.randomUUID(),
298
- },
299
- ],
300
- { editContext, createAiContext: createWizardAiContext },
301
- { model: step.fields.aiModel || undefined },
302
- { signal: abortController.signal },
303
- (response) => {
304
- try {
305
- const newLayout = JSON.parse(response.content) as WizardPageModel;
306
- if (newLayout?.name) {
307
- setPageModel((prev) => ({ ...prev, name: newLayout.name }));
308
- }
309
- } catch (parseError: unknown) {}
310
- },
311
- );
282
+ const pageModelResult =
283
+ await executePromptWithJsonResult<WizardPageModel>(
284
+ [
285
+ {
286
+ content: `${processedInstructions?.trim()} Reply with a json object of type PageModel = { name: string; };
287
+ The language of the page is ${language}.`,
288
+ name: "system",
289
+ role: "system",
290
+ id: crypto.randomUUID(),
291
+ },
292
+ {
293
+ content: promptContent,
294
+ name: "user",
295
+ role: "user",
296
+ id: crypto.randomUUID(),
297
+ },
298
+ ],
299
+ { editContext, createAiContext: createWizardAiContext },
300
+ { model: step.fields.aiModel || undefined },
301
+ { signal: abortController.signal },
302
+ );
312
303
 
313
- if (result && result.messages && result.messages.length > 0) {
314
- const lastMessage = result.messages[result.messages.length - 1];
315
- if (lastMessage && lastMessage.content) {
316
- const pageModelResult = JSON.parse(
317
- lastMessage.content,
318
- ) as WizardPageModel;
319
- if (pageModelResult?.name) {
320
- setPageModel((prev) => ({ ...prev, name: pageModelResult.name }));
321
- }
322
- }
304
+ if (pageModelResult && pageModelResult.name) {
305
+ setPageModel((prev) => ({ ...prev, name: pageModelResult.name }));
323
306
  }
324
307
  } catch (error) {
325
308
  console.error("Error generating page name", error);
@@ -838,7 +821,7 @@ export function ContentStep({
838
821
 
839
822
  console.log("PROMPT: ", prompt, filteredSchema, existingPageModel);
840
823
 
841
- const result = await executePrompt(
824
+ const result = await executePromptWithJsonResult<WizardPageModel>(
842
825
  prompt,
843
826
  {
844
827
  editContext: editContextRef.current!,
@@ -850,33 +833,16 @@ export function ContentStep({
850
833
  endpoint: "/alpaca/editor/page-wizard/prompt",
851
834
  },
852
835
  { signal: localAbortController.signal },
853
- (response: any) => {
836
+ (response: WizardPageModel) => {
854
837
  try {
855
- // Handle streaming response - might have different structure during streaming
856
- const content =
857
- response.content ||
858
- response.messages?.[response.messages.length - 1]?.content;
859
- if (content) {
860
- const newLayout = JSON.parse(content) as WizardPageModel;
861
-
862
- if (newLayout) {
863
- setPageModel((prev) => mergeLayout(prev, newLayout));
864
- }
865
-
866
- setMessage(newLayout.message);
867
- }
838
+ setPageModel((prev) => mergeLayout(prev, response));
839
+ setMessage(response.message);
868
840
  } catch (parseError: unknown) {}
869
841
  },
870
842
  );
871
843
 
872
- if (result && result.messages && result.messages.length > 0) {
873
- const lastMessage = result.messages[result.messages.length - 1];
874
- if (lastMessage && lastMessage.content) {
875
- const finalLayout = JSON.parse(lastMessage.content);
876
- console.log("RESULT LAYOUT: ", finalLayout);
877
- setPageModel((prev) => mergeLayout(prev, finalLayout));
878
- }
879
- }
844
+ console.log("RESULT LAYOUT: ", result);
845
+ setPageModel((prev) => mergeLayout(prev, result));
880
846
  setStepCompleted(true);
881
847
  } catch (error) {
882
848
  console.error(error);
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { FullItem } from "../../editor/pageModel";
3
3
  import { WizardPageModel } from "../PageWizard";
4
- import { executePrompt } from "../../editor/services/aiService";
4
+ import { executePromptWithJsonResult } from "../../editor/services/aiService";
5
5
  import { createWizardAiContext } from "../service";
6
6
  import { useEditContext } from "../../editor/client/editContext";
7
7
  import { Textarea } from "../../components/ui/textarea";
@@ -24,7 +24,6 @@ export function MetaDataStep({
24
24
  setStepCompleted,
25
25
  }: StepComponentProps) {
26
26
  const [isGenerating, setIsGenerating] = useState(false);
27
- const [fullParentItem, setFullParentItem] = useState<FullItem>();
28
27
  const editContext = useEditContext();
29
28
 
30
29
  useEffect(() => {
@@ -35,15 +34,6 @@ export function MetaDataStep({
35
34
  }
36
35
  }, [editContext]);
37
36
 
38
- useEffect(() => {
39
- const loadParentItem = async () => {
40
- if (!parentItem) return;
41
- const item = await editContext?.itemsRepository.getItem(parentItem);
42
- setFullParentItem(item);
43
- };
44
- loadParentItem();
45
- }, [parentItem]);
46
-
47
37
  // Mark step as completed immediately since this is now just informational
48
38
  useEffect(() => {
49
39
  setStepCompleted(true);
@@ -87,56 +77,58 @@ export function MetaDataStep({
87
77
  try {
88
78
  const abortController = new AbortController();
89
79
 
90
- const result = await executePrompt(
91
- [
92
- {
93
- content: `${processedSystemInstructions}\nYou are a helpful assistant that generates SEO metadata for a page.\n\nReturn ONLY valid JSON in this exact shape: {"metaDescription": string, "metaKeywords": string}.\nLanguage: ${parentItem?.language || "en"}.`,
94
- name: "system",
95
- role: "system",
96
- id: crypto.randomUUID(),
97
- },
98
- {
99
- content: `${processedInstructions}\n\nCurrent data: ${JSON.stringify(
100
- inputData,
101
- null,
102
- 2,
103
- )}`,
104
- name: "user",
105
- role: "user",
106
- id: crypto.randomUUID(),
107
- },
108
- ],
109
- { editContext, createAiContext: createWizardAiContext },
110
- { model: step.fields.aiModel || undefined },
111
- { signal: abortController.signal },
112
- (response: any) => {
113
- try {
114
- const newLayout = JSON.parse(response.content) as WizardPageModel;
115
- if (newLayout && setPageModel) {
116
- setPageModel((prev) => ({
117
- ...prev,
118
- metaDescription:
119
- (newLayout as any)?.metaDescription ?? prev.metaDescription,
120
- metaKeywords:
121
- (newLayout as any)?.metaKeywords ?? prev.metaKeywords,
122
- }));
80
+ const pageModelResult =
81
+ await executePromptWithJsonResult<WizardPageModel>(
82
+ [
83
+ {
84
+ content: `${processedSystemInstructions}\nYou are a helpful assistant that generates SEO metadata for a page.\n\nReturn ONLY valid JSON in this exact shape: {"metaDescription": string, "metaKeywords": string}.\nLanguage: ${parentItem?.language || "en"}.`,
85
+ name: "system",
86
+ role: "system",
87
+ id: crypto.randomUUID(),
88
+ },
89
+ {
90
+ content: `${processedInstructions}\n\nCurrent data: ${JSON.stringify(
91
+ inputData,
92
+ null,
93
+ 2,
94
+ )}`,
95
+ name: "user",
96
+ role: "user",
97
+ id: crypto.randomUUID(),
98
+ },
99
+ ],
100
+ { editContext, createAiContext: createWizardAiContext },
101
+ { model: step.fields.aiModel || undefined },
102
+ { signal: abortController.signal },
103
+ (response: any) => {
104
+ try {
105
+ const newLayout = JSON.parse(response.content) as WizardPageModel;
106
+ if (newLayout && setPageModel) {
107
+ setPageModel((prev) => ({
108
+ ...prev,
109
+ metaDescription:
110
+ (newLayout as any)?.metaDescription ?? prev.metaDescription,
111
+ metaKeywords:
112
+ (newLayout as any)?.metaKeywords ?? prev.metaKeywords,
113
+ }));
114
+ }
115
+ } catch {
116
+ // Ignore parsing errors during streaming
123
117
  }
124
- } catch {}
125
- },
126
- );
127
-
128
- if (result && result.messages && result.messages.length > 0) {
129
- const lastMessage = result.messages[result.messages.length - 1];
130
- if (lastMessage && lastMessage.content) {
131
- const pageModelResult = JSON.parse(
132
- lastMessage.content,
133
- ) as WizardPageModel;
134
- setPageModel((prev) => ({
135
- ...prev,
136
- metaDescription: pageModelResult.metaDescription,
137
- metaKeywords: pageModelResult.metaKeywords,
138
- }));
139
- }
118
+ },
119
+ );
120
+
121
+ // Parse the final result
122
+ if (
123
+ pageModelResult &&
124
+ pageModelResult.metaDescription &&
125
+ pageModelResult.metaKeywords
126
+ ) {
127
+ setPageModel((prev) => ({
128
+ ...prev,
129
+ metaDescription: pageModelResult.metaDescription,
130
+ metaKeywords: pageModelResult.metaKeywords,
131
+ }));
140
132
  }
141
133
  } catch (error) {
142
134
  console.error("Error generating meta fields", error);