@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,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
|
+
}
|