@alpaca-editor/core 1.0.3942 → 1.0.3943

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 (308) hide show
  1. package/.prettierrc +3 -0
  2. package/build.css +3 -0
  3. package/components.json +21 -0
  4. package/dist/editor/ContentTree.d.ts +2 -1
  5. package/dist/editor/ContentTree.js +23 -21
  6. package/dist/editor/ContentTree.js.map +1 -1
  7. package/dist/editor/FieldActionsOverlay.js +0 -2
  8. package/dist/editor/FieldActionsOverlay.js.map +1 -1
  9. package/dist/editor/ScrollingContentTree.js +1 -1
  10. package/dist/editor/ScrollingContentTree.js.map +1 -1
  11. package/dist/editor/Titlebar.js +1 -1
  12. package/dist/editor/Titlebar.js.map +1 -1
  13. package/dist/editor/ai/GhostWriter.js +24 -3
  14. package/dist/editor/ai/GhostWriter.js.map +1 -1
  15. package/dist/editor/client/EditorClient.js +7 -7
  16. package/dist/editor/client/EditorClient.js.map +1 -1
  17. package/dist/editor/field-types/InternalLinkFieldEditor.js +60 -10
  18. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  19. package/dist/editor/media-selector/MediaFolderBrowser.js +48 -1
  20. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  21. package/dist/editor/menubar/PageSelector.js +116 -65
  22. package/dist/editor/menubar/PageSelector.js.map +1 -1
  23. package/dist/editor/page-viewer/EditorForm.js +5 -2
  24. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  25. package/dist/editor/ui/ItemSearch.js +14 -8
  26. package/dist/editor/ui/ItemSearch.js.map +1 -1
  27. package/dist/editor/ui/PerfectTree.d.ts +4 -2
  28. package/dist/editor/ui/PerfectTree.js +78 -4
  29. package/dist/editor/ui/PerfectTree.js.map +1 -1
  30. package/dist/editor/ui/Splitter.js +1 -1
  31. package/dist/revision.d.ts +2 -2
  32. package/dist/revision.js +2 -2
  33. package/dist/styles.css +8 -2
  34. package/eslint.config.mjs +4 -0
  35. package/images/bg-shape-black.webp +0 -0
  36. package/images/wizard-bg.png +0 -0
  37. package/images/wizard-tour.png +0 -0
  38. package/images/wizard.png +0 -0
  39. package/package.json +2 -8
  40. package/src/client-components/api.ts +6 -0
  41. package/src/client-components/index.ts +19 -0
  42. package/src/components/ActionButton.tsx +50 -0
  43. package/src/components/Error.tsx +57 -0
  44. package/src/components/ui/CardConnector.tsx +56 -0
  45. package/src/components/ui/button.tsx +62 -0
  46. package/src/components/ui/card.tsx +372 -0
  47. package/src/components/ui/context-menu.tsx +250 -0
  48. package/src/config/config.tsx +917 -0
  49. package/src/config/types.ts +286 -0
  50. package/src/editor/ComponentInfo.tsx +90 -0
  51. package/src/editor/ConfirmationDialog.tsx +103 -0
  52. package/src/editor/ContentTree.tsx +733 -0
  53. package/src/editor/ContextMenu.tsx +230 -0
  54. package/src/editor/Editor.tsx +90 -0
  55. package/src/editor/EditorWarning.tsx +34 -0
  56. package/src/editor/EditorWarnings.tsx +33 -0
  57. package/src/editor/FieldActionsOverlay.tsx +296 -0
  58. package/src/editor/FieldEditorPopup.tsx +65 -0
  59. package/src/editor/FieldHistory.tsx +75 -0
  60. package/src/editor/FieldList.tsx +190 -0
  61. package/src/editor/FieldListField.tsx +391 -0
  62. package/src/editor/FieldListFieldWithFallbacks.tsx +217 -0
  63. package/src/editor/FloatingToolbar.tsx +163 -0
  64. package/src/editor/ImageEditor.tsx +128 -0
  65. package/src/editor/ItemInfo.tsx +90 -0
  66. package/src/editor/LinkEditorDialog.tsx +196 -0
  67. package/src/editor/MainLayout.tsx +95 -0
  68. package/src/editor/MobileLayout.tsx +68 -0
  69. package/src/editor/NewEditorClient.tsx +11 -0
  70. package/src/editor/PictureCropper.tsx +568 -0
  71. package/src/editor/PictureEditor.tsx +301 -0
  72. package/src/editor/PictureEditorDialog.tsx +381 -0
  73. package/src/editor/PublishDialog.ignore +74 -0
  74. package/src/editor/ScrollingContentTree.tsx +68 -0
  75. package/src/editor/Terminal.tsx +227 -0
  76. package/src/editor/Titlebar.tsx +104 -0
  77. package/src/editor/ai/AiPopup.tsx +59 -0
  78. package/src/editor/ai/AiResponseMessage.tsx +106 -0
  79. package/src/editor/ai/AiTerminal.tsx +503 -0
  80. package/src/editor/ai/AiToolCall.tsx +61 -0
  81. package/src/editor/ai/EditorAiTerminal.tsx +20 -0
  82. package/src/editor/ai/GhostWriter.tsx +480 -0
  83. package/src/editor/ai/aiPageModel.ts +108 -0
  84. package/src/editor/ai/editorAiContext.ts +18 -0
  85. package/src/editor/client/AboutDialog.tsx +44 -0
  86. package/src/editor/client/EditorClient.tsx +2241 -0
  87. package/src/editor/client/GenericDialog.tsx +50 -0
  88. package/src/editor/client/editContext.ts +416 -0
  89. package/src/editor/client/helpers.ts +44 -0
  90. package/src/editor/client/itemsRepository.ts +574 -0
  91. package/src/editor/client/operations.ts +768 -0
  92. package/src/editor/client/pageModelBuilder.ts +219 -0
  93. package/src/editor/commands/commands.ts +22 -0
  94. package/src/editor/commands/componentCommands.tsx +431 -0
  95. package/src/editor/commands/createVersionCommand.ts +33 -0
  96. package/src/editor/commands/deleteVersionCommand.ts +71 -0
  97. package/src/editor/commands/itemCommands.tsx +351 -0
  98. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
  99. package/src/editor/commands/localizeItem/LocalizeItemUtils.ts +27 -0
  100. package/src/editor/commands/undo.ts +39 -0
  101. package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
  102. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
  103. package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
  104. package/src/editor/component-designer/ComponentEditor.tsx +97 -0
  105. package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
  106. package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
  107. package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
  108. package/src/editor/component-designer/PlaceholdersEditor.tsx +179 -0
  109. package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
  110. package/src/editor/component-designer/TemplateEditor.tsx +236 -0
  111. package/src/editor/component-designer/aiContext.ts +23 -0
  112. package/src/editor/componentTreeHelper.tsx +116 -0
  113. package/src/editor/context-menu/CopyMoveMenu.tsx +103 -0
  114. package/src/editor/context-menu/InsertMenu.tsx +347 -0
  115. package/src/editor/control-center/About.tsx +342 -0
  116. package/src/editor/control-center/ControlCenterMenu.tsx +76 -0
  117. package/src/editor/control-center/IndexOverview.tsx +50 -0
  118. package/src/editor/control-center/IndexSettings.tsx +266 -0
  119. package/src/editor/control-center/Info.tsx +104 -0
  120. package/src/editor/control-center/QuotaInfo.tsx +301 -0
  121. package/src/editor/control-center/Status.tsx +113 -0
  122. package/src/editor/control-center/WebSocketMessages.tsx +155 -0
  123. package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
  124. package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
  125. package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
  126. package/src/editor/editor-warnings/NoWriteAccess.tsx +16 -0
  127. package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
  128. package/src/editor/field-types/AttachmentEditor.tsx +9 -0
  129. package/src/editor/field-types/CheckboxEditor.tsx +47 -0
  130. package/src/editor/field-types/DropLinkEditor.tsx +80 -0
  131. package/src/editor/field-types/DropListEditor.tsx +84 -0
  132. package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
  133. package/src/editor/field-types/InternalLinkFieldEditor.tsx +188 -0
  134. package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
  135. package/src/editor/field-types/MultiLineText.tsx +82 -0
  136. package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
  137. package/src/editor/field-types/RawEditor.tsx +53 -0
  138. package/src/editor/field-types/ReactQuill.tsx +580 -0
  139. package/src/editor/field-types/RichTextEditor.tsx +22 -0
  140. package/src/editor/field-types/RichTextEditorComponent.tsx +127 -0
  141. package/src/editor/field-types/SingleLineText.tsx +174 -0
  142. package/src/editor/field-types/TreeListEditor.tsx +261 -0
  143. package/src/editor/fieldTypes.ts +140 -0
  144. package/src/editor/media-selector/AiImageSearch.tsx +185 -0
  145. package/src/editor/media-selector/AiImageSearchPrompt.tsx +94 -0
  146. package/src/editor/media-selector/MediaFolderBrowser.tsx +321 -0
  147. package/src/editor/media-selector/MediaSelector.tsx +42 -0
  148. package/src/editor/media-selector/Preview.tsx +14 -0
  149. package/src/editor/media-selector/Thumbnails.tsx +48 -0
  150. package/src/editor/media-selector/TreeSelector.tsx +292 -0
  151. package/src/editor/media-selector/UploadZone.tsx +137 -0
  152. package/src/editor/media-selector/index.ts +8 -0
  153. package/src/editor/menubar/ActionsMenu.tsx +94 -0
  154. package/src/editor/menubar/ActiveUsers.tsx +17 -0
  155. package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
  156. package/src/editor/menubar/BrowseHistory.tsx +28 -0
  157. package/src/editor/menubar/ItemLanguageVersion.tsx +76 -0
  158. package/src/editor/menubar/LanguageSelector.tsx +226 -0
  159. package/src/editor/menubar/Menu.tsx +83 -0
  160. package/src/editor/menubar/NavButtons.tsx +74 -0
  161. package/src/editor/menubar/PageSelector.tsx +278 -0
  162. package/src/editor/menubar/PageViewerControls.tsx +120 -0
  163. package/src/editor/menubar/PreviewSecondaryControls.tsx +18 -0
  164. package/src/editor/menubar/SecondaryControls.tsx +45 -0
  165. package/src/editor/menubar/Separator.tsx +12 -0
  166. package/src/editor/menubar/SiteInfo.tsx +53 -0
  167. package/src/editor/menubar/User.tsx +27 -0
  168. package/src/editor/menubar/VersionSelector.tsx +142 -0
  169. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +307 -0
  170. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
  171. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +59 -0
  172. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
  173. package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
  174. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
  175. package/src/editor/page-editor-chrome/FrameMenu.tsx +338 -0
  176. package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
  177. package/src/editor/page-editor-chrome/InlineEditor.tsx +765 -0
  178. package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
  179. package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
  180. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +122 -0
  181. package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +161 -0
  182. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +169 -0
  183. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +315 -0
  184. package/src/editor/page-editor-chrome/SuggestionHighlighting.tsx +300 -0
  185. package/src/editor/page-editor-chrome/SuggestionHighlightings.tsx +40 -0
  186. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +828 -0
  187. package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
  188. package/src/editor/page-viewer/EditorForm.tsx +262 -0
  189. package/src/editor/page-viewer/MiniMap.tsx +362 -0
  190. package/src/editor/page-viewer/PageViewer.tsx +169 -0
  191. package/src/editor/page-viewer/PageViewerFrame.tsx +1022 -0
  192. package/src/editor/page-viewer/pageModelSkeletonBuilder.ts +412 -0
  193. package/src/editor/page-viewer/pageViewContext.ts +186 -0
  194. package/src/editor/pageModel.ts +220 -0
  195. package/src/editor/picture-shared.tsx +53 -0
  196. package/src/editor/reviews/Comment.tsx +308 -0
  197. package/src/editor/reviews/Comments.tsx +125 -0
  198. package/src/editor/reviews/DiffView.tsx +109 -0
  199. package/src/editor/reviews/PreviewInfo.tsx +35 -0
  200. package/src/editor/reviews/Reviews.tsx +280 -0
  201. package/src/editor/reviews/SuggestedEdit.tsx +316 -0
  202. package/src/editor/reviews/reviewCommands.tsx +47 -0
  203. package/src/editor/reviews/useReviews.tsx +70 -0
  204. package/src/editor/services/aiService.ts +173 -0
  205. package/src/editor/services/componentDesignerService.ts +151 -0
  206. package/src/editor/services/contentService.ts +180 -0
  207. package/src/editor/services/editService.ts +488 -0
  208. package/src/editor/services/indexService.ts +24 -0
  209. package/src/editor/services/reviewsService.ts +53 -0
  210. package/src/editor/services/serviceHelper.ts +95 -0
  211. package/src/editor/services/suggestedEditsService.ts +39 -0
  212. package/src/editor/services/systemService.ts +5 -0
  213. package/src/editor/services/translationService.ts +21 -0
  214. package/src/editor/services-server/api.ts +150 -0
  215. package/src/editor/services-server/graphQL.ts +106 -0
  216. package/src/editor/sidebar/ComponentPalette.tsx +161 -0
  217. package/src/editor/sidebar/ComponentTree.tsx +549 -0
  218. package/src/editor/sidebar/Debug.tsx +111 -0
  219. package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
  220. package/src/editor/sidebar/EditHistory.tsx +134 -0
  221. package/src/editor/sidebar/GraphQL.tsx +164 -0
  222. package/src/editor/sidebar/Insert.tsx +35 -0
  223. package/src/editor/sidebar/MainContentTree.tsx +102 -0
  224. package/src/editor/sidebar/Performance.tsx +53 -0
  225. package/src/editor/sidebar/Sessions.tsx +35 -0
  226. package/src/editor/sidebar/Sidebar.tsx +20 -0
  227. package/src/editor/sidebar/SidebarView.tsx +152 -0
  228. package/src/editor/sidebar/Translations.tsx +295 -0
  229. package/src/editor/sidebar/Validation.tsx +102 -0
  230. package/src/editor/sidebar/ViewSelector.tsx +60 -0
  231. package/src/editor/sidebar/Workbox.tsx +209 -0
  232. package/src/editor/ui/CenteredMessage.tsx +7 -0
  233. package/src/editor/ui/CopyMoveTargetSelectorDialog.tsx +81 -0
  234. package/src/editor/ui/CopyToClipboardButton.tsx +24 -0
  235. package/src/editor/ui/DialogButtons.tsx +11 -0
  236. package/src/editor/ui/Icons.tsx +709 -0
  237. package/src/editor/ui/ItemList.tsx +76 -0
  238. package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
  239. package/src/editor/ui/ItemSearch.tsx +159 -0
  240. package/src/editor/ui/PerfectTree.tsx +676 -0
  241. package/src/editor/ui/Section.tsx +35 -0
  242. package/src/editor/ui/SimpleIconButton.tsx +54 -0
  243. package/src/editor/ui/SimpleMenu.tsx +40 -0
  244. package/src/editor/ui/SimpleTable.tsx +60 -0
  245. package/src/editor/ui/SimpleTabs.tsx +60 -0
  246. package/src/editor/ui/SimpleToolbar.tsx +7 -0
  247. package/src/editor/ui/Spinner.tsx +9 -0
  248. package/src/editor/ui/Splitter.tsx +420 -0
  249. package/src/editor/ui/StackedPanels.tsx +134 -0
  250. package/src/editor/ui/Toolbar.tsx +7 -0
  251. package/src/editor/utils/id-helper.ts +3 -0
  252. package/src/editor/utils/insertOptions.ts +69 -0
  253. package/src/editor/utils/itemutils.ts +29 -0
  254. package/src/editor/utils/useMemoDebug.ts +28 -0
  255. package/src/editor/utils.ts +486 -0
  256. package/src/editor/views/CompareView.tsx +245 -0
  257. package/src/editor/views/EditView.tsx +27 -0
  258. package/src/editor/views/ItemEditor.tsx +58 -0
  259. package/src/editor/views/MediaFolderEditView.tsx +66 -0
  260. package/src/editor/views/SingleEditView.tsx +57 -0
  261. package/src/fonts/Geist-Black.woff2 +0 -0
  262. package/src/fonts/Geist-Bold.woff2 +0 -0
  263. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  264. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  265. package/src/fonts/Geist-Light.woff2 +0 -0
  266. package/src/fonts/Geist-Medium.woff2 +0 -0
  267. package/src/fonts/Geist-Regular.woff2 +0 -0
  268. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  269. package/src/fonts/Geist-Thin.woff2 +0 -0
  270. package/src/fonts/Geist[wght].woff2 +0 -0
  271. package/src/fonts/index.ts +10 -0
  272. package/src/index.ts +23 -0
  273. package/src/lib/safelist.tsx +16 -0
  274. package/src/lib/utils.ts +6 -0
  275. package/src/page-wizard/PageWizard.tsx +139 -0
  276. package/src/page-wizard/WizardBox.tsx +4 -0
  277. package/src/page-wizard/WizardBoxConnector.tsx +56 -0
  278. package/src/page-wizard/WizardSteps.tsx +458 -0
  279. package/src/page-wizard/service.ts +35 -0
  280. package/src/page-wizard/startPageWizardCommand.ts +26 -0
  281. package/src/page-wizard/steps/BuildPageStep.tsx +259 -0
  282. package/src/page-wizard/steps/CollectStep.tsx +296 -0
  283. package/src/page-wizard/steps/ComponentTypesSelector.tsx +454 -0
  284. package/src/page-wizard/steps/Components.tsx +193 -0
  285. package/src/page-wizard/steps/ContentStep.tsx +890 -0
  286. package/src/page-wizard/steps/EditButton.tsx +34 -0
  287. package/src/page-wizard/steps/FieldEditor.tsx +102 -0
  288. package/src/page-wizard/steps/Generate.tsx +60 -0
  289. package/src/page-wizard/steps/ImagesStep.tsx +382 -0
  290. package/src/page-wizard/steps/LayoutStep.tsx +227 -0
  291. package/src/page-wizard/steps/MetaDataStep.tsx +173 -0
  292. package/src/page-wizard/steps/SelectStep.tsx +281 -0
  293. package/src/page-wizard/steps/schema.ts +180 -0
  294. package/src/page-wizard/steps/usePageCreator.ts +325 -0
  295. package/src/page-wizard/usePageWizard.ts +79 -0
  296. package/src/revision.ts +2 -0
  297. package/src/splash-screen/NewPage.tsx +294 -0
  298. package/src/splash-screen/OpenPage.tsx +113 -0
  299. package/src/splash-screen/RecentPages.tsx +123 -0
  300. package/src/splash-screen/SectionHeadline.tsx +21 -0
  301. package/src/splash-screen/SplashScreen.tsx +195 -0
  302. package/src/tour/Tour.tsx +566 -0
  303. package/src/tour/default-tour.tsx +301 -0
  304. package/src/tour/preview-tour.tsx +128 -0
  305. package/src/types.ts +335 -0
  306. package/styles.css +765 -0
  307. package/tsconfig.build.json +31 -0
  308. package/tsconfig.json +14 -0
