@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,102 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ import { WizardField } from "../PageWizard";
4
+ import { TextField } from "../../editor/fieldTypes";
5
+
6
+ import { Editor } from "primereact/editor";
7
+
8
+ export function FieldEditor({
9
+ field,
10
+ onFieldEdited,
11
+ }: {
12
+ field: WizardField;
13
+ onFieldEdited: () => void;
14
+ }) {
15
+ const [fieldValue, setFieldValue] = useState<string>("");
16
+ const [isEditing, setIsEditing] = useState<boolean>(false);
17
+
18
+ useEffect(() => {
19
+ if (isEditing) {
20
+ const value = (field as TextField).value;
21
+ setFieldValue(typeof value === "string" ? value : "");
22
+ }
23
+ }, [isEditing]);
24
+
25
+ if (!isEditing) {
26
+ return (
27
+ <div
28
+ onClick={() => setIsEditing(true)}
29
+ className="cursor-pointer mb-2"
30
+ >
31
+ <div className="font-bold text-gray-900">{field.name}:</div>
32
+ <div
33
+ className="text-gray-700 [&_ul]:list-disc [&_ul]:pl-5 [&_li]:my-1 [&_p]:my-1"
34
+ dangerouslySetInnerHTML={{ __html: field.value }}
35
+ ></div>
36
+ {/* <SimpleIconButton
37
+
38
+ label="Edit"
39
+ icon="pi pi-pencil"
40
+ /> */}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ const isRichText = field.type === "Rich Text";
46
+
47
+ const handleSave = () => {
48
+ console.log(`Saving changes to field ${field.name}:`, fieldValue);
49
+ field.value = fieldValue;
50
+ setIsEditing(false);
51
+ onFieldEdited();
52
+ };
53
+
54
+ const handleCancel = () => {
55
+ setIsEditing(false);
56
+ };
57
+
58
+ return (
59
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-1000">
60
+ <div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-lg">
61
+ <h3 className="text-lg font-medium text-gray-900 mb-4">
62
+ Edit {field.name || "Field"}
63
+ </h3>
64
+
65
+ {isRichText ? (
66
+ <div className="mb-4">
67
+ <Editor
68
+ className="w-full h-64 p-2 border border-gray-300 rounded-md"
69
+ value={fieldValue}
70
+ onTextChange={(e) => setFieldValue(e.htmlValue || "")}
71
+ />
72
+ </div>
73
+ ) : (
74
+ <div className="mb-4">
75
+ <textarea
76
+ className="w-full p-2 border border-gray-300 rounded-md"
77
+ value={fieldValue}
78
+ onChange={(e) => setFieldValue(e.target.value)}
79
+ />
80
+ </div>
81
+ )}
82
+
83
+ <div className="flex justify-end space-x-3">
84
+ <button
85
+ type="button"
86
+ onClick={handleCancel}
87
+ className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
88
+ >
89
+ Cancel
90
+ </button>
91
+ <button
92
+ type="button"
93
+ onClick={handleSave}
94
+ className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
95
+ >
96
+ Save
97
+ </button>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,32 @@
1
+ import { classNames } from "primereact/utils";
2
+ import { SparkleIconBig } from "../../editor/ui/Icons";
3
+ import { SparkleIconSmall } from "../../editor/ui/Icons";
4
+
5
+ interface GenerateProps {
6
+ title: string;
7
+ }
8
+
9
+ function Generate({ title }: GenerateProps) {
10
+ console.log("Generate", title);
11
+ return (
12
+ <div
13
+ className={classNames(
14
+ "flex flex-col justify-center items-center h-full transition-all duration-300 animate-fadeIn text-canvas-pink"
15
+ )}
16
+ >
17
+ <div className="mb-20">
18
+ <div className="flex justify-center ml-20 animate-sparkle w-auto min-h-10 delay-700">
19
+ <SparkleIconSmall />
20
+ </div>
21
+ <div className="flex justify-center ml-5 animate-sparkle w-auto min-h-20">
22
+ <SparkleIconBig />
23
+ </div>
24
+ <div className="flex justify-center mr-20 animate-sparkle w-auto min-h-10 delay-500">
25
+ <SparkleIconSmall />
26
+ </div>
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ export default Generate;
@@ -0,0 +1,318 @@
1
+ import { StepComponentProps } from "../../config/types";
2
+ import { useEffect, useState } from "react";
3
+ import { ScrollingContentTree } from "../../editor/ScrollingContentTree";
4
+ import { ItemTreeNodeData } from "../../editor/services/contentService";
5
+ import { Splitter, SplitterPanel } from "../../editor/ui/Splitter";
6
+ import { useEditContext } from "../../editor/client/editContext";
7
+ import { executePrompt, executeSearch } from "../../editor/services/aiService";
8
+ import { createWizardAiContext } from "../service";
9
+ import { ActionButton } from "../../components/ActionButton";
10
+ import { classNames } from "primereact/utils";
11
+ import { Button } from "primereact/button";
12
+ import { Dialog } from "primereact/dialog";
13
+ import { FullItem } from "../../editor/pageModel";
14
+ type Thumbnail = {
15
+ id: string;
16
+ name: string;
17
+ thumbUrl?: string;
18
+ description?: string;
19
+ selected?: boolean;
20
+ };
21
+
22
+ type Image = {
23
+ title: string;
24
+ keywords: string[];
25
+ options?: Thumbnail[];
26
+ };
27
+
28
+ export const ImagesStep = (props: StepComponentProps) => {
29
+ const mediaRootId = "3D6658D8-A0BF-4E75-B3E2-D050FABCF4E1";
30
+ const [selectedFolderId, setSelectedFolderId] = useState<string>(mediaRootId);
31
+ const [isLoading, setIsLoading] = useState<boolean>(false);
32
+ const [error, setError] = useState<string | null>(null);
33
+
34
+ const editContext = useEditContext();
35
+
36
+ const propName = props.step.propertyName || "images";
37
+
38
+ const [showFolderDialog, setShowFolderDialog] = useState<boolean>(false);
39
+
40
+ const [selectedFolder, setSelectedFolder] = useState<FullItem>();
41
+
42
+ useEffect(() => {
43
+ if (props.data[propName]?.length > 0) {
44
+ props.setStepCompleted(true);
45
+ }
46
+ }, [props.data]);
47
+
48
+ const findMatchingImages = async () => {
49
+ setIsLoading(true);
50
+ setError(null);
51
+ props.setData((prevData) => ({
52
+ ...prevData,
53
+ [propName]: [],
54
+ }));
55
+
56
+ try {
57
+ // First, ask AI for keywords based on the content
58
+ const aiPromptResult = await executePrompt(
59
+ [
60
+ {
61
+ content: `You are an AI assistant for building a web page. You will later have to create images for the page.
62
+ Please anaylze the provided data and suggest a number of images and specific keywords per image. The keywords will be used to find matching images in the image library.
63
+ Response JSON format: { images: Image[] } type Image = { title: string, keywords: string[] }
64
+ Input data: ${JSON.stringify(props.data)}
65
+ Instructions: ${props.step.instructions}`,
66
+
67
+ name: "system",
68
+ role: "system",
69
+ },
70
+ ],
71
+ editContext!,
72
+ createWizardAiContext,
73
+ [""],
74
+ true,
75
+ undefined,
76
+ "o3-mini-low",
77
+ );
78
+ const images = JSON.parse(aiPromptResult.content).images;
79
+
80
+ // Create an array of promises for all search operations
81
+ const searchPromises = images.map(async (image: Image) => {
82
+ const searchResult = await executeSearch({
83
+ query: image.keywords.join(" "),
84
+ rootItemIds: selectedFolderId ? [selectedFolderId] : undefined,
85
+ editContext: editContext!,
86
+ maxResults: 6,
87
+ index: "media",
88
+ skipValidation: true,
89
+ });
90
+
91
+ if (searchResult.type === "success") {
92
+ const thumbnails = (searchResult.data as Thumbnail[]).map(
93
+ (thumbnail) => ({
94
+ name: thumbnail.name,
95
+ id: thumbnail.id,
96
+ thumbUrl: thumbnail.thumbUrl,
97
+ description: thumbnail.description,
98
+ }),
99
+ );
100
+
101
+ props.setInternalState((state: any) => ({
102
+ ...state,
103
+ thumbnails: [
104
+ ...(state.thumbnails || []),
105
+ ...thumbnails.filter(
106
+ (newThumb: Thumbnail) =>
107
+ !(state.thumbnails || []).some(
108
+ (existingThumb: Thumbnail) =>
109
+ existingThumb.id === newThumb.id,
110
+ ),
111
+ ),
112
+ ],
113
+ }));
114
+
115
+ props.setData((prevData) => ({
116
+ ...prevData,
117
+
118
+ [propName]: [
119
+ ...(prevData[propName] || []),
120
+ {
121
+ ...image,
122
+ options: thumbnails,
123
+ },
124
+ ],
125
+ }));
126
+ } else {
127
+ setError(
128
+ `Error searching for images: ${searchResult.response.statusText}`,
129
+ );
130
+ }
131
+ });
132
+
133
+ // Wait for all search operations to complete
134
+ await Promise.all(searchPromises);
135
+ } catch (err) {
136
+ setError(
137
+ `Error finding matching images: ${
138
+ err instanceof Error ? err.message : String(err)
139
+ }`,
140
+ );
141
+ console.error("Error in findMatchingImages:", err);
142
+ } finally {
143
+ setIsLoading(false);
144
+ }
145
+ };
146
+
147
+ useEffect(() => {
148
+ const loadFolder = async () => {
149
+ if (selectedFolderId) {
150
+ const item = await editContext?.itemsRepository.getItem({
151
+ id: selectedFolderId,
152
+ language: "en",
153
+ version: 1,
154
+ });
155
+ console.log("ITEM", item);
156
+ setSelectedFolder(item);
157
+ }
158
+ };
159
+ loadFolder();
160
+ }, [selectedFolderId]);
161
+
162
+ console.log("FOLDER", selectedFolder, selectedFolderId);
163
+
164
+ const panels: SplitterPanel[] = [
165
+ {
166
+ name: "Settings",
167
+ defaultSize: 400,
168
+ content: (
169
+ <div className="flex h-full flex-col gap-2 pr-6 pb-4">
170
+ <div className="relative mb-2 rounded-md border border-gray-400 bg-white p-4">
171
+ <div className="mb-2 text-sm font-bold text-gray-800">
172
+ Image library folder
173
+ </div>
174
+ {selectedFolder && (
175
+ <div className="mb-2 text-sm text-gray-500">
176
+ {selectedFolder.path}
177
+ </div>
178
+ )}
179
+ <Button
180
+ label="Change Folder"
181
+ onClick={() => {
182
+ setShowFolderDialog(true);
183
+ }}
184
+ />
185
+ <Dialog
186
+ header="Select Folder"
187
+ visible={showFolderDialog}
188
+ onHide={() => {
189
+ setShowFolderDialog(false);
190
+ }}
191
+ style={{ width: "50vw", height: "50vh" }}
192
+ >
193
+ <div className="relative flex h-full flex-col">
194
+ <ScrollingContentTree
195
+ rootItemId={mediaRootId}
196
+ onSelectionChange={(selection) => {
197
+ const selectedNode = selection[0] as ItemTreeNodeData;
198
+ if (selectedNode) setSelectedFolderId(selectedNode.id);
199
+ else setSelectedFolderId(mediaRootId);
200
+ setShowFolderDialog(false);
201
+ }}
202
+ />
203
+ </div>
204
+ </Dialog>
205
+ </div>
206
+ <ActionButton
207
+ onClick={findMatchingImages}
208
+ isLoading={isLoading}
209
+ disabled={isLoading}
210
+ loadingText="Searching..."
211
+ >
212
+ Find Matching Images
213
+ </ActionButton>
214
+ </div>
215
+ ),
216
+ },
217
+ {
218
+ name: "Images",
219
+ defaultSize: "auto",
220
+ content: (
221
+ <div className="absolute inset-0 overflow-auto">
222
+ <div className="flex flex-col gap-4 p-4 pt-0">
223
+ <div className="flex items-center gap-4">
224
+ {error && <div className="text-red-500">{error}</div>}
225
+
226
+ {props.data[propName]?.length > 0 && (
227
+ <div className="flex flex-col gap-4">
228
+ {props.data[propName]?.map((image: Image, index: number) => (
229
+ <span
230
+ key={index}
231
+ className={classNames(
232
+ "rounded border border-gray-400 bg-white p-4 text-sm",
233
+ index % 2 === 0
234
+ ? "animate-fadeRight"
235
+ : "animate-fadeLeft",
236
+ )}
237
+ >
238
+ <div className="mb-3 text-lg">{image.title}</div>
239
+ <div className="flex flex-wrap gap-5">
240
+ {image.options?.map(
241
+ (option: Thumbnail, thumbnailIndex: number) => (
242
+ <div
243
+ key={thumbnailIndex}
244
+ className="relative flex h-28 w-28 cursor-pointer items-center justify-center border border-gray-400"
245
+ onClick={() => {
246
+ props.setData((prevData) => ({
247
+ ...prevData,
248
+ [propName]: prevData[propName].map(
249
+ (img: Image, imgIndex: number) =>
250
+ ({
251
+ ...img,
252
+ options:
253
+ imgIndex !== index
254
+ ? img.options
255
+ : img.options?.map(
256
+ (
257
+ option,
258
+ optionIndex: number,
259
+ ) =>
260
+ optionIndex === thumbnailIndex
261
+ ? {
262
+ ...option,
263
+ selected:
264
+ !option.selected,
265
+ }
266
+ : option,
267
+ ),
268
+ }) as Image,
269
+ ),
270
+ }));
271
+ }}
272
+ >
273
+ <img
274
+ key={thumbnailIndex}
275
+ src={option.thumbUrl}
276
+ alt={option.name}
277
+ />
278
+ <div
279
+ className={classNames(
280
+ "peer ring-offset-background focus-visible:ring-ring pointer-events-none absolute right-1 bottom-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
281
+ option.selected
282
+ ? "text-canvas-pink border-canvas-pink bg-white"
283
+ : "",
284
+ )}
285
+ >
286
+ {option.selected && (
287
+ <svg
288
+ xmlns="http://www.w3.org/2000/svg"
289
+ width="24"
290
+ height="24"
291
+ viewBox="0 0 24 24"
292
+ fill="none"
293
+ stroke="currentColor"
294
+ strokeWidth="2"
295
+ strokeLinecap="round"
296
+ strokeLinejoin="round"
297
+ >
298
+ <path d="M20 6 9 17l-5-5"></path>
299
+ </svg>
300
+ )}
301
+ </div>
302
+ </div>
303
+ ),
304
+ )}
305
+ </div>
306
+ </span>
307
+ ))}
308
+ </div>
309
+ )}
310
+ </div>
311
+ </div>
312
+ </div>
313
+ ),
314
+ },
315
+ ];
316
+
317
+ return <Splitter panels={panels} />;
318
+ };
@@ -0,0 +1,228 @@
1
+ import { useEffect, useState } from "react";
2
+ import { WizardData, WizardPageModel } from "../PageWizard";
3
+
4
+ import { useEditContext } from "../../editor/client/editContext";
5
+ import { executePrompt } from "../../editor/services/aiService";
6
+ import { createWizardAiContext } from "../service";
7
+ import { Components } from "./Components";
8
+ import { Splitter, SplitterPanel } from "../../editor/ui/Splitter";
9
+ import { convertPageSchemaToWizardComponents } from "./schema";
10
+ import { StepComponentProps } from "../../config/types";
11
+ import { ActionButton } from "../../components/ActionButton";
12
+ import { ComponentTypeSelector } from "./ComponentTypesSelector";
13
+ export function LayoutStep({
14
+ wizard,
15
+ step,
16
+ data,
17
+ setData,
18
+ setStepCompleted,
19
+ internalState,
20
+ setInternalState,
21
+ }: StepComponentProps) {
22
+ const editContext = useEditContext();
23
+ const abortController = new AbortController();
24
+
25
+ const localAbortController = abortController || new AbortController();
26
+ const [isLoading, setIsLoading] = useState(false);
27
+
28
+ const [message, setMessage] = useState<string>();
29
+
30
+ const [selectedComponentTypes, setSelectedComponentTypes] = useState<
31
+ string[]
32
+ >([]);
33
+
34
+ const [customInstructions, setCustomInstructions] = useState<string>();
35
+
36
+ if (!editContext) {
37
+ return null;
38
+ }
39
+
40
+ useEffect(() => {
41
+ if (data.pageModel?.components?.length) {
42
+ setStepCompleted(!isLoading);
43
+ }
44
+ }, [data.pageModel, isLoading]);
45
+
46
+ useEffect(() => {
47
+ setCustomInstructions(step.instructions);
48
+ }, []);
49
+
50
+ const createLayout = async () => {
51
+ try {
52
+ setIsLoading(true);
53
+
54
+ // Parse schema if it's a string
55
+ const schema =
56
+ typeof wizard.schema === "string"
57
+ ? JSON.parse(wizard.schema)
58
+ : wizard.schema;
59
+
60
+ // Filter the schema based on selected component types and placeholders
61
+ const filteredSchema = convertPageSchemaToWizardComponents(
62
+ schema,
63
+ selectedComponentTypes
64
+ );
65
+
66
+ const result = await executePrompt(
67
+ [
68
+ {
69
+ content: `${customInstructions?.trim()} Reply with a json object of type PageModel = { name: string; metaDescription: string; components: Component[]; message: string; };
70
+ Component = { name: string, type: string; fields: Field[]; placeholder?: string; children?: Component[]; };
71
+ Field = { name: string; value: string; type: string; };
72
+ Generate a descriptive name for each component including the topic.
73
+ Only use component types that are in the page schema. Also suggest a page name following default sitecore item naming conventions (no underscores, no white space, no umlaute, no special characters, hyphens are allowed) and the page meta description.
74
+ Component types: ${JSON.stringify(
75
+ filteredSchema
76
+ )} Root level component types ${filteredSchema
77
+ .filter((c) => c.allowedOnRoot)
78
+ .map((c) => c.type)
79
+ .join(", ")}
80
+ Tell the user your reasoning in the message field.
81
+ Fill image ids into picture / image fields.
82
+ Input data: ${JSON.stringify(data)}`,
83
+
84
+ name: "system",
85
+ role: "system",
86
+ },
87
+ ],
88
+ editContext,
89
+ createWizardAiContext,
90
+ [""],
91
+ true,
92
+ { signal: localAbortController.signal },
93
+ "o3-mini-low",
94
+ (response) => {
95
+ try {
96
+ const newLayout = JSON.parse(response.content) as WizardPageModel;
97
+
98
+ if (newLayout) {
99
+ setData((prev: WizardData) => ({
100
+ ...prev,
101
+ pageModel: newLayout,
102
+ }));
103
+ }
104
+
105
+ setMessage(newLayout.message);
106
+ } catch (parseError: unknown) {}
107
+ }
108
+ );
109
+
110
+ const pageModel = JSON.parse(result.content);
111
+
112
+ setData({
113
+ ...data,
114
+ pageModel: JSON.parse(result.content),
115
+ customInstructions,
116
+ });
117
+
118
+ setMessage(pageModel.message);
119
+ } catch (error) {
120
+ console.error(error);
121
+ } finally {
122
+ setIsLoading(false);
123
+ }
124
+ };
125
+
126
+ // Toggle placeholder selection
127
+
128
+ // Regenerate layout with current selected component types and placeholders
129
+ const regenerateLayout = () => {
130
+ createLayout();
131
+ };
132
+
133
+ useEffect(() => {
134
+ if (!selectedComponentTypes.length) return;
135
+
136
+ // Only set custom instructions if they're not already set or if step.instructions has changed
137
+ if (customInstructions !== step.instructions) {
138
+ setCustomInstructions(step.instructions);
139
+ }
140
+ }, [step.instructions, selectedComponentTypes]);
141
+
142
+ // Custom instructions panel
143
+ const customInstructionsPanel = () => {
144
+ return (
145
+ <div className="mb-4">
146
+ <h3 className="text-sm font-bold">Instructions</h3>
147
+ <div className="mb-3 text-xs text-gray-500">
148
+ Provide guidance for the AI when generating your layout
149
+ </div>
150
+ <textarea
151
+ value={customInstructions}
152
+ onChange={(e) => setCustomInstructions(e.target.value)}
153
+ placeholder="Example: Make it modern and minimalist. Focus on images. Include a hero section at the top."
154
+ 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"
155
+ disabled={isLoading}
156
+ />
157
+ </div>
158
+ );
159
+ };
160
+
161
+ const settingsPanel: SplitterPanel = {
162
+ name: "Settings",
163
+ collapsible: false,
164
+ defaultSize: 450,
165
+ content: (
166
+ <div className="absolute inset-0 overflow-auto flex flex-col h-full pr-4">
167
+ {customInstructionsPanel()}
168
+ <ComponentTypeSelector
169
+ selectedComponentTypes={selectedComponentTypes}
170
+ setSelectedComponentTypes={setSelectedComponentTypes}
171
+ schema={wizard.schema}
172
+ data={data}
173
+ setData={setData}
174
+ step={step}
175
+ />
176
+ <div className="flex gap-2">
177
+ <ActionButton
178
+ onClick={regenerateLayout}
179
+ disabled={isLoading || selectedComponentTypes.length === 0}
180
+ isLoading={isLoading}
181
+ loadingText="Working"
182
+ className="flex-1"
183
+ >
184
+ {data.pageModel ? "Regenerate Layout" : "Generate Layout"}
185
+ </ActionButton>
186
+ {/* Abort button - only visible when loading */}
187
+ {isLoading && (
188
+ <button
189
+ onClick={() => {
190
+ localAbortController.abort();
191
+ setIsLoading(false);
192
+ }}
193
+ className="bg-red-500 text-white px-2 py-1 rounded text-xs hover:bg-red-600 flex items-center gap-1"
194
+ >
195
+ <span className="pi pi-times"></span>
196
+ Abort
197
+ </button>
198
+ )}
199
+ </div>
200
+ <div className="text-xs text-gray-500 p-2">{message}</div>
201
+ </div>
202
+ ),
203
+ };
204
+
205
+ const layoutPanel: SplitterPanel = {
206
+ name: "Layout",
207
+ defaultSize: "auto",
208
+ collapsible: false,
209
+ content: (
210
+ <div className="absolute inset-2 flex flex-col justify-center items-center text-gray-600">
211
+ <Components
212
+ pageModel={data.pageModel}
213
+ onComponentRemoved={() => setData({ ...data })}
214
+ onFieldEdited={() => setData({ ...data })}
215
+ thumbnails={internalState.thumbnails}
216
+ setInternalState={setInternalState}
217
+ />
218
+ </div>
219
+ ),
220
+ };
221
+
222
+ return (
223
+ <Splitter
224
+ panels={[settingsPanel, layoutPanel]}
225
+ localStorageKey="editor.page-wizard.layout"
226
+ />
227
+ );
228
+ }