@alpaca-editor/core 1.0.0

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 (239) hide show
  1. package/.prettierrc +3 -0
  2. package/eslint.config.mjs +4 -0
  3. package/images/bg-shape-black.webp +0 -0
  4. package/package.json +52 -0
  5. package/src/client-components/api.ts +6 -0
  6. package/src/client-components/index.ts +19 -0
  7. package/src/components/ActionButton.tsx +43 -0
  8. package/src/components/Error.tsx +57 -0
  9. package/src/config/config.tsx +737 -0
  10. package/src/config/types.ts +263 -0
  11. package/src/editor/ComponentInfo.tsx +77 -0
  12. package/src/editor/ConfirmationDialog.tsx +103 -0
  13. package/src/editor/ContentTree.tsx +654 -0
  14. package/src/editor/ContextMenu.tsx +155 -0
  15. package/src/editor/Editor.tsx +91 -0
  16. package/src/editor/EditorWarning.tsx +34 -0
  17. package/src/editor/EditorWarnings.tsx +33 -0
  18. package/src/editor/FieldEditorPopup.tsx +65 -0
  19. package/src/editor/FieldHistory.tsx +74 -0
  20. package/src/editor/FieldList.tsx +190 -0
  21. package/src/editor/FieldListField.tsx +387 -0
  22. package/src/editor/FieldListFieldWithFallbacks.tsx +211 -0
  23. package/src/editor/FloatingToolbar.tsx +163 -0
  24. package/src/editor/ImageEditor.tsx +129 -0
  25. package/src/editor/InsertMenu.tsx +332 -0
  26. package/src/editor/ItemInfo.tsx +90 -0
  27. package/src/editor/LinkEditorDialog.tsx +192 -0
  28. package/src/editor/MainLayout.tsx +94 -0
  29. package/src/editor/NewEditorClient.tsx +11 -0
  30. package/src/editor/PictureCropper.tsx +505 -0
  31. package/src/editor/PictureEditor.tsx +206 -0
  32. package/src/editor/PictureEditorDialog.tsx +381 -0
  33. package/src/editor/PublishDialog.ignore +74 -0
  34. package/src/editor/ScrollingContentTree.tsx +47 -0
  35. package/src/editor/Terminal.tsx +215 -0
  36. package/src/editor/Titlebar.tsx +23 -0
  37. package/src/editor/ai/AiPopup.tsx +59 -0
  38. package/src/editor/ai/AiResponseMessage.tsx +82 -0
  39. package/src/editor/ai/AiTerminal.tsx +450 -0
  40. package/src/editor/ai/AiToolCall.tsx +46 -0
  41. package/src/editor/ai/EditorAiTerminal.tsx +20 -0
  42. package/src/editor/ai/editorAiContext.ts +18 -0
  43. package/src/editor/client/DialogContext.tsx +49 -0
  44. package/src/editor/client/EditorClient.tsx +1831 -0
  45. package/src/editor/client/GenericDialog.tsx +50 -0
  46. package/src/editor/client/editContext.ts +330 -0
  47. package/src/editor/client/helpers.ts +44 -0
  48. package/src/editor/client/itemsRepository.ts +391 -0
  49. package/src/editor/client/operations.ts +610 -0
  50. package/src/editor/client/pageModelBuilder.ts +182 -0
  51. package/src/editor/commands/commands.ts +23 -0
  52. package/src/editor/commands/componentCommands.tsx +408 -0
  53. package/src/editor/commands/createVersionCommand.ts +33 -0
  54. package/src/editor/commands/deleteVersionCommand.ts +71 -0
  55. package/src/editor/commands/itemCommands.tsx +186 -0
  56. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
  57. package/src/editor/commands/undo.ts +39 -0
  58. package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
  59. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
  60. package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
  61. package/src/editor/component-designer/ComponentEditor.tsx +97 -0
  62. package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
  63. package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
  64. package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
  65. package/src/editor/component-designer/PlaceholdersEditor.tsx +183 -0
  66. package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
  67. package/src/editor/component-designer/TemplateEditor.tsx +236 -0
  68. package/src/editor/component-designer/aiContext.ts +23 -0
  69. package/src/editor/componentTreeHelper.tsx +114 -0
  70. package/src/editor/control-center/ControlCenterMenu.tsx +71 -0
  71. package/src/editor/control-center/IndexOverview.tsx +50 -0
  72. package/src/editor/control-center/IndexSettings.tsx +266 -0
  73. package/src/editor/control-center/Status.tsx +7 -0
  74. package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
  75. package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
  76. package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
  77. package/src/editor/editor-warnings/NoWriteAccess.tsx +15 -0
  78. package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
  79. package/src/editor/field-types/AttachmentEditor.tsx +9 -0
  80. package/src/editor/field-types/CheckboxEditor.tsx +47 -0
  81. package/src/editor/field-types/DropLinkEditor.tsx +75 -0
  82. package/src/editor/field-types/DropListEditor.tsx +84 -0
  83. package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
  84. package/src/editor/field-types/InternalLinkFieldEditor.tsx +112 -0
  85. package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
  86. package/src/editor/field-types/MultiLineText.tsx +63 -0
  87. package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
  88. package/src/editor/field-types/RawEditor.tsx +53 -0
  89. package/src/editor/field-types/ReactQuill.tsx +580 -0
  90. package/src/editor/field-types/RichTextEditor.tsx +22 -0
  91. package/src/editor/field-types/RichTextEditorComponent.tsx +108 -0
  92. package/src/editor/field-types/SingleLineText.tsx +150 -0
  93. package/src/editor/field-types/TreeListEditor.tsx +261 -0
  94. package/src/editor/fieldTypes.ts +140 -0
  95. package/src/editor/media-selector/AiImageSearch.tsx +186 -0
  96. package/src/editor/media-selector/AiImageSearchPrompt.tsx +95 -0
  97. package/src/editor/media-selector/MediaSelector.tsx +42 -0
  98. package/src/editor/media-selector/Preview.tsx +14 -0
  99. package/src/editor/media-selector/Thumbnails.tsx +48 -0
  100. package/src/editor/media-selector/TreeSelector.tsx +292 -0
  101. package/src/editor/media-selector/UploadZone.tsx +137 -0
  102. package/src/editor/menubar/ActionsMenu.tsx +47 -0
  103. package/src/editor/menubar/ActiveUsers.tsx +17 -0
  104. package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
  105. package/src/editor/menubar/BrowseHistory.tsx +37 -0
  106. package/src/editor/menubar/ItemLanguageVersion.tsx +52 -0
  107. package/src/editor/menubar/LanguageSelector.tsx +152 -0
  108. package/src/editor/menubar/Menu.tsx +83 -0
  109. package/src/editor/menubar/NavButtons.tsx +74 -0
  110. package/src/editor/menubar/PageSelector.tsx +139 -0
  111. package/src/editor/menubar/PageViewerControls.tsx +99 -0
  112. package/src/editor/menubar/Separator.tsx +12 -0
  113. package/src/editor/menubar/SiteInfo.tsx +53 -0
  114. package/src/editor/menubar/User.tsx +27 -0
  115. package/src/editor/menubar/VersionSelector.tsx +143 -0
  116. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +287 -0
  117. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
  118. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +44 -0
  119. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
  120. package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
  121. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
  122. package/src/editor/page-editor-chrome/FrameMenu.tsx +263 -0
  123. package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
  124. package/src/editor/page-editor-chrome/InlineEditor.tsx +147 -0
  125. package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
  126. package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
  127. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +119 -0
  128. package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +154 -0
  129. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +171 -0
  130. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +233 -0
  131. package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
  132. package/src/editor/page-viewer/EditorForm.tsx +247 -0
  133. package/src/editor/page-viewer/MiniMap.tsx +351 -0
  134. package/src/editor/page-viewer/PageViewer.tsx +127 -0
  135. package/src/editor/page-viewer/PageViewerFrame.tsx +1030 -0
  136. package/src/editor/page-viewer/pageViewContext.ts +186 -0
  137. package/src/editor/pageModel.ts +191 -0
  138. package/src/editor/picture-shared.tsx +53 -0
  139. package/src/editor/reviews/Comment.tsx +265 -0
  140. package/src/editor/reviews/Comments.tsx +50 -0
  141. package/src/editor/reviews/PreviewInfo.tsx +35 -0
  142. package/src/editor/reviews/Reviews.tsx +280 -0
  143. package/src/editor/reviews/reviewCommands.tsx +47 -0
  144. package/src/editor/reviews/useReviews.tsx +70 -0
  145. package/src/editor/services/aiService.ts +155 -0
  146. package/src/editor/services/componentDesignerService.ts +151 -0
  147. package/src/editor/services/contentService.ts +159 -0
  148. package/src/editor/services/editService.ts +462 -0
  149. package/src/editor/services/indexService.ts +24 -0
  150. package/src/editor/services/reviewsService.ts +45 -0
  151. package/src/editor/services/serviceHelper.ts +95 -0
  152. package/src/editor/services/systemService.ts +5 -0
  153. package/src/editor/services/translationService.ts +21 -0
  154. package/src/editor/services-server/api.ts +150 -0
  155. package/src/editor/services-server/graphQL.ts +106 -0
  156. package/src/editor/sidebar/ComponentPalette.tsx +146 -0
  157. package/src/editor/sidebar/ComponentTree.tsx +512 -0
  158. package/src/editor/sidebar/ComponentTree2.tsxx +490 -0
  159. package/src/editor/sidebar/Debug.tsx +105 -0
  160. package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
  161. package/src/editor/sidebar/EditHistory.tsx +134 -0
  162. package/src/editor/sidebar/GraphQL.tsx +164 -0
  163. package/src/editor/sidebar/Insert.tsx +35 -0
  164. package/src/editor/sidebar/MainContentTree.tsx +95 -0
  165. package/src/editor/sidebar/Performance.tsx +53 -0
  166. package/src/editor/sidebar/Sessions.tsx +35 -0
  167. package/src/editor/sidebar/Sidebar.tsx +20 -0
  168. package/src/editor/sidebar/SidebarView.tsx +150 -0
  169. package/src/editor/sidebar/Translations.tsx +276 -0
  170. package/src/editor/sidebar/Validation.tsx +102 -0
  171. package/src/editor/sidebar/ViewSelector.tsx +49 -0
  172. package/src/editor/sidebar/Workbox.tsx +209 -0
  173. package/src/editor/ui/CenteredMessage.tsx +7 -0
  174. package/src/editor/ui/CopyToClipboardButton.tsx +23 -0
  175. package/src/editor/ui/DialogButtons.tsx +11 -0
  176. package/src/editor/ui/Icons.tsx +585 -0
  177. package/src/editor/ui/ItemNameDialog.tsx +94 -0
  178. package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
  179. package/src/editor/ui/ItemSearch.tsx +173 -0
  180. package/src/editor/ui/PerfectTree.tsx +550 -0
  181. package/src/editor/ui/Section.tsx +35 -0
  182. package/src/editor/ui/SimpleIconButton.tsx +43 -0
  183. package/src/editor/ui/SimpleMenu.tsx +48 -0
  184. package/src/editor/ui/SimpleTable.tsx +63 -0
  185. package/src/editor/ui/SimpleTabs.tsx +55 -0
  186. package/src/editor/ui/SimpleToolbar.tsx +7 -0
  187. package/src/editor/ui/Spinner.tsx +7 -0
  188. package/src/editor/ui/Splitter.tsx +247 -0
  189. package/src/editor/ui/StackedPanels.tsx +134 -0
  190. package/src/editor/ui/Toolbar.tsx +7 -0
  191. package/src/editor/utils/id-helper.ts +3 -0
  192. package/src/editor/utils/insertOptions.ts +69 -0
  193. package/src/editor/utils/itemutils.ts +29 -0
  194. package/src/editor/utils/useMemoDebug.ts +28 -0
  195. package/src/editor/utils.ts +435 -0
  196. package/src/editor/views/CompareView.tsx +256 -0
  197. package/src/editor/views/EditView.tsx +27 -0
  198. package/src/editor/views/ItemEditor.tsx +58 -0
  199. package/src/editor/views/SingleEditView.tsx +44 -0
  200. package/src/fonts/Geist-Black.woff2 +0 -0
  201. package/src/fonts/Geist-Bold.woff2 +0 -0
  202. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  203. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  204. package/src/fonts/Geist-Light.woff2 +0 -0
  205. package/src/fonts/Geist-Medium.woff2 +0 -0
  206. package/src/fonts/Geist-Regular.woff2 +0 -0
  207. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  208. package/src/fonts/Geist-Thin.woff2 +0 -0
  209. package/src/fonts/Geist[wght].woff2 +0 -0
  210. package/src/index.ts +7 -0
  211. package/src/page-wizard/PageWizard.tsx +163 -0
  212. package/src/page-wizard/SelectWizard.tsx +109 -0
  213. package/src/page-wizard/WizardSteps.tsx +207 -0
  214. package/src/page-wizard/service.ts +35 -0
  215. package/src/page-wizard/startPageWizardCommand.ts +27 -0
  216. package/src/page-wizard/steps/BuildPageStep.tsx +266 -0
  217. package/src/page-wizard/steps/CollectStep.tsx +233 -0
  218. package/src/page-wizard/steps/ComponentTypesSelector.tsx +443 -0
  219. package/src/page-wizard/steps/Components.tsx +193 -0
  220. package/src/page-wizard/steps/CreatePage.tsx +285 -0
  221. package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +384 -0
  222. package/src/page-wizard/steps/EditButton.tsx +34 -0
  223. package/src/page-wizard/steps/FieldEditor.tsx +102 -0
  224. package/src/page-wizard/steps/Generate.tsx +32 -0
  225. package/src/page-wizard/steps/ImagesStep.tsx +318 -0
  226. package/src/page-wizard/steps/LayoutStep.tsx +228 -0
  227. package/src/page-wizard/steps/SelectStep.tsx +256 -0
  228. package/src/page-wizard/steps/schema.ts +180 -0
  229. package/src/page-wizard/steps/usePageCreator.ts +279 -0
  230. package/src/splash-screen/NewPage.tsx +232 -0
  231. package/src/splash-screen/SectionHeadline.tsx +21 -0
  232. package/src/splash-screen/SplashScreen.tsx +156 -0
  233. package/src/tour/Tour.tsx +558 -0
  234. package/src/tour/default-tour.tsx +300 -0
  235. package/src/tour/preview-tour.tsx +127 -0
  236. package/src/types.ts +302 -0
  237. package/styles.css +476 -0
  238. package/tsconfig.build.json +21 -0
  239. package/tsconfig.json +11 -0
@@ -0,0 +1,285 @@
1
+ import { useEffect, useState } from "react";
2
+ import { ActionButton } from "../../components/ActionButton";
3
+ import { useDebouncedCallback } from "use-debounce";
4
+ import { FullItem, ItemDescriptor } from "../../editor/pageModel";
5
+ import { getChildren } from "../../editor/services/contentService";
6
+ import { Wizard, WizardData, WizardPageModel, WizardStep } from "../PageWizard";
7
+ import { executePrompt } from "../../editor/services/aiService";
8
+ import { createWizardAiContext } from "../service";
9
+ import { useEditContext } from "../../editor/client/editContext";
10
+ import { LanguageSelector } from "../../editor/menubar/LanguageSelector";
11
+
12
+ export function CreatePage({
13
+ wizard,
14
+ parentItem,
15
+ setPageItem,
16
+ pageItem,
17
+ pageModel,
18
+ setPageModel,
19
+ step,
20
+ data,
21
+ }: {
22
+ wizard: Wizard;
23
+ parentItem: ItemDescriptor;
24
+ setPageItem: (item: FullItem) => void;
25
+ pageItem?: FullItem;
26
+ pageModel: WizardPageModel;
27
+ setPageModel: (model: WizardPageModel) => void;
28
+ step: WizardStep;
29
+ data: WizardData;
30
+ }) {
31
+ const [validationMessage, setValidationMessage] = useState<string>();
32
+ const [isBuilding, setIsBuilding] = useState(false);
33
+ const [isGenerating, setIsGenerating] = useState(false);
34
+ const [language, setLanguage] = useState<string>(parentItem.language || "en");
35
+ const [previousLanguage, setPreviousLanguage] = useState<string>(language);
36
+ const [fullParentItem, setFullParentItem] = useState<FullItem>();
37
+ const editContext = useEditContext();
38
+
39
+ useEffect(() => {
40
+ if (!editContext) return;
41
+
42
+ const generateNameAndMetaDescription = async () => {
43
+ const metaInstructions =
44
+ step["Instructions for generating item name and meta fields"];
45
+
46
+ setIsGenerating(true);
47
+
48
+ try {
49
+ const abortController = new AbortController();
50
+
51
+ const result = await executePrompt(
52
+ [
53
+ {
54
+ content: `${metaInstructions?.trim()} Reply with a json object of type PageModel = { name: string; metaDescription: string; metaKeywords: string; };
55
+ The item name should be a valid sitecore item name. Spaces are allowed but no special characters or Umlaute.
56
+ The language of the page is ${language}.
57
+ Input data: ${JSON.stringify(data)}`,
58
+ name: "system",
59
+ role: "system",
60
+ },
61
+ ],
62
+ editContext,
63
+ createWizardAiContext,
64
+ [""],
65
+ true,
66
+ { signal: abortController.signal },
67
+ "o3-mini-low",
68
+ (response) => {
69
+ try {
70
+ const newLayout = JSON.parse(response.content) as WizardPageModel;
71
+
72
+ setPageModel({
73
+ ...pageModel,
74
+ name: newLayout.name,
75
+ metaDescription: newLayout.metaDescription,
76
+ metaKeywords: newLayout.metaKeywords,
77
+ });
78
+ } catch (parseError: unknown) {}
79
+ }
80
+ );
81
+
82
+ const pageModel = JSON.parse(result.content) as WizardPageModel;
83
+ setPageModel({
84
+ ...pageModel,
85
+ name: pageModel.name,
86
+ metaDescription: pageModel.metaDescription,
87
+ metaKeywords: pageModel.metaKeywords,
88
+ });
89
+ } catch (error) {
90
+ console.error("Error generating name and meta description", error);
91
+ } finally {
92
+ setIsGenerating(false);
93
+ }
94
+ };
95
+
96
+ if (!pageModel.name || language !== previousLanguage) {
97
+ generateNameAndMetaDescription();
98
+ setPreviousLanguage(language);
99
+ }
100
+ }, [language, pageModel.name, previousLanguage, data, step, editContext]);
101
+
102
+ useEffect(() => {
103
+ const loadParentItem = async () => {
104
+ if (!parentItem) return;
105
+ const item = await editContext?.itemsRepository.getItem(parentItem);
106
+ setFullParentItem(item);
107
+ };
108
+ loadParentItem();
109
+ }, [parentItem]);
110
+
111
+ const checkName = async () => {
112
+ if (!parentItem) return;
113
+ let valid: boolean =
114
+ !!wizard.templateId && pageModel.name.trim().length >= 3;
115
+ if (valid) {
116
+ const children = await getChildren(
117
+ parentItem.id,
118
+ editContext?.sessionId ?? "",
119
+ [],
120
+ false,
121
+ editContext?.contentEditorItem?.language || "en"
122
+ );
123
+ if (
124
+ children.find(
125
+ (x) =>
126
+ x.name.toLocaleLowerCase() ===
127
+ pageModel.name.trim().toLocaleLowerCase()
128
+ )
129
+ ) {
130
+ valid = false;
131
+ setValidationMessage("A page with this name already exists.");
132
+ }
133
+ } else {
134
+ if (pageModel.name.trim().length > 0 && pageModel.name.trim().length < 3)
135
+ setValidationMessage("Name is too short.");
136
+ else setValidationMessage(undefined);
137
+ }
138
+
139
+ if (valid) setValidationMessage(undefined);
140
+ return valid;
141
+ };
142
+
143
+ const checkNameValidDebounced = useDebouncedCallback(
144
+ async () => checkName(),
145
+ 500
146
+ );
147
+
148
+ useEffect(() => {
149
+ checkNameValidDebounced();
150
+ }, [pageModel.name]);
151
+
152
+ const createPage = () => {
153
+ setTimeout(async () => {
154
+ if (!editContext || !parentItem) return;
155
+ try {
156
+ if (!(await checkName())) return;
157
+ setIsBuilding(true);
158
+ const result = await editContext.operations.createItem(
159
+ { ...parentItem, language },
160
+ wizard.templateId,
161
+ pageModel.name
162
+ );
163
+ if (result) {
164
+ editContext?.loadItem(result, { addToBrowseHistory: true });
165
+ const item = await editContext?.itemsRepository.getItem(result);
166
+ if (item) setPageItem(item);
167
+ else console.error("Failed to load newly created item", result);
168
+
169
+ console.log("Page created", result, "Page model", pageModel);
170
+ }
171
+ } catch (error) {
172
+ console.error("Error creating page", error);
173
+ } finally {
174
+ setIsBuilding(false);
175
+ }
176
+ }, 1);
177
+ };
178
+
179
+ const handleInputChange = (field: keyof typeof pageModel, value: string) => {
180
+ const updatedPageModel = {
181
+ ...pageModel,
182
+ [field]: value,
183
+ };
184
+
185
+ setPageModel(updatedPageModel);
186
+ };
187
+
188
+ return (
189
+ <div className="pr-6" style={{ viewTransitionName: "page-properties" }}>
190
+ <div className="mb-4">
191
+ <div className="text-sm font-medium mb-1">Target Parent Item</div>
192
+ <div className="mb-4 text-xs break-after-all">
193
+ {fullParentItem?.path}
194
+ </div>
195
+ </div>
196
+ <div className="mb-4 relative">
197
+ <label
198
+ htmlFor="language"
199
+ className="block text-sm font-medium mb-1"
200
+ >
201
+ Language
202
+ </label>
203
+ <div className="w-fit">
204
+ <LanguageSelector
205
+ selectedLanguage={language}
206
+ darkMode={true}
207
+ disabled={isBuilding || isGenerating}
208
+ onLanguageSelected={(language) =>
209
+ setLanguage(language.languageCode)
210
+ }
211
+ showAllLanguages={true}
212
+ />
213
+ </div>
214
+ </div>
215
+ <div className="mb-4">
216
+ <label
217
+ htmlFor="pageName"
218
+ className="block text-sm font-medium mb-1"
219
+ >
220
+ Page Name
221
+ </label>
222
+ <input
223
+ id="pageName"
224
+ type="text"
225
+ disabled={!!pageItem || isBuilding || isGenerating}
226
+ className="w-full p-2 border rounded text-sm"
227
+ value={pageModel.name}
228
+ onChange={(e) => handleInputChange("name", e.target.value)}
229
+ placeholder="Enter page name"
230
+ />
231
+ {validationMessage && (
232
+ <div className="mt-2 text-sm text-red-500">
233
+ {validationMessage}
234
+ </div>
235
+ )}
236
+ </div>
237
+
238
+ <div className="mb-4">
239
+ <label
240
+ htmlFor="metaDescription"
241
+ className="block text-sm font-medium mb-1"
242
+ >
243
+ Meta Description
244
+ </label>
245
+ <textarea
246
+ id="metaDescription"
247
+ disabled={!!pageItem || isBuilding || isGenerating}
248
+ className="w-full p-2 border rounded min-h-[100px] text-sm"
249
+ value={pageModel.metaDescription}
250
+ onChange={(e) => handleInputChange("metaDescription", e.target.value)}
251
+ placeholder="Enter meta description"
252
+ />
253
+ </div>
254
+ <div className="mb-4">
255
+ <label
256
+ htmlFor="metaKeywords"
257
+ className="block text-sm font-medium mb-1"
258
+ >
259
+ Meta Keywords
260
+ </label>
261
+ <textarea
262
+ id="metaKeywords"
263
+ disabled={!!pageItem || isBuilding || isGenerating}
264
+ className="w-full p-2 border rounded min-h-[100px] text-sm"
265
+ value={pageModel.metaKeywords}
266
+ onChange={(e) => handleInputChange("metaKeywords", e.target.value)}
267
+ placeholder="Enter meta keywords"
268
+ />
269
+ </div>
270
+ {!pageItem && (
271
+ <ActionButton
272
+ disabled={isBuilding || isGenerating || !!validationMessage}
273
+ className="flex-1"
274
+ onClick={() => {
275
+ createPage();
276
+ }}
277
+ isLoading={isBuilding || isGenerating}
278
+ loadingText={isGenerating ? "Generating..." : "Create Page"}
279
+ >
280
+ Create Page
281
+ </ActionButton>
282
+ )}
283
+ </div>
284
+ );
285
+ }
@@ -0,0 +1,384 @@
1
+ import { Splitter, SplitterPanel } from "../../editor/ui/Splitter";
2
+ import { StepComponentProps } from "../../config/types";
3
+
4
+ import { CreatePage } from "./CreatePage";
5
+ import { useEffect, useState } from "react";
6
+ import { PageViewer } from "../../editor/page-viewer/PageViewer";
7
+ import { useEditContext } from "../../editor/client/editContext";
8
+ import { classNames } from "primereact/utils";
9
+ import { ComponentTypeSelector } from "./ComponentTypesSelector";
10
+ import { ActionButton } from "../../components/ActionButton";
11
+ import { convertPageSchemaToWizardComponents } from "./schema";
12
+ import { WizardPageModel } from "../PageWizard";
13
+ import { createWizardAiContext, wipeComponents } from "../service";
14
+
15
+ import { executePrompt } from "../../editor/services/aiService";
16
+ import { usePageCreator } from "./usePageCreator";
17
+ import { useThrottledCallback } from "use-debounce";
18
+
19
+ export function CreatePageAndLayoutStep({
20
+ wizard,
21
+ step,
22
+ parentItem,
23
+ pageModel,
24
+ setPageModel,
25
+ data,
26
+ setData,
27
+ internalState,
28
+ setInternalState,
29
+ setStepCompleted,
30
+ }: StepComponentProps) {
31
+ const [pageLoaded, setPageLoaded] = useState(false);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const editContext = useEditContext();
34
+ const [abortController, setAbortController] = useState<AbortController>();
35
+ const [message, setMessage] = useState<string>();
36
+ const [isCreatingComponents, setIsCreatingComponents] = useState(false);
37
+
38
+ const pageItem = internalState.pageItem;
39
+
40
+ const pageCreator = usePageCreator(pageItem, wizard, setPageModel);
41
+
42
+ if (!parentItem) return "No parent item";
43
+
44
+ useEffect(() => {
45
+ if (!internalState.layoutInstructions) {
46
+ setInternalState({
47
+ ...internalState,
48
+ layoutInstructions: step.instructions,
49
+ });
50
+ }
51
+ }, []);
52
+
53
+ const createComponents = async () => {
54
+ if (!pageLoaded) return;
55
+ if (pageModel?.components && !isCreatingComponents) {
56
+ try {
57
+ setIsCreatingComponents(true);
58
+
59
+ await pageCreator.createComponentsRecursively(
60
+ pageModel.components,
61
+ "root"
62
+ );
63
+ setInternalState((prev: any) => ({
64
+ ...prev,
65
+ componentsCreated: true,
66
+ }));
67
+ } finally {
68
+ setIsCreatingComponents(false);
69
+ }
70
+ }
71
+ };
72
+
73
+ useEffect(() => {
74
+ if (
75
+ internalState.componentsCreated &&
76
+ !isCreatingComponents &&
77
+ !isLoading
78
+ ) {
79
+ editContext?.requestRefresh("immediate");
80
+ }
81
+ }, [isCreatingComponents, isLoading]);
82
+
83
+ const createComponentsThrottled = useThrottledCallback(createComponents, 300);
84
+
85
+ useEffect(() => {
86
+ createComponentsThrottled();
87
+ }, [pageModel]);
88
+
89
+ useEffect(() => {
90
+ if (
91
+ editContext &&
92
+ editContext.page &&
93
+ editContext.page.item &&
94
+ pageItem &&
95
+ pageItem.id === editContext.page.item.descriptor.id
96
+ )
97
+ setPageLoaded(true);
98
+ }, [editContext?.page, pageItem]);
99
+
100
+ if (!pageItem) {
101
+ return (
102
+ <CreatePage
103
+ wizard={wizard}
104
+ parentItem={parentItem}
105
+ setPageItem={(pageItem) =>
106
+ setInternalState((prev: any) => ({ ...prev, pageItem }))
107
+ }
108
+ pageItem={pageItem}
109
+ pageModel={pageModel}
110
+ setPageModel={setPageModel}
111
+ step={step}
112
+ data={data}
113
+ />
114
+ );
115
+ }
116
+
117
+ const createLayout = async () => {
118
+ try {
119
+ if (!editContext) return;
120
+
121
+ setIsLoading(true);
122
+ setStepCompleted(false);
123
+
124
+ if (internalState.componentsCreated) {
125
+ await wipeComponents(pageItem.descriptor);
126
+ setPageModel((prev) => ({
127
+ ...prev,
128
+ components: [],
129
+ }));
130
+ editContext?.requestRefresh("immediate");
131
+ }
132
+
133
+ // Parse schema if it's a string
134
+ const schema =
135
+ typeof wizard.schema === "string"
136
+ ? JSON.parse(wizard.schema)
137
+ : wizard.schema;
138
+
139
+ // Filter the schema based on selected component types and placeholders
140
+ const filteredSchema = convertPageSchemaToWizardComponents(
141
+ schema,
142
+ data.selectedComponentTypes
143
+ );
144
+
145
+ const localAbortController = new AbortController();
146
+ setAbortController(localAbortController);
147
+
148
+ const result = await executePrompt(
149
+ [
150
+ {
151
+ content: `${internalState.layoutInstructions?.trim()} Reply with a json object of type PageModel = { components: Component[]; message: string; };
152
+ Component = { name: string, type: string; fields: Field[]; placeholder?: string; children?: Component[]; };
153
+ Field = { name: string; value: string; type: string; };
154
+ Generate a descriptive name for each component including the topic.
155
+ Only use component types that are in the page schema.
156
+
157
+ Component types: ${JSON.stringify(
158
+ filteredSchema
159
+ )} Root level component types ${filteredSchema
160
+ .filter((c) => c.allowedOnRoot)
161
+ .map((c) => c.type)
162
+ .join(", ")}
163
+ Tell the user your reasoning in the message field.
164
+ Fill image ids into picture / image fields.
165
+ The language of the page is ${pageItem.descriptor.language}.
166
+ Input data: ${JSON.stringify(data)}`,
167
+
168
+ name: "system",
169
+ role: "system",
170
+ },
171
+ ],
172
+ editContext,
173
+ createWizardAiContext,
174
+ [""],
175
+ true,
176
+ { signal: localAbortController.signal },
177
+ step.aiModel || "o3-mini-low",
178
+ (response) => {
179
+ try {
180
+ const newLayout = JSON.parse(response.content) as WizardPageModel;
181
+
182
+ if (newLayout) {
183
+ setPageModel((prev) => mergeLayout(prev, newLayout));
184
+ }
185
+
186
+ setMessage(newLayout.message);
187
+ } catch (parseError: unknown) {}
188
+ }
189
+ );
190
+
191
+ const pageModel = JSON.parse(result.content);
192
+
193
+ setPageModel((prev) => mergeLayout(prev, pageModel));
194
+ setMessage(pageModel.message);
195
+ setStepCompleted(true);
196
+ } catch (error) {
197
+ console.error(error);
198
+ } finally {
199
+ setIsLoading(false);
200
+ }
201
+ };
202
+
203
+ // Custom instructions panel
204
+ const customInstructionsPanel = () => {
205
+ return (
206
+ <div className="mb-4">
207
+ <h3 className="text-sm font-semibold">Instructions</h3>
208
+ <div className="mb-3 text-xs text-gray-500">
209
+ Provide guidance for the AI when generating your layout
210
+ </div>
211
+ <textarea
212
+ value={internalState.layoutInstructions}
213
+ onChange={(e) =>
214
+ setInternalState({
215
+ ...internalState,
216
+ layoutInstructions: e.target.value,
217
+ })
218
+ }
219
+ placeholder="Example: Make it modern and minimalist. Focus on images. Include a hero section at the top."
220
+ className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 h-48"
221
+ />
222
+ </div>
223
+ );
224
+ };
225
+
226
+ const panels: SplitterPanel[] = [
227
+ {
228
+ name: "settings",
229
+ content: (
230
+ <div className="pr-6">
231
+ <div className="text-sm font-medium">Page item</div>
232
+ <div className="text-xs text-gray-500">{pageItem.path}</div>
233
+ <ComponentTypeSelector
234
+ selectedComponentTypes={data.selectedComponentTypes}
235
+ setSelectedComponentTypes={(selectedTypes) => {
236
+ setData({
237
+ ...data,
238
+ selectedComponentTypes: selectedTypes,
239
+ });
240
+ }}
241
+ schema={wizard.schema}
242
+ data={data}
243
+ setData={setData}
244
+ step={step}
245
+ />
246
+ {customInstructionsPanel()}
247
+ <div className="flex gap-2">
248
+ <ActionButton
249
+ onClick={createLayout}
250
+ disabled={
251
+ isLoading ||
252
+ !data.selectedComponentTypes ||
253
+ data.selectedComponentTypes.length === 0
254
+ }
255
+ isLoading={isLoading}
256
+ loadingText="Working"
257
+ className="flex-1"
258
+ >
259
+ {internalState.componentsCreated
260
+ ? "Delete and Regenerate Layout"
261
+ : "Generate Layout"}
262
+ </ActionButton>
263
+
264
+ {isLoading && (
265
+ <ActionButton
266
+ isLoading={false}
267
+ onClick={() => abortController?.abort("User aborted")}
268
+ >
269
+ Abort
270
+ </ActionButton>
271
+ )}
272
+ </div>
273
+ {message && <div className="mt-2 text-xs">{message}</div>}
274
+ </div>
275
+ ),
276
+ defaultSize: 400,
277
+ },
278
+ {
279
+ name: "layout",
280
+ content: (
281
+ <div
282
+ className={classNames("px-6", pageLoaded ? "h-full" : "h-0")}
283
+ >
284
+ <PageViewer
285
+ name="single"
286
+ mode={editContext?.previewMode ? "view" : "edit"}
287
+ showFormEditor={false}
288
+ followEditsDefault={true}
289
+ pageViewContext={editContext!.pageView}
290
+ />
291
+ </div>
292
+ ),
293
+ defaultSize: "auto",
294
+ },
295
+ ];
296
+
297
+ return <Splitter panels={panels}></Splitter>;
298
+ }
299
+
300
+ /**
301
+ * Merges a new layout with the previous layout
302
+ * - Keeps all previous components
303
+ * - Adds new components
304
+ * - Updates existing fields with new values
305
+ * - Adds new fields
306
+ */
307
+ const mergeLayout = (
308
+ prev: WizardPageModel | null,
309
+ newLayout: WizardPageModel
310
+ ): WizardPageModel => {
311
+ if (!prev) return newLayout;
312
+
313
+ // Merge top-level properties
314
+ const result: WizardPageModel = {
315
+ name: newLayout.name || prev.name,
316
+ message: newLayout.message,
317
+ metaDescription: prev.metaDescription,
318
+ metaKeywords: prev.metaKeywords,
319
+ components: [...(prev.components || [])],
320
+ };
321
+
322
+ // Function to merge components recursively
323
+ const mergeComponents = (
324
+ existingComponents: any[],
325
+ newComponents: any[]
326
+ ): any[] => {
327
+ if (!newComponents?.length) return existingComponents;
328
+
329
+ const result = [...existingComponents];
330
+
331
+ for (const newComp of newComponents) {
332
+ // Try to find a matching component by type and name
333
+ const existingIndex = result.findIndex(
334
+ (comp) => comp.type === newComp.type && comp.name === newComp.name
335
+ );
336
+
337
+ if (existingIndex >= 0) {
338
+ // Update existing component
339
+ const existing = result[existingIndex];
340
+
341
+ // Merge fields - update existing fields and add new ones
342
+ const mergedFields = [...(existing.fields || [])];
343
+ for (const newField of newComp.fields || []) {
344
+ const fieldIndex = mergedFields.findIndex(
345
+ (f) => f.name === newField.name
346
+ );
347
+ if (fieldIndex >= 0) {
348
+ mergedFields[fieldIndex].value = newField.value; // Update existing field
349
+ mergedFields[fieldIndex].type = newField.type;
350
+ } else {
351
+ mergedFields.push(newField); // Add new field
352
+ }
353
+ }
354
+
355
+ // Recursive merge of children components
356
+ const mergedChildren = mergeComponents(
357
+ existing.children || [],
358
+ newComp.children || []
359
+ );
360
+
361
+ // Create updated component
362
+ result[existingIndex] = {
363
+ ...existing,
364
+ fields: mergedFields,
365
+ children: mergedChildren,
366
+ placeholder: newComp.placeholder || existing.placeholder,
367
+ };
368
+ } else {
369
+ // Add new component
370
+ result.push(newComp);
371
+ }
372
+ }
373
+
374
+ return result;
375
+ };
376
+
377
+ // Merge components recursively
378
+ result.components = mergeComponents(
379
+ result.components,
380
+ newLayout.components || []
381
+ );
382
+
383
+ return result;
384
+ };
@@ -0,0 +1,34 @@
1
+ import { WizardField } from "../PageWizard";
2
+
3
+ interface EditButtonProps {
4
+ field: WizardField;
5
+ onEdit: () => void;
6
+ className?: string;
7
+ }
8
+
9
+ export function EditButton({ field, onEdit, className = "" }: EditButtonProps) {
10
+ return (
11
+ <button
12
+ type="button"
13
+ onClick={onEdit}
14
+ className={`inline-flex items-center justify-center p-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${className}`}
15
+ aria-label={`Edit ${field.name || "field"}`}
16
+ title={`Edit ${field.name || "field"}`}
17
+ >
18
+ <svg
19
+ xmlns="http://www.w3.org/2000/svg"
20
+ className="h-4 w-4"
21
+ fill="none"
22
+ viewBox="0 0 24 24"
23
+ stroke="currentColor"
24
+ >
25
+ <path
26
+ strokeLinecap="round"
27
+ strokeLinejoin="round"
28
+ strokeWidth={2}
29
+ d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
30
+ />
31
+ </svg>
32
+ </button>
33
+ );
34
+ }