@@ -0,0 +1,890 @@
1
+ import { StepComponentProps } from "../../config/types";
2
+
3
+ import { useEffect, useState, useCallback, useRef } from "react";
4
+ import { getChildren } from "../../editor/services/contentService";
5
+ import { getItemDescriptor } from "../../editor/utils";
6
+ import { PageViewer } from "../../editor/page-viewer/PageViewer";
7
+ import {
8
+ useEditContext,
9
+ useEditContextRef,
10
+ useModifiedFieldsContext,
11
+ } from "../../editor/client/editContext";
12
+ import { ExecutionResult } from "../../editor/services/serviceHelper";
13
+
14
+ import {
15
+ ComponentTypeSelector,
16
+ getComponentTypeSelectorSummary,
17
+ } from "./ComponentTypesSelector";
18
+ import { ActionButton } from "../../components/ActionButton";
19
+ import { convertPageSchemaToWizardComponents } from "./schema";
20
+ import { WizardPageModel } from "../PageWizard";
21
+ import { createWizardAiContext, wipeComponents } from "../service";
22
+
23
+ import { executePrompt } from "../../editor/services/aiService";
24
+ import { usePageCreator } from "./usePageCreator";
25
+ import { useThrottledCallback } from "use-debounce";
26
+ import { InputTextarea } from "primereact/inputtextarea";
27
+ import { InputText } from "primereact/inputtext";
28
+ import { LanguageSelector } from "../../editor/menubar/LanguageSelector";
29
+
30
+ import { convertToAiPageModel } from "../../editor/ai/aiPageModel";
31
+ import { WizardBox } from "../WizardBox";
32
+ import { Wand2, PanelsTopLeft, Settings, FileText } from "lucide-react";
33
+ import { cn } from "../../lib/utils";
34
+ import { WizardBoxConnector } from "../WizardBoxConnector";
35
+ import { ItemDescriptor, FullItem } from "../../editor/pageModel";
36
+ import Generate from "./Generate";
37
+
38
+ export function ContentStep({
39
+ wizard,
40
+ step,
41
+ parentItem,
42
+ pageModel,
43
+ setPageModel,
44
+ data,
45
+ setData,
46
+ internalState,
47
+ setInternalState,
48
+ setStepCompleted,
49
+ }: StepComponentProps) {
50
+ const [pageLoaded, setPageLoaded] = useState(false);
51
+ const pageLoadedRef = useRef(false);
52
+ const [isLoading, setIsLoading] = useState(false);
53
+ const [availableComponentTypes, setAvailableComponentTypes] = useState<
54
+ string[]
55
+ >([]);
56
+
57
+ const editContext = useEditContext();
58
+ const editContextRef = useEditContextRef();
59
+ const [abortController, setAbortController] = useState<AbortController>();
60
+ const [message, setMessage] = useState<string>();
61
+ const [isCreatingComponents, setIsCreatingComponents] = useState(false);
62
+ const [pageItem, setPageItem] = useState<ItemDescriptor>();
63
+
64
+ // New state for name and language functionality
65
+ const [isGenerating, setIsGenerating] = useState(false);
66
+ const [language, setLanguage] = useState<string>(
67
+ parentItem?.language || "en",
68
+ );
69
+ const [fullParentItem, setFullParentItem] = useState<FullItem>();
70
+ const [nameValidation, setNameValidation] = useState<{
71
+ isValid: boolean;
72
+ message?: string;
73
+ }>({ isValid: false });
74
+
75
+ const modifiedFieldsContext = useModifiedFieldsContext();
76
+
77
+ const pageCreator = usePageCreator(pageItem, wizard, setPageModel);
78
+
79
+ if (!parentItem) return "No parent item";
80
+
81
+ const checkPageName = useCallback(async () => {
82
+ if (!parentItem) {
83
+ setNameValidation({ isValid: false, message: "No parent item" });
84
+ return false;
85
+ }
86
+
87
+ // Check if page name is valid
88
+ if (!pageModel.name || pageModel.name.trim().length < 3) {
89
+ setNameValidation({
90
+ isValid: false,
91
+ message: "Page name must be at least 3 characters long",
92
+ });
93
+ return false;
94
+ }
95
+
96
+ try {
97
+ // Check if page with same name already exists
98
+ const children = await getChildren(
99
+ parentItem.id,
100
+ editContext?.sessionId ?? "",
101
+ [],
102
+ false,
103
+ editContext?.contentEditorItem?.language || "en",
104
+ );
105
+
106
+ if (
107
+ children.find(
108
+ (x) =>
109
+ x.name.toLocaleLowerCase() ===
110
+ pageModel.name.trim().toLocaleLowerCase(),
111
+ )
112
+ ) {
113
+ setNameValidation({
114
+ isValid: false,
115
+ message: "A page with this name already exists",
116
+ });
117
+ return false;
118
+ }
119
+
120
+ setNameValidation({ isValid: true });
121
+ return true;
122
+ } catch (error) {
123
+ setNameValidation({
124
+ isValid: false,
125
+ message: "Error checking page name",
126
+ });
127
+ return false;
128
+ }
129
+ }, [
130
+ parentItem,
131
+ pageModel.name,
132
+ editContext?.sessionId,
133
+ editContext?.contentEditorItem?.language,
134
+ ]);
135
+
136
+ const handleInputChange = (field: keyof typeof pageModel, value: string) => {
137
+ const updatedPageModel = {
138
+ ...pageModel,
139
+ [field]: value,
140
+ };
141
+
142
+ setPageModel(updatedPageModel);
143
+ };
144
+
145
+ const createPageIfNeeded = useCallback(async (): Promise<boolean> => {
146
+ if (!editContext || !parentItem || pageItem) return true; // Page already exists
147
+
148
+ try {
149
+ if (!(await checkPageName())) {
150
+ console.error("Page name validation failed");
151
+ return false;
152
+ }
153
+
154
+ const result = await editContext.operations.createItem(
155
+ {
156
+ ...getItemDescriptor(parentItem),
157
+ language: parentItem.language || "en",
158
+ },
159
+ wizard.templateId,
160
+ pageModel.name,
161
+ );
162
+
163
+ if (result) {
164
+ editContext?.loadItem(result, { addToBrowseHistory: true });
165
+ const item = await editContext?.itemsRepository.getItem(result);
166
+ if (item && setPageItem) {
167
+ setPageItem(item);
168
+ return true;
169
+ } else {
170
+ console.error("Failed to load newly created item", result);
171
+ return false;
172
+ }
173
+ }
174
+ return false;
175
+ } catch (error) {
176
+ console.error("Error creating page", error);
177
+ return false;
178
+ }
179
+ }, [
180
+ editContext,
181
+ parentItem,
182
+ pageItem,
183
+ checkPageName,
184
+ wizard.templateId,
185
+ pageModel.name,
186
+ setPageItem,
187
+ ]);
188
+
189
+ useEffect(() => {
190
+ editContext?.setMode("edit");
191
+ if (!internalState.layoutInstructions) {
192
+ setInternalState({
193
+ ...internalState,
194
+ layoutInstructions: step.fields.instructions,
195
+ });
196
+ }
197
+
198
+ // Extract available component types from schema
199
+ if (wizard.schema) {
200
+ try {
201
+ const schema =
202
+ typeof wizard.schema === "string"
203
+ ? JSON.parse(wizard.schema)
204
+ : wizard.schema;
205
+
206
+ // Extract component types (reusing logic from ComponentTypeSelector)
207
+ const extractComponentTypes = (items: any): string[] => {
208
+ if (!items || !Array.isArray(items)) return [];
209
+ let types: string[] = [];
210
+ for (const item of items) {
211
+ if ("type" in item) {
212
+ types.push(item.type);
213
+ }
214
+ if ("placeholders" in item && Array.isArray(item.placeholders)) {
215
+ for (const placeholder of item.placeholders) {
216
+ if (
217
+ placeholder.components &&
218
+ Array.isArray(placeholder.components)
219
+ ) {
220
+ types = [
221
+ ...types,
222
+ ...extractComponentTypes(placeholder.components),
223
+ ];
224
+ }
225
+ }
226
+ }
227
+ if ("components" in item && Array.isArray(item.components)) {
228
+ types = [...types, ...extractComponentTypes(item.components)];
229
+ }
230
+ }
231
+ return types;
232
+ };
233
+
234
+ const componentTypes = extractComponentTypes(schema);
235
+ const uniqueTypes = Array.from(new Set(componentTypes)).sort();
236
+ setAvailableComponentTypes(uniqueTypes);
237
+
238
+ // Initialize selected component types if not already set
239
+ if (
240
+ !data.selectedComponentTypes ||
241
+ !Array.isArray(data.selectedComponentTypes)
242
+ ) {
243
+ // Parse preselected components from step if available
244
+ let stepPreselectedTypes: string[] = [];
245
+ if (step.fields["preselectedComponents"]) {
246
+ stepPreselectedTypes = step.fields["preselectedComponents"]
247
+ .split(/[,\n\r]+/)
248
+ .map((type: string) => type.trim())
249
+ .filter((type: string) => type && uniqueTypes.includes(type));
250
+ }
251
+
252
+ // Set initial selection
253
+ const initialSelection =
254
+ stepPreselectedTypes.length > 0
255
+ ? stepPreselectedTypes
256
+ : uniqueTypes;
257
+
258
+ setData({
259
+ ...data,
260
+ selectedComponentTypes: initialSelection,
261
+ });
262
+ }
263
+ } catch (error) {
264
+ console.error("Error extracting component types:", error);
265
+ }
266
+ }
267
+ }, []);
268
+
269
+ // Check page name whenever it changes
270
+ useEffect(() => {
271
+ if (pageModel.name && !pageItem) {
272
+ checkPageName();
273
+ }
274
+ }, [pageModel.name, checkPageName]);
275
+
276
+ // Load parent item details
277
+ useEffect(() => {
278
+ const loadParentItem = async () => {
279
+ if (!parentItem) return;
280
+ const item = await editContext?.itemsRepository.getItem(parentItem);
281
+ setFullParentItem(item);
282
+ };
283
+ loadParentItem();
284
+ }, [parentItem]);
285
+
286
+ // Auto-generate page name and meta description if not set
287
+ useEffect(() => {
288
+ if (!editContext) return;
289
+
290
+ const generatePageName = async () => {
291
+ const metaInstructions =
292
+ step.fields["Instructions for generating item name and meta fields"];
293
+
294
+ setIsGenerating(true);
295
+
296
+ try {
297
+ const abortController = new AbortController();
298
+
299
+ const result = await executePrompt(
300
+ [
301
+ {
302
+ content: `${metaInstructions?.trim()} Reply with a json object of type PageModel = { name: string; };
303
+ The item name should be a valid sitecore item name. Spaces are allowed but no special characters or Umlaute.
304
+ The language of the page is ${language}.
305
+ Input data: ${JSON.stringify(data)}`,
306
+ name: "system",
307
+ role: "system",
308
+ },
309
+ ],
310
+ editContext,
311
+ createWizardAiContext,
312
+ {},
313
+ { signal: abortController.signal },
314
+ "gpt-4.1",
315
+ (response) => {
316
+ try {
317
+ const newLayout = JSON.parse(response.content) as WizardPageModel;
318
+
319
+ setPageModel({
320
+ ...pageModel,
321
+ name: newLayout.name,
322
+ });
323
+ } catch (parseError: unknown) {}
324
+ },
325
+ );
326
+
327
+ const pageModelResult = JSON.parse(result.content) as WizardPageModel;
328
+ setPageModel({
329
+ ...pageModel,
330
+ name: pageModelResult.name,
331
+ });
332
+ } catch (error) {
333
+ console.error("Error generating page name", error);
334
+ } finally {
335
+ setIsGenerating(false);
336
+ }
337
+ };
338
+
339
+ if (!pageModel.name) generatePageName();
340
+ }, []);
341
+
342
+ const createComponents = async () => {
343
+ if (!pageLoaded) return;
344
+ if (pageModel?.components && !isCreatingComponents) {
345
+ try {
346
+ setIsCreatingComponents(true);
347
+
348
+ await pageCreator.createComponentsRecursively(
349
+ pageModel.components,
350
+ "root",
351
+ );
352
+ setInternalState((prev: any) => ({
353
+ ...prev,
354
+ componentsCreated: true,
355
+ }));
356
+ } finally {
357
+ setIsCreatingComponents(false);
358
+ }
359
+ }
360
+ };
361
+
362
+ useEffect(() => {
363
+ if (
364
+ internalState.componentsCreated &&
365
+ !isCreatingComponents &&
366
+ !isLoading
367
+ ) {
368
+ editContext?.requestRefresh("immediate");
369
+ }
370
+ }, [isCreatingComponents, isLoading]);
371
+
372
+ const createComponentsThrottled = useThrottledCallback(createComponents, 300);
373
+
374
+ useEffect(() => {
375
+ createComponentsThrottled();
376
+ }, [pageModel]);
377
+
378
+ // useEffect(() => {
379
+ // if (!pageItem) {
380
+ // setPageItem(pageItem);
381
+ // }
382
+ // }, [pageItem]);
383
+
384
+ useEffect(() => {
385
+ //console.log("$$$$ Active field actions: ", editContext?.activeFieldActions);
386
+ if (
387
+ editContext &&
388
+ editContext.activeFieldActions &&
389
+ !editContext.activeFieldActions.find((x) => x.state === "running")
390
+ ) {
391
+ console.log("$$$ Refreshing!");
392
+ editContext.requestRefresh("immediate");
393
+ }
394
+ }, [editContext?.activeFieldActions]);
395
+
396
+ useEffect(() => {
397
+ if (
398
+ editContext &&
399
+ editContext.page &&
400
+ editContext.page.item &&
401
+ pageItem &&
402
+ pageItem.id === editContext.page.item.descriptor.id
403
+ ) {
404
+ if (!pageLoaded) {
405
+ setTimeout(() => {
406
+ setPageLoaded(true);
407
+ pageLoadedRef.current = true;
408
+ }, 1000);
409
+ }
410
+ }
411
+ }, [editContext?.page, pageItem]);
412
+
413
+ const createLayout = async () => {
414
+ try {
415
+ if (!editContext) return;
416
+ setMessage("");
417
+ setIsLoading(true);
418
+ setStepCompleted(false);
419
+
420
+ // Create page if it doesn't exist yet
421
+ let currentPageItem = pageItem;
422
+ // Wait for pageLoaded to become true before continuing
423
+ const waitForPageLoaded = async (): Promise<boolean> => {
424
+ const maxWaitTime = 10000; // 10 seconds max wait
425
+ const pollInterval = 100; // Check every 100ms
426
+ let elapsed = 0;
427
+
428
+ while (elapsed < maxWaitTime) {
429
+ // Check if page is loaded using the ref which gets updated by the useEffect
430
+ if (pageLoadedRef.current) {
431
+ return true;
432
+ }
433
+
434
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
435
+ elapsed += pollInterval;
436
+ }
437
+
438
+ return false;
439
+ };
440
+
441
+ if (!currentPageItem) {
442
+ const pageCreated = await createPageIfNeeded();
443
+ if (!pageCreated) {
444
+ setMessage(
445
+ "Failed to create page. Please check the page name and try again.",
446
+ );
447
+ setIsLoading(false);
448
+ return;
449
+ }
450
+
451
+ const isPageLoaded = await waitForPageLoaded();
452
+ if (!isPageLoaded) {
453
+ setMessage("Page creation timed out. Please try again.");
454
+ setIsLoading(false);
455
+ return;
456
+ }
457
+ }
458
+
459
+ if (internalState.componentsCreated) {
460
+ console.log("Wiping components");
461
+ if (currentPageItem) {
462
+ const wipeResult: ExecutionResult<any> = await wipeComponents(
463
+ getItemDescriptor(currentPageItem),
464
+ );
465
+ if (wipeResult.type !== "success") {
466
+ console.error("Failed to wipe components:", wipeResult);
467
+ setMessage(
468
+ wipeResult.summary ||
469
+ "Failed to clear existing components. Please try again.",
470
+ );
471
+ setIsLoading(false);
472
+ return;
473
+ }
474
+ }
475
+ console.log("Wiping components 2");
476
+ editContextRef.current?.itemsRepository.clear();
477
+ modifiedFieldsContext?.clear();
478
+ setInternalState((prev: any) => ({
479
+ ...prev,
480
+ componentsCreated: false,
481
+ }));
482
+
483
+ setPageModel((prev) => ({
484
+ ...prev,
485
+ components: [],
486
+ message: "",
487
+ }));
488
+
489
+ pageLoadedRef.current = false;
490
+
491
+ console.log("Wiping components 3");
492
+ editContextRef.current?.requestRefresh("immediate");
493
+ await waitForPageLoaded();
494
+ console.log("Wiping components 4");
495
+ }
496
+
497
+ // Parse schema if it's a string
498
+ const schema =
499
+ typeof wizard.schema === "string"
500
+ ? JSON.parse(wizard.schema)
501
+ : wizard.schema;
502
+
503
+ // Filter the schema based on selected component types and placeholders
504
+ const filteredSchema = convertPageSchemaToWizardComponents(
505
+ schema,
506
+ data.selectedComponentTypes,
507
+ );
508
+
509
+ const localAbortController = new AbortController();
510
+ setAbortController(localAbortController);
511
+
512
+ // Get the existing page model
513
+ const existingPageModel = await convertToAiPageModel(
514
+ editContextRef.current?.page!,
515
+ editContextRef.current!,
516
+ );
517
+
518
+ console.log("Existing page model: ", existingPageModel);
519
+
520
+ const prompt = [
521
+ {
522
+ content: `${internalState.layoutInstructions ? internalState.layoutInstructions?.trim() : ""} Reply with a json object of type PageModel = { components: Component[]; message: string; };
523
+ Component = { id: string | undefined, name: string, type: string; fields: Field[]; placeholder?: string; children?: Component[]; };
524
+ Field = { name: string; value: string; type: string; };
525
+ Generate a descriptive name for each component including the topic.
526
+ Only use component types that are in the page schema.
527
+ Keep existing components with their ids. Leave id field empty for new components.
528
+ Existing page model: ${JSON.stringify(existingPageModel)}
529
+ Fill empty fields of existing components before you insert new components.
530
+ Component types: ${JSON.stringify(
531
+ filteredSchema,
532
+ )} Root level component types ${filteredSchema
533
+ .filter((c) => c.allowedOnRoot)
534
+ .map((c) => c.type)
535
+ .join(", ")}
536
+ Tell the user your reasoning in the message field.
537
+ If the user provided image ids, fill them into picture / image fields with an "id:" prefix. The image id needs to be guid.
538
+ If you dont have a matching image id, provide a description of a picture that you would like to use with a "generate:" prefix instead.
539
+ In any case the field value must start with "id:" or "generate:", nothing else. Do not inject json into picture fields even if the existing value is json.
540
+ The language of the page is ${editContextRef.current!.page!.item!.language}.
541
+ Input data: ${JSON.stringify(data)}`,
542
+
543
+ name: "system",
544
+ role: "system",
545
+ },
546
+ ];
547
+
548
+ console.log("PAGE WIZARD PROMPT: ", prompt);
549
+
550
+ const result = await executePrompt(
551
+ prompt,
552
+ editContextRef.current!,
553
+ createWizardAiContext,
554
+ { allowedFunctions: [] },
555
+ { signal: localAbortController.signal },
556
+ step.fields.aiModel || "gpt-4.1",
557
+ (response) => {
558
+ try {
559
+ const newLayout = JSON.parse(response.content) as WizardPageModel;
560
+
561
+ if (newLayout) {
562
+ setPageModel((prev) => mergeLayout(prev, newLayout));
563
+ }
564
+
565
+ setMessage(newLayout.message);
566
+ } catch (parseError: unknown) {}
567
+ },
568
+ );
569
+
570
+ const pageModel = result;
571
+
572
+ const finalLayout = JSON.parse(pageModel.content);
573
+ console.log("RESULT LAYOUT: ", pageModel, finalLayout);
574
+ setPageModel((prev) => mergeLayout(prev, finalLayout));
575
+ setStepCompleted(true);
576
+ } catch (error) {
577
+ console.error(error);
578
+ } finally {
579
+ setIsLoading(false);
580
+ }
581
+ };
582
+
583
+ // Custom instructions panel
584
+ const customInstructionsPanel = () => {
585
+ return (
586
+ <InputTextarea
587
+ value={internalState.layoutInstructions}
588
+ onChange={(e) =>
589
+ setInternalState({
590
+ ...internalState,
591
+ layoutInstructions: e.target.value,
592
+ })
593
+ }
594
+ placeholder="Example: Make it modern and minimalist. Focus on images. Include a hero section at the top."
595
+ className="h-48 w-full rounded border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:ring-blue-500 focus:outline-none"
596
+ />
597
+ );
598
+ };
599
+
600
+ return (
601
+ <div className="flex h-full flex-col md:flex-row">
602
+ <div className="flex flex-col items-stretch overflow-y-auto md:h-full">
603
+ <WizardBox
604
+ title="Page Setup"
605
+ icon={<Settings />}
606
+ description="Configure the page name, target location and language for your new page"
607
+ collapsible="yes"
608
+ summary={
609
+ <div className="text-xs text-gray-500">
610
+ Folder: {fullParentItem?.path}, Language: {language}, Name:{" "}
611
+ {pageModel.name || "Not set"}
612
+ </div>
613
+ }
614
+ className="w-full md:w-96"
615
+ >
616
+ <div className="flex flex-col items-stretch">
617
+ <div className="relative flex-1">
618
+ {isGenerating ? (
619
+ <div className="flex h-full items-center justify-center">
620
+ <Generate title="Generating page name..." />
621
+ </div>
622
+ ) : (
623
+ <>
624
+ <div className="mb-4">
625
+ <label
626
+ htmlFor="pageName"
627
+ className="mb-1 block text-sm font-medium"
628
+ >
629
+ Page Name
630
+ </label>
631
+ <InputText
632
+ id="pageName"
633
+ type="text"
634
+ disabled={!!pageItem}
635
+ className={`w-full rounded border p-2 text-sm ${
636
+ !nameValidation.isValid
637
+ ? "border-red-500"
638
+ : nameValidation.isValid && pageModel.name
639
+ ? "border-green-500"
640
+ : ""
641
+ }`}
642
+ value={pageModel.name}
643
+ onChange={(e) =>
644
+ handleInputChange("name", e.target.value)
645
+ }
646
+ placeholder="Enter page name"
647
+ />
648
+ {nameValidation.message && !nameValidation.isValid && (
649
+ <div className="mt-1 text-xs text-red-600">
650
+ {nameValidation.message}
651
+ </div>
652
+ )}
653
+ {nameValidation.isValid && pageModel.name && (
654
+ <div className="mt-1 text-xs text-green-600">
655
+ ✓ Page name is available
656
+ </div>
657
+ )}
658
+ </div>
659
+
660
+ <div className="mb-4">
661
+ <div className="mb-1 text-sm font-medium">
662
+ Target Parent Item
663
+ </div>
664
+ <div className="mb-4 break-after-all text-xs text-gray-600">
665
+ {fullParentItem?.path || "Loading..."}
666
+ </div>
667
+ </div>
668
+
669
+ <div className="relative mb-4">
670
+ <label
671
+ htmlFor="language"
672
+ className="mb-1 block text-sm font-medium"
673
+ >
674
+ Language
675
+ </label>
676
+ <div className="w-fit">
677
+ <LanguageSelector
678
+ selectedLanguage={language}
679
+ darkMode={true}
680
+ disabled={isGenerating}
681
+ onLanguageSelected={(language) =>
682
+ setLanguage(language.languageCode)
683
+ }
684
+ showAllLanguages={true}
685
+ />
686
+ </div>
687
+ </div>
688
+ </>
689
+ )}
690
+ </div>
691
+ </div>
692
+ </WizardBox>
693
+ <WizardBoxConnector direction="vertical" />
694
+ <WizardBox
695
+ title="Content modules"
696
+ icon={<Wand2 />}
697
+ description="Select the component types the AI can choose from"
698
+ className="w-full md:w-96"
699
+ collapsible="yes"
700
+ defaultCollapsed="yes"
701
+ summary=<div className="text-xs text-gray-500">
702
+ {getComponentTypeSelectorSummary(
703
+ data.selectedComponentTypes,
704
+ availableComponentTypes,
705
+ )}
706
+ </div>
707
+ >
708
+ <ComponentTypeSelector
709
+ selectedComponentTypes={data.selectedComponentTypes}
710
+ setSelectedComponentTypes={(selectedTypes) => {
711
+ setData({
712
+ ...data,
713
+ selectedComponentTypes: selectedTypes,
714
+ });
715
+ }}
716
+ schema={wizard.schema}
717
+ data={data}
718
+ setData={setData}
719
+ step={step}
720
+ />
721
+ </WizardBox>
722
+ <WizardBoxConnector direction="vertical" />
723
+ <WizardBox
724
+ title="Generate content"
725
+ icon={<Wand2 />}
726
+ collapsible="yes"
727
+ defaultCollapsed="mobileOnly"
728
+ description="Refine instructions and generate content"
729
+ className="w-full md:w-96"
730
+ footer={
731
+ <div>
732
+ <div className="flex w-full gap-2">
733
+ <ActionButton
734
+ onClick={createLayout}
735
+ disabled={
736
+ isLoading ||
737
+ isCreatingComponents ||
738
+ !data.selectedComponentTypes ||
739
+ data.selectedComponentTypes.length === 0 ||
740
+ (!pageItem &&
741
+ (!pageModel.name ||
742
+ pageModel.name.trim().length < 3 ||
743
+ !nameValidation.isValid))
744
+ }
745
+ isLoading={
746
+ isLoading ||
747
+ isCreatingComponents ||
748
+ !!editContext?.activeFieldActions.find(
749
+ (action) => action.state === "running",
750
+ )
751
+ }
752
+ loadingText={"Working"}
753
+ className="w-full flex-1"
754
+ >
755
+ {!pageItem ? "Create page" : "Delete & Regenerate content"}
756
+ </ActionButton>
757
+
758
+ {isLoading && (
759
+ <ActionButton
760
+ isLoading={false}
761
+ onClick={() => abortController?.abort("User aborted")}
762
+ >
763
+ Abort
764
+ </ActionButton>
765
+ )}
766
+ </div>
767
+ {message && <div className="mt-2 text-xs">{message}</div>}
768
+ </div>
769
+ }
770
+ >
771
+ <div>{customInstructionsPanel()}</div>
772
+ </WizardBox>
773
+ </div>
774
+ {pageItem && (
775
+ <>
776
+ <WizardBoxConnector />
777
+ <WizardBox
778
+ title="Page preview"
779
+ icon={<PanelsTopLeft />}
780
+ description="After finalizing the page, you can edit, further enhance the content and share the page with your team for collaboration."
781
+ noPadding={true}
782
+ className="flex-1"
783
+ showFullscreenButton={true}
784
+ >
785
+ <div
786
+ className={cn(
787
+ "min-h-[300px] border-t border-gray-200",
788
+ pageLoaded ? "h-full" : "h-0",
789
+ )}
790
+ >
791
+ <PageViewer
792
+ name="single"
793
+ compareView={false}
794
+ showFormEditor={false}
795
+ followEditsDefault={true}
796
+ pageViewContext={editContext!.pageView}
797
+ />
798
+ </div>
799
+ </WizardBox>
800
+ </>
801
+ )}
802
+ </div>
803
+ );
804
+ }
805
+
806
+ /**
807
+ * Merges a new layout with the previous layout
808
+ * - Keeps all previous components
809
+ * - Adds new components
810
+ * - Updates existing fields with new values
811
+ * - Adds new fields
812
+ */
813
+ const mergeLayout = (
814
+ prev: WizardPageModel | null,
815
+ newLayout: WizardPageModel,
816
+ ): WizardPageModel => {
817
+ if (!prev) return newLayout;
818
+
819
+ // Merge top-level properties
820
+ const result: WizardPageModel = {
821
+ name: newLayout.name || prev.name,
822
+ message: newLayout.message,
823
+ metaDescription: prev.metaDescription,
824
+ metaKeywords: prev.metaKeywords,
825
+ components: [...(prev.components || [])],
826
+ };
827
+
828
+ // Function to merge components recursively
829
+ const mergeComponents = (
830
+ existingComponents: any[],
831
+ newComponents: any[],
832
+ ): any[] => {
833
+ if (!newComponents?.length) return existingComponents;
834
+
835
+ const result = [...existingComponents];
836
+
837
+ for (const newComp of newComponents) {
838
+ // Try to find a matching component by type and name
839
+ const existingIndex = result.findIndex(
840
+ (comp) => comp.type === newComp.type && comp.name === newComp.name,
841
+ );
842
+
843
+ if (existingIndex >= 0) {
844
+ // Update existing component
845
+ const existing = result[existingIndex];
846
+
847
+ // Merge fields - update existing fields and add new ones
848
+ const mergedFields = [...(existing.fields || [])];
849
+ for (const newField of newComp.fields || []) {
850
+ const fieldIndex = mergedFields.findIndex(
851
+ (f) => f.name === newField.name,
852
+ );
853
+ if (fieldIndex >= 0) {
854
+ mergedFields[fieldIndex].value = newField.value; // Update existing field
855
+ mergedFields[fieldIndex].type = newField.type;
856
+ } else {
857
+ mergedFields.push(newField); // Add new field
858
+ }
859
+ }
860
+
861
+ // Recursive merge of children components
862
+ const mergedChildren = mergeComponents(
863
+ existing.children || [],
864
+ newComp.children || [],
865
+ );
866
+
867
+ // Create updated component
868
+ result[existingIndex] = {
869
+ ...existing,
870
+ fields: mergedFields,
871
+ children: mergedChildren,
872
+ placeholder: newComp.placeholder || existing.placeholder,
873
+ };
874
+ } else {
875
+ // Add new component
876
+ result.push(newComp);
877
+ }
878
+ }
879
+
880
+ return result;
881
+ };
882
+
883
+ // Merge components recursively
884
+ result.components = mergeComponents(
885
+ result.components,
886
+ newLayout.components || [],
887
+ );
888
+
889
+ return result;
890
+ };