@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.
- package/.prettierrc +3 -0
- package/eslint.config.mjs +4 -0
- package/images/bg-shape-black.webp +0 -0
- package/package.json +52 -0
- package/src/client-components/api.ts +6 -0
- package/src/client-components/index.ts +19 -0
- package/src/components/ActionButton.tsx +43 -0
- package/src/components/Error.tsx +57 -0
- package/src/config/config.tsx +737 -0
- package/src/config/types.ts +263 -0
- package/src/editor/ComponentInfo.tsx +77 -0
- package/src/editor/ConfirmationDialog.tsx +103 -0
- package/src/editor/ContentTree.tsx +654 -0
- package/src/editor/ContextMenu.tsx +155 -0
- package/src/editor/Editor.tsx +91 -0
- package/src/editor/EditorWarning.tsx +34 -0
- package/src/editor/EditorWarnings.tsx +33 -0
- package/src/editor/FieldEditorPopup.tsx +65 -0
- package/src/editor/FieldHistory.tsx +74 -0
- package/src/editor/FieldList.tsx +190 -0
- package/src/editor/FieldListField.tsx +387 -0
- package/src/editor/FieldListFieldWithFallbacks.tsx +211 -0
- package/src/editor/FloatingToolbar.tsx +163 -0
- package/src/editor/ImageEditor.tsx +129 -0
- package/src/editor/InsertMenu.tsx +332 -0
- package/src/editor/ItemInfo.tsx +90 -0
- package/src/editor/LinkEditorDialog.tsx +192 -0
- package/src/editor/MainLayout.tsx +94 -0
- package/src/editor/NewEditorClient.tsx +11 -0
- package/src/editor/PictureCropper.tsx +505 -0
- package/src/editor/PictureEditor.tsx +206 -0
- package/src/editor/PictureEditorDialog.tsx +381 -0
- package/src/editor/PublishDialog.ignore +74 -0
- package/src/editor/ScrollingContentTree.tsx +47 -0
- package/src/editor/Terminal.tsx +215 -0
- package/src/editor/Titlebar.tsx +23 -0
- package/src/editor/ai/AiPopup.tsx +59 -0
- package/src/editor/ai/AiResponseMessage.tsx +82 -0
- package/src/editor/ai/AiTerminal.tsx +450 -0
- package/src/editor/ai/AiToolCall.tsx +46 -0
- package/src/editor/ai/EditorAiTerminal.tsx +20 -0
- package/src/editor/ai/editorAiContext.ts +18 -0
- package/src/editor/client/DialogContext.tsx +49 -0
- package/src/editor/client/EditorClient.tsx +1831 -0
- package/src/editor/client/GenericDialog.tsx +50 -0
- package/src/editor/client/editContext.ts +330 -0
- package/src/editor/client/helpers.ts +44 -0
- package/src/editor/client/itemsRepository.ts +391 -0
- package/src/editor/client/operations.ts +610 -0
- package/src/editor/client/pageModelBuilder.ts +182 -0
- package/src/editor/commands/commands.ts +23 -0
- package/src/editor/commands/componentCommands.tsx +408 -0
- package/src/editor/commands/createVersionCommand.ts +33 -0
- package/src/editor/commands/deleteVersionCommand.ts +71 -0
- package/src/editor/commands/itemCommands.tsx +186 -0
- package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
- package/src/editor/commands/undo.ts +39 -0
- package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
- package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
- package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
- package/src/editor/component-designer/ComponentEditor.tsx +97 -0
- package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
- package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
- package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
- package/src/editor/component-designer/PlaceholdersEditor.tsx +183 -0
- package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
- package/src/editor/component-designer/TemplateEditor.tsx +236 -0
- package/src/editor/component-designer/aiContext.ts +23 -0
- package/src/editor/componentTreeHelper.tsx +114 -0
- package/src/editor/control-center/ControlCenterMenu.tsx +71 -0
- package/src/editor/control-center/IndexOverview.tsx +50 -0
- package/src/editor/control-center/IndexSettings.tsx +266 -0
- package/src/editor/control-center/Status.tsx +7 -0
- package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
- package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
- package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
- package/src/editor/editor-warnings/NoWriteAccess.tsx +15 -0
- package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
- package/src/editor/field-types/AttachmentEditor.tsx +9 -0
- package/src/editor/field-types/CheckboxEditor.tsx +47 -0
- package/src/editor/field-types/DropLinkEditor.tsx +75 -0
- package/src/editor/field-types/DropListEditor.tsx +84 -0
- package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
- package/src/editor/field-types/InternalLinkFieldEditor.tsx +112 -0
- package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
- package/src/editor/field-types/MultiLineText.tsx +63 -0
- package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
- package/src/editor/field-types/RawEditor.tsx +53 -0
- package/src/editor/field-types/ReactQuill.tsx +580 -0
- package/src/editor/field-types/RichTextEditor.tsx +22 -0
- package/src/editor/field-types/RichTextEditorComponent.tsx +108 -0
- package/src/editor/field-types/SingleLineText.tsx +150 -0
- package/src/editor/field-types/TreeListEditor.tsx +261 -0
- package/src/editor/fieldTypes.ts +140 -0
- package/src/editor/media-selector/AiImageSearch.tsx +186 -0
- package/src/editor/media-selector/AiImageSearchPrompt.tsx +95 -0
- package/src/editor/media-selector/MediaSelector.tsx +42 -0
- package/src/editor/media-selector/Preview.tsx +14 -0
- package/src/editor/media-selector/Thumbnails.tsx +48 -0
- package/src/editor/media-selector/TreeSelector.tsx +292 -0
- package/src/editor/media-selector/UploadZone.tsx +137 -0
- package/src/editor/menubar/ActionsMenu.tsx +47 -0
- package/src/editor/menubar/ActiveUsers.tsx +17 -0
- package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
- package/src/editor/menubar/BrowseHistory.tsx +37 -0
- package/src/editor/menubar/ItemLanguageVersion.tsx +52 -0
- package/src/editor/menubar/LanguageSelector.tsx +152 -0
- package/src/editor/menubar/Menu.tsx +83 -0
- package/src/editor/menubar/NavButtons.tsx +74 -0
- package/src/editor/menubar/PageSelector.tsx +139 -0
- package/src/editor/menubar/PageViewerControls.tsx +99 -0
- package/src/editor/menubar/Separator.tsx +12 -0
- package/src/editor/menubar/SiteInfo.tsx +53 -0
- package/src/editor/menubar/User.tsx +27 -0
- package/src/editor/menubar/VersionSelector.tsx +143 -0
- package/src/editor/page-editor-chrome/CommentHighlighting.tsx +287 -0
- package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
- package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +44 -0
- package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
- package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
- package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
- package/src/editor/page-editor-chrome/FrameMenu.tsx +263 -0
- package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
- package/src/editor/page-editor-chrome/InlineEditor.tsx +147 -0
- package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
- package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
- package/src/editor/page-editor-chrome/PageEditorChrome.tsx +119 -0
- package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +154 -0
- package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +171 -0
- package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +233 -0
- package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
- package/src/editor/page-viewer/EditorForm.tsx +247 -0
- package/src/editor/page-viewer/MiniMap.tsx +351 -0
- package/src/editor/page-viewer/PageViewer.tsx +127 -0
- package/src/editor/page-viewer/PageViewerFrame.tsx +1030 -0
- package/src/editor/page-viewer/pageViewContext.ts +186 -0
- package/src/editor/pageModel.ts +191 -0
- package/src/editor/picture-shared.tsx +53 -0
- package/src/editor/reviews/Comment.tsx +265 -0
- package/src/editor/reviews/Comments.tsx +50 -0
- package/src/editor/reviews/PreviewInfo.tsx +35 -0
- package/src/editor/reviews/Reviews.tsx +280 -0
- package/src/editor/reviews/reviewCommands.tsx +47 -0
- package/src/editor/reviews/useReviews.tsx +70 -0
- package/src/editor/services/aiService.ts +155 -0
- package/src/editor/services/componentDesignerService.ts +151 -0
- package/src/editor/services/contentService.ts +159 -0
- package/src/editor/services/editService.ts +462 -0
- package/src/editor/services/indexService.ts +24 -0
- package/src/editor/services/reviewsService.ts +45 -0
- package/src/editor/services/serviceHelper.ts +95 -0
- package/src/editor/services/systemService.ts +5 -0
- package/src/editor/services/translationService.ts +21 -0
- package/src/editor/services-server/api.ts +150 -0
- package/src/editor/services-server/graphQL.ts +106 -0
- package/src/editor/sidebar/ComponentPalette.tsx +146 -0
- package/src/editor/sidebar/ComponentTree.tsx +512 -0
- package/src/editor/sidebar/ComponentTree2.tsxx +490 -0
- package/src/editor/sidebar/Debug.tsx +105 -0
- package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
- package/src/editor/sidebar/EditHistory.tsx +134 -0
- package/src/editor/sidebar/GraphQL.tsx +164 -0
- package/src/editor/sidebar/Insert.tsx +35 -0
- package/src/editor/sidebar/MainContentTree.tsx +95 -0
- package/src/editor/sidebar/Performance.tsx +53 -0
- package/src/editor/sidebar/Sessions.tsx +35 -0
- package/src/editor/sidebar/Sidebar.tsx +20 -0
- package/src/editor/sidebar/SidebarView.tsx +150 -0
- package/src/editor/sidebar/Translations.tsx +276 -0
- package/src/editor/sidebar/Validation.tsx +102 -0
- package/src/editor/sidebar/ViewSelector.tsx +49 -0
- package/src/editor/sidebar/Workbox.tsx +209 -0
- package/src/editor/ui/CenteredMessage.tsx +7 -0
- package/src/editor/ui/CopyToClipboardButton.tsx +23 -0
- package/src/editor/ui/DialogButtons.tsx +11 -0
- package/src/editor/ui/Icons.tsx +585 -0
- package/src/editor/ui/ItemNameDialog.tsx +94 -0
- package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
- package/src/editor/ui/ItemSearch.tsx +173 -0
- package/src/editor/ui/PerfectTree.tsx +550 -0
- package/src/editor/ui/Section.tsx +35 -0
- package/src/editor/ui/SimpleIconButton.tsx +43 -0
- package/src/editor/ui/SimpleMenu.tsx +48 -0
- package/src/editor/ui/SimpleTable.tsx +63 -0
- package/src/editor/ui/SimpleTabs.tsx +55 -0
- package/src/editor/ui/SimpleToolbar.tsx +7 -0
- package/src/editor/ui/Spinner.tsx +7 -0
- package/src/editor/ui/Splitter.tsx +247 -0
- package/src/editor/ui/StackedPanels.tsx +134 -0
- package/src/editor/ui/Toolbar.tsx +7 -0
- package/src/editor/utils/id-helper.ts +3 -0
- package/src/editor/utils/insertOptions.ts +69 -0
- package/src/editor/utils/itemutils.ts +29 -0
- package/src/editor/utils/useMemoDebug.ts +28 -0
- package/src/editor/utils.ts +435 -0
- package/src/editor/views/CompareView.tsx +256 -0
- package/src/editor/views/EditView.tsx +27 -0
- package/src/editor/views/ItemEditor.tsx +58 -0
- package/src/editor/views/SingleEditView.tsx +44 -0
- package/src/fonts/Geist-Black.woff2 +0 -0
- package/src/fonts/Geist-Bold.woff2 +0 -0
- package/src/fonts/Geist-ExtraBold.woff2 +0 -0
- package/src/fonts/Geist-ExtraLight.woff2 +0 -0
- package/src/fonts/Geist-Light.woff2 +0 -0
- package/src/fonts/Geist-Medium.woff2 +0 -0
- package/src/fonts/Geist-Regular.woff2 +0 -0
- package/src/fonts/Geist-SemiBold.woff2 +0 -0
- package/src/fonts/Geist-Thin.woff2 +0 -0
- package/src/fonts/Geist[wght].woff2 +0 -0
- package/src/index.ts +7 -0
- package/src/page-wizard/PageWizard.tsx +163 -0
- package/src/page-wizard/SelectWizard.tsx +109 -0
- package/src/page-wizard/WizardSteps.tsx +207 -0
- package/src/page-wizard/service.ts +35 -0
- package/src/page-wizard/startPageWizardCommand.ts +27 -0
- package/src/page-wizard/steps/BuildPageStep.tsx +266 -0
- package/src/page-wizard/steps/CollectStep.tsx +233 -0
- package/src/page-wizard/steps/ComponentTypesSelector.tsx +443 -0
- package/src/page-wizard/steps/Components.tsx +193 -0
- package/src/page-wizard/steps/CreatePage.tsx +285 -0
- package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +384 -0
- package/src/page-wizard/steps/EditButton.tsx +34 -0
- package/src/page-wizard/steps/FieldEditor.tsx +102 -0
- package/src/page-wizard/steps/Generate.tsx +32 -0
- package/src/page-wizard/steps/ImagesStep.tsx +318 -0
- package/src/page-wizard/steps/LayoutStep.tsx +228 -0
- package/src/page-wizard/steps/SelectStep.tsx +256 -0
- package/src/page-wizard/steps/schema.ts +180 -0
- package/src/page-wizard/steps/usePageCreator.ts +279 -0
- package/src/splash-screen/NewPage.tsx +232 -0
- package/src/splash-screen/SectionHeadline.tsx +21 -0
- package/src/splash-screen/SplashScreen.tsx +156 -0
- package/src/tour/Tour.tsx +558 -0
- package/src/tour/default-tour.tsx +300 -0
- package/src/tour/preview-tour.tsx +127 -0
- package/src/types.ts +302 -0
- package/styles.css +476 -0
- package/tsconfig.build.json +21 -0
- 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
|
+
}
|