@alpaca-editor/core 1.0.3941 → 1.0.3943

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/.prettierrc +3 -0
  2. package/build.css +3 -0
  3. package/components.json +21 -0
  4. package/dist/editor/ContentTree.d.ts +2 -1
  5. package/dist/editor/ContentTree.js +23 -21
  6. package/dist/editor/ContentTree.js.map +1 -1
  7. package/dist/editor/FieldActionsOverlay.js +0 -2
  8. package/dist/editor/FieldActionsOverlay.js.map +1 -1
  9. package/dist/editor/ScrollingContentTree.js +1 -1
  10. package/dist/editor/ScrollingContentTree.js.map +1 -1
  11. package/dist/editor/Titlebar.js +1 -1
  12. package/dist/editor/Titlebar.js.map +1 -1
  13. package/dist/editor/ai/GhostWriter.js +24 -3
  14. package/dist/editor/ai/GhostWriter.js.map +1 -1
  15. package/dist/editor/client/EditorClient.js +7 -7
  16. package/dist/editor/client/EditorClient.js.map +1 -1
  17. package/dist/editor/field-types/InternalLinkFieldEditor.js +60 -10
  18. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  19. package/dist/editor/media-selector/MediaFolderBrowser.js +48 -1
  20. package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
  21. package/dist/editor/menubar/PageSelector.js +116 -65
  22. package/dist/editor/menubar/PageSelector.js.map +1 -1
  23. package/dist/editor/page-viewer/EditorForm.js +5 -2
  24. package/dist/editor/page-viewer/EditorForm.js.map +1 -1
  25. package/dist/editor/ui/ItemSearch.js +14 -8
  26. package/dist/editor/ui/ItemSearch.js.map +1 -1
  27. package/dist/editor/ui/PerfectTree.d.ts +4 -2
  28. package/dist/editor/ui/PerfectTree.js +78 -4
  29. package/dist/editor/ui/PerfectTree.js.map +1 -1
  30. package/dist/editor/ui/Splitter.js +1 -1
  31. package/dist/revision.d.ts +2 -2
  32. package/dist/revision.js +2 -2
  33. package/dist/styles.css +8 -2
  34. package/eslint.config.mjs +4 -0
  35. package/images/bg-shape-black.webp +0 -0
  36. package/images/wizard-bg.png +0 -0
  37. package/images/wizard-tour.png +0 -0
  38. package/images/wizard.png +0 -0
  39. package/package.json +2 -8
  40. package/src/client-components/api.ts +6 -0
  41. package/src/client-components/index.ts +19 -0
  42. package/src/components/ActionButton.tsx +50 -0
  43. package/src/components/Error.tsx +57 -0
  44. package/src/components/ui/CardConnector.tsx +56 -0
  45. package/src/components/ui/button.tsx +62 -0
  46. package/src/components/ui/card.tsx +372 -0
  47. package/src/components/ui/context-menu.tsx +250 -0
  48. package/src/config/config.tsx +917 -0
  49. package/src/config/types.ts +286 -0
  50. package/src/editor/ComponentInfo.tsx +90 -0
  51. package/src/editor/ConfirmationDialog.tsx +103 -0
  52. package/src/editor/ContentTree.tsx +733 -0
  53. package/src/editor/ContextMenu.tsx +230 -0
  54. package/src/editor/Editor.tsx +90 -0
  55. package/src/editor/EditorWarning.tsx +34 -0
  56. package/src/editor/EditorWarnings.tsx +33 -0
  57. package/src/editor/FieldActionsOverlay.tsx +296 -0
  58. package/src/editor/FieldEditorPopup.tsx +65 -0
  59. package/src/editor/FieldHistory.tsx +75 -0
  60. package/src/editor/FieldList.tsx +190 -0
  61. package/src/editor/FieldListField.tsx +391 -0
  62. package/src/editor/FieldListFieldWithFallbacks.tsx +217 -0
  63. package/src/editor/FloatingToolbar.tsx +163 -0
  64. package/src/editor/ImageEditor.tsx +128 -0
  65. package/src/editor/ItemInfo.tsx +90 -0
  66. package/src/editor/LinkEditorDialog.tsx +196 -0
  67. package/src/editor/MainLayout.tsx +95 -0
  68. package/src/editor/MobileLayout.tsx +68 -0
  69. package/src/editor/NewEditorClient.tsx +11 -0
  70. package/src/editor/PictureCropper.tsx +568 -0
  71. package/src/editor/PictureEditor.tsx +301 -0
  72. package/src/editor/PictureEditorDialog.tsx +381 -0
  73. package/src/editor/PublishDialog.ignore +74 -0
  74. package/src/editor/ScrollingContentTree.tsx +68 -0
  75. package/src/editor/Terminal.tsx +227 -0
  76. package/src/editor/Titlebar.tsx +104 -0
  77. package/src/editor/ai/AiPopup.tsx +59 -0
  78. package/src/editor/ai/AiResponseMessage.tsx +106 -0
  79. package/src/editor/ai/AiTerminal.tsx +503 -0
  80. package/src/editor/ai/AiToolCall.tsx +61 -0
  81. package/src/editor/ai/EditorAiTerminal.tsx +20 -0
  82. package/src/editor/ai/GhostWriter.tsx +480 -0
  83. package/src/editor/ai/aiPageModel.ts +108 -0
  84. package/src/editor/ai/editorAiContext.ts +18 -0
  85. package/src/editor/client/AboutDialog.tsx +44 -0
  86. package/src/editor/client/EditorClient.tsx +2241 -0
  87. package/src/editor/client/GenericDialog.tsx +50 -0
  88. package/src/editor/client/editContext.ts +416 -0
  89. package/src/editor/client/helpers.ts +44 -0
  90. package/src/editor/client/itemsRepository.ts +574 -0
  91. package/src/editor/client/operations.ts +768 -0
  92. package/src/editor/client/pageModelBuilder.ts +219 -0
  93. package/src/editor/commands/commands.ts +22 -0
  94. package/src/editor/commands/componentCommands.tsx +431 -0
  95. package/src/editor/commands/createVersionCommand.ts +33 -0
  96. package/src/editor/commands/deleteVersionCommand.ts +71 -0
  97. package/src/editor/commands/itemCommands.tsx +351 -0
  98. package/src/editor/commands/localizeItem/LocalizeItemDialog.tsx +201 -0
  99. package/src/editor/commands/localizeItem/LocalizeItemUtils.ts +27 -0
  100. package/src/editor/commands/undo.ts +39 -0
  101. package/src/editor/component-designer/ComponentDesigner.tsx +70 -0
  102. package/src/editor/component-designer/ComponentDesignerAiTerminal.tsx +11 -0
  103. package/src/editor/component-designer/ComponentDesignerMenu.tsx +91 -0
  104. package/src/editor/component-designer/ComponentEditor.tsx +97 -0
  105. package/src/editor/component-designer/ComponentRenderingCodeEditor.tsx +31 -0
  106. package/src/editor/component-designer/ComponentRenderingEditor.tsx +104 -0
  107. package/src/editor/component-designer/ComponentsDropdown.tsx +39 -0
  108. package/src/editor/component-designer/PlaceholdersEditor.tsx +179 -0
  109. package/src/editor/component-designer/RenderingsDropdown.tsx +36 -0
  110. package/src/editor/component-designer/TemplateEditor.tsx +236 -0
  111. package/src/editor/component-designer/aiContext.ts +23 -0
  112. package/src/editor/componentTreeHelper.tsx +116 -0
  113. package/src/editor/context-menu/CopyMoveMenu.tsx +103 -0
  114. package/src/editor/context-menu/InsertMenu.tsx +347 -0
  115. package/src/editor/control-center/About.tsx +342 -0
  116. package/src/editor/control-center/ControlCenterMenu.tsx +76 -0
  117. package/src/editor/control-center/IndexOverview.tsx +50 -0
  118. package/src/editor/control-center/IndexSettings.tsx +266 -0
  119. package/src/editor/control-center/Info.tsx +104 -0
  120. package/src/editor/control-center/QuotaInfo.tsx +301 -0
  121. package/src/editor/control-center/Status.tsx +113 -0
  122. package/src/editor/control-center/WebSocketMessages.tsx +155 -0
  123. package/src/editor/editor-warnings/ItemLocked.tsx +63 -0
  124. package/src/editor/editor-warnings/NoLanguageWriteAccess.tsx +22 -0
  125. package/src/editor/editor-warnings/NoWorkflowWriteAccess.tsx +23 -0
  126. package/src/editor/editor-warnings/NoWriteAccess.tsx +16 -0
  127. package/src/editor/editor-warnings/ValidationErrors.tsx +54 -0
  128. package/src/editor/field-types/AttachmentEditor.tsx +9 -0
  129. package/src/editor/field-types/CheckboxEditor.tsx +47 -0
  130. package/src/editor/field-types/DropLinkEditor.tsx +80 -0
  131. package/src/editor/field-types/DropListEditor.tsx +84 -0
  132. package/src/editor/field-types/ImageFieldEditor.tsx +65 -0
  133. package/src/editor/field-types/InternalLinkFieldEditor.tsx +188 -0
  134. package/src/editor/field-types/LinkFieldEditor.tsx +85 -0
  135. package/src/editor/field-types/MultiLineText.tsx +82 -0
  136. package/src/editor/field-types/PictureFieldEditor.tsx +121 -0
  137. package/src/editor/field-types/RawEditor.tsx +53 -0
  138. package/src/editor/field-types/ReactQuill.tsx +580 -0
  139. package/src/editor/field-types/RichTextEditor.tsx +22 -0
  140. package/src/editor/field-types/RichTextEditorComponent.tsx +127 -0
  141. package/src/editor/field-types/SingleLineText.tsx +174 -0
  142. package/src/editor/field-types/TreeListEditor.tsx +261 -0
  143. package/src/editor/fieldTypes.ts +140 -0
  144. package/src/editor/media-selector/AiImageSearch.tsx +185 -0
  145. package/src/editor/media-selector/AiImageSearchPrompt.tsx +94 -0
  146. package/src/editor/media-selector/MediaFolderBrowser.tsx +321 -0
  147. package/src/editor/media-selector/MediaSelector.tsx +42 -0
  148. package/src/editor/media-selector/Preview.tsx +14 -0
  149. package/src/editor/media-selector/Thumbnails.tsx +48 -0
  150. package/src/editor/media-selector/TreeSelector.tsx +292 -0
  151. package/src/editor/media-selector/UploadZone.tsx +137 -0
  152. package/src/editor/media-selector/index.ts +8 -0
  153. package/src/editor/menubar/ActionsMenu.tsx +94 -0
  154. package/src/editor/menubar/ActiveUsers.tsx +17 -0
  155. package/src/editor/menubar/ApproveAndPublish.tsx +18 -0
  156. package/src/editor/menubar/BrowseHistory.tsx +28 -0
  157. package/src/editor/menubar/ItemLanguageVersion.tsx +76 -0
  158. package/src/editor/menubar/LanguageSelector.tsx +226 -0
  159. package/src/editor/menubar/Menu.tsx +83 -0
  160. package/src/editor/menubar/NavButtons.tsx +74 -0
  161. package/src/editor/menubar/PageSelector.tsx +278 -0
  162. package/src/editor/menubar/PageViewerControls.tsx +120 -0
  163. package/src/editor/menubar/PreviewSecondaryControls.tsx +18 -0
  164. package/src/editor/menubar/SecondaryControls.tsx +45 -0
  165. package/src/editor/menubar/Separator.tsx +12 -0
  166. package/src/editor/menubar/SiteInfo.tsx +53 -0
  167. package/src/editor/menubar/User.tsx +27 -0
  168. package/src/editor/menubar/VersionSelector.tsx +142 -0
  169. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +307 -0
  170. package/src/editor/page-editor-chrome/CommentHighlightings.tsx +35 -0
  171. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +59 -0
  172. package/src/editor/page-editor-chrome/FieldActionIndicators.tsx +23 -0
  173. package/src/editor/page-editor-chrome/FieldEditedIndicator.tsx +64 -0
  174. package/src/editor/page-editor-chrome/FieldEditedIndicators.tsx +35 -0
  175. package/src/editor/page-editor-chrome/FrameMenu.tsx +338 -0
  176. package/src/editor/page-editor-chrome/FrameMenus.tsx +48 -0
  177. package/src/editor/page-editor-chrome/InlineEditor.tsx +765 -0
  178. package/src/editor/page-editor-chrome/LockedFieldIndicator.tsx +61 -0
  179. package/src/editor/page-editor-chrome/NoLayout.tsx +36 -0
  180. package/src/editor/page-editor-chrome/PageEditorChrome.tsx +122 -0
  181. package/src/editor/page-editor-chrome/PictureEditorOverlay.tsx +161 -0
  182. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +169 -0
  183. package/src/editor/page-editor-chrome/PlaceholderDropZones.tsx +315 -0
  184. package/src/editor/page-editor-chrome/SuggestionHighlighting.tsx +300 -0
  185. package/src/editor/page-editor-chrome/SuggestionHighlightings.tsx +40 -0
  186. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +828 -0
  187. package/src/editor/page-viewer/DeviceToolbar.tsx +70 -0
  188. package/src/editor/page-viewer/EditorForm.tsx +262 -0
  189. package/src/editor/page-viewer/MiniMap.tsx +362 -0
  190. package/src/editor/page-viewer/PageViewer.tsx +169 -0
  191. package/src/editor/page-viewer/PageViewerFrame.tsx +1022 -0
  192. package/src/editor/page-viewer/pageModelSkeletonBuilder.ts +412 -0
  193. package/src/editor/page-viewer/pageViewContext.ts +186 -0
  194. package/src/editor/pageModel.ts +220 -0
  195. package/src/editor/picture-shared.tsx +53 -0
  196. package/src/editor/reviews/Comment.tsx +308 -0
  197. package/src/editor/reviews/Comments.tsx +125 -0
  198. package/src/editor/reviews/DiffView.tsx +109 -0
  199. package/src/editor/reviews/PreviewInfo.tsx +35 -0
  200. package/src/editor/reviews/Reviews.tsx +280 -0
  201. package/src/editor/reviews/SuggestedEdit.tsx +316 -0
  202. package/src/editor/reviews/reviewCommands.tsx +47 -0
  203. package/src/editor/reviews/useReviews.tsx +70 -0
  204. package/src/editor/services/aiService.ts +173 -0
  205. package/src/editor/services/componentDesignerService.ts +151 -0
  206. package/src/editor/services/contentService.ts +180 -0
  207. package/src/editor/services/editService.ts +488 -0
  208. package/src/editor/services/indexService.ts +24 -0
  209. package/src/editor/services/reviewsService.ts +53 -0
  210. package/src/editor/services/serviceHelper.ts +95 -0
  211. package/src/editor/services/suggestedEditsService.ts +39 -0
  212. package/src/editor/services/systemService.ts +5 -0
  213. package/src/editor/services/translationService.ts +21 -0
  214. package/src/editor/services-server/api.ts +150 -0
  215. package/src/editor/services-server/graphQL.ts +106 -0
  216. package/src/editor/sidebar/ComponentPalette.tsx +161 -0
  217. package/src/editor/sidebar/ComponentTree.tsx +549 -0
  218. package/src/editor/sidebar/Debug.tsx +111 -0
  219. package/src/editor/sidebar/DictionaryEditor.tsx +261 -0
  220. package/src/editor/sidebar/EditHistory.tsx +134 -0
  221. package/src/editor/sidebar/GraphQL.tsx +164 -0
  222. package/src/editor/sidebar/Insert.tsx +35 -0
  223. package/src/editor/sidebar/MainContentTree.tsx +102 -0
  224. package/src/editor/sidebar/Performance.tsx +53 -0
  225. package/src/editor/sidebar/Sessions.tsx +35 -0
  226. package/src/editor/sidebar/Sidebar.tsx +20 -0
  227. package/src/editor/sidebar/SidebarView.tsx +152 -0
  228. package/src/editor/sidebar/Translations.tsx +295 -0
  229. package/src/editor/sidebar/Validation.tsx +102 -0
  230. package/src/editor/sidebar/ViewSelector.tsx +60 -0
  231. package/src/editor/sidebar/Workbox.tsx +209 -0
  232. package/src/editor/ui/CenteredMessage.tsx +7 -0
  233. package/src/editor/ui/CopyMoveTargetSelectorDialog.tsx +81 -0
  234. package/src/editor/ui/CopyToClipboardButton.tsx +24 -0
  235. package/src/editor/ui/DialogButtons.tsx +11 -0
  236. package/src/editor/ui/Icons.tsx +709 -0
  237. package/src/editor/ui/ItemList.tsx +76 -0
  238. package/src/editor/ui/ItemNameDialogNew.tsx +118 -0
  239. package/src/editor/ui/ItemSearch.tsx +159 -0
  240. package/src/editor/ui/PerfectTree.tsx +676 -0
  241. package/src/editor/ui/Section.tsx +35 -0
  242. package/src/editor/ui/SimpleIconButton.tsx +54 -0
  243. package/src/editor/ui/SimpleMenu.tsx +40 -0
  244. package/src/editor/ui/SimpleTable.tsx +60 -0
  245. package/src/editor/ui/SimpleTabs.tsx +60 -0
  246. package/src/editor/ui/SimpleToolbar.tsx +7 -0
  247. package/src/editor/ui/Spinner.tsx +9 -0
  248. package/src/editor/ui/Splitter.tsx +420 -0
  249. package/src/editor/ui/StackedPanels.tsx +134 -0
  250. package/src/editor/ui/Toolbar.tsx +7 -0
  251. package/src/editor/utils/id-helper.ts +3 -0
  252. package/src/editor/utils/insertOptions.ts +69 -0
  253. package/src/editor/utils/itemutils.ts +29 -0
  254. package/src/editor/utils/useMemoDebug.ts +28 -0
  255. package/src/editor/utils.ts +486 -0
  256. package/src/editor/views/CompareView.tsx +245 -0
  257. package/src/editor/views/EditView.tsx +27 -0
  258. package/src/editor/views/ItemEditor.tsx +58 -0
  259. package/src/editor/views/MediaFolderEditView.tsx +66 -0
  260. package/src/editor/views/SingleEditView.tsx +57 -0
  261. package/src/fonts/Geist-Black.woff2 +0 -0
  262. package/src/fonts/Geist-Bold.woff2 +0 -0
  263. package/src/fonts/Geist-ExtraBold.woff2 +0 -0
  264. package/src/fonts/Geist-ExtraLight.woff2 +0 -0
  265. package/src/fonts/Geist-Light.woff2 +0 -0
  266. package/src/fonts/Geist-Medium.woff2 +0 -0
  267. package/src/fonts/Geist-Regular.woff2 +0 -0
  268. package/src/fonts/Geist-SemiBold.woff2 +0 -0
  269. package/src/fonts/Geist-Thin.woff2 +0 -0
  270. package/src/fonts/Geist[wght].woff2 +0 -0
  271. package/src/fonts/index.ts +10 -0
  272. package/src/index.ts +23 -0
  273. package/src/lib/safelist.tsx +16 -0
  274. package/src/lib/utils.ts +6 -0
  275. package/src/page-wizard/PageWizard.tsx +139 -0
  276. package/src/page-wizard/WizardBox.tsx +4 -0
  277. package/src/page-wizard/WizardBoxConnector.tsx +56 -0
  278. package/src/page-wizard/WizardSteps.tsx +458 -0
  279. package/src/page-wizard/service.ts +35 -0
  280. package/src/page-wizard/startPageWizardCommand.ts +26 -0
  281. package/src/page-wizard/steps/BuildPageStep.tsx +259 -0
  282. package/src/page-wizard/steps/CollectStep.tsx +296 -0
  283. package/src/page-wizard/steps/ComponentTypesSelector.tsx +454 -0
  284. package/src/page-wizard/steps/Components.tsx +193 -0
  285. package/src/page-wizard/steps/ContentStep.tsx +890 -0
  286. package/src/page-wizard/steps/EditButton.tsx +34 -0
  287. package/src/page-wizard/steps/FieldEditor.tsx +102 -0
  288. package/src/page-wizard/steps/Generate.tsx +60 -0
  289. package/src/page-wizard/steps/ImagesStep.tsx +382 -0
  290. package/src/page-wizard/steps/LayoutStep.tsx +227 -0
  291. package/src/page-wizard/steps/MetaDataStep.tsx +173 -0
  292. package/src/page-wizard/steps/SelectStep.tsx +281 -0
  293. package/src/page-wizard/steps/schema.ts +180 -0
  294. package/src/page-wizard/steps/usePageCreator.ts +325 -0
  295. package/src/page-wizard/usePageWizard.ts +79 -0
  296. package/src/revision.ts +2 -0
  297. package/src/splash-screen/NewPage.tsx +294 -0
  298. package/src/splash-screen/OpenPage.tsx +113 -0
  299. package/src/splash-screen/RecentPages.tsx +123 -0
  300. package/src/splash-screen/SectionHeadline.tsx +21 -0
  301. package/src/splash-screen/SplashScreen.tsx +195 -0
  302. package/src/tour/Tour.tsx +566 -0
  303. package/src/tour/default-tour.tsx +301 -0
  304. package/src/tour/preview-tour.tsx +128 -0
  305. package/src/types.ts +335 -0
  306. package/styles.css +765 -0
  307. package/tsconfig.build.json +31 -0
  308. package/tsconfig.json +14 -0
@@ -0,0 +1,480 @@
1
+ import { useState, useEffect, useRef, useCallback } from "react";
2
+ import { useEditContext } from "../client/editContext";
3
+ import { AiResponseMessage } from "./AiResponseMessage";
4
+ import { createEditorAiContext } from "./editorAiContext";
5
+
6
+ import { ToolCall, Message as AiTerminalMessage } from "./AiTerminal";
7
+ import { executePrompt } from "../services/aiService";
8
+ import { EditOperation } from "../../types";
9
+ import { useDebouncedCallback } from "use-debounce";
10
+ // Simple debounce function
11
+ function debounce<F extends (...args: any[]) => any>(
12
+ func: F,
13
+ waitFor: number,
14
+ ): (...args: Parameters<F>) => void {
15
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
16
+
17
+ return (...args: Parameters<F>): void => {
18
+ if (timeoutId !== null) {
19
+ clearTimeout(timeoutId);
20
+ }
21
+ timeoutId = setTimeout(() => func(...args), waitFor);
22
+ };
23
+ }
24
+
25
+ // This matches the Message type in aiService.ts which is used by executePrompt
26
+ type Message = {
27
+ content: string;
28
+ name: string;
29
+ role: string;
30
+ };
31
+
32
+ type Response = {
33
+ responseText: string;
34
+ editOperations: EditOperation[];
35
+ numInputTokens: number;
36
+ numOutputTokens: number;
37
+ numCachedTokens: number;
38
+ messages: AiTerminalMessage[];
39
+ toolCalls?: ToolCall[];
40
+ state: string;
41
+ };
42
+
43
+ const GHOST_WRITER_CONTEXT_FIELD_ID =
44
+ "5B14129E-E14F-4C17-A7D2-0FCB27601A18".toLowerCase();
45
+
46
+ export function GhostWriter() {
47
+ const editContext = useEditContext();
48
+ const [context, setContext] = useState("");
49
+ const [messages, setMessages] = useState<AiTerminalMessage[]>([]);
50
+ const [thinking, setThinking] = useState<Response | null>(null);
51
+ const [isMonitoring, setIsMonitoring] = useState(false);
52
+ const [isThinking, setIsThinking] = useState(false);
53
+ const lastOpIndexRef = useRef(0);
54
+ const isMountedRef = useRef(false); // To prevent running on initial mount
55
+ const [newMessages, setNewMessages] = useState<AiTerminalMessage[]>([]);
56
+ const newMessagesRef = useRef<AiTerminalMessage[]>([]);
57
+
58
+ useEffect(() => {
59
+ newMessagesRef.current = newMessages;
60
+ }, [newMessages]);
61
+
62
+ // Memoized debounced function
63
+ const debouncedCheckIfAiCanHelp = useDebouncedCallback(() => {
64
+ // Check if component is still mounted and conditions are met
65
+ if (isMountedRef.current && isMonitoring && !isThinking && editContext) {
66
+ console.log("Debounced check triggered");
67
+ checkIfAiCanHelp();
68
+ }
69
+ }, 3000); // Wait 3 seconds
70
+
71
+ // Effect to monitor edit history length for changes
72
+ useEffect(() => {
73
+ if (!editContext || !isMonitoring) {
74
+ isMountedRef.current = false; // Reset mount status if disabled/no context
75
+ return;
76
+ }
77
+
78
+ // Set mounted status only after initial render and when enabled
79
+ isMountedRef.current = true;
80
+
81
+ // Trigger debounce when edit history changes *after* initial mount
82
+ // Only trigger if the last edit was NOT made by the AI
83
+ if (isMountedRef.current && editContext?.editHistory) {
84
+ const history = editContext.editHistory;
85
+ if (history.length > 0) {
86
+ const lastOp = history[0];
87
+ // Ensure lastOp exists and check the user.ai flag
88
+ if (lastOp && !lastOp.user?.ai) {
89
+ console.log(
90
+ "User edit detected, calling debounce for Ghost Writer check",
91
+ );
92
+ debouncedCheckIfAiCanHelp();
93
+ } else {
94
+ console.log("AI edit detected, skipping Ghost Writer check");
95
+ }
96
+ }
97
+ }
98
+
99
+ // Cleanup function to set mounted status to false
100
+ return () => {
101
+ isMountedRef.current = false;
102
+ };
103
+ }, [editContext?.editHistory, isMonitoring, debouncedCheckIfAiCanHelp]); // Watch history length and monitoring status
104
+
105
+ // Load initial context from Sitecore field and reset state when item changes
106
+ useEffect(() => {
107
+ if (
108
+ !editContext ||
109
+ !editContext.currentItemDescriptor ||
110
+ !editContext.itemsRepository
111
+ ) {
112
+ return;
113
+ }
114
+ const { currentItemDescriptor, itemsRepository } = editContext;
115
+
116
+ // Reset all state when item changes
117
+ setMessages([]);
118
+ setThinking(null);
119
+ setIsThinking(false);
120
+ setNewMessages([]);
121
+ lastOpIndexRef.current = 0;
122
+ newMessagesRef.current = [];
123
+ isMountedRef.current = false;
124
+
125
+ const loadContext = async () => {
126
+ try {
127
+ // Get the full item first
128
+ const item = await itemsRepository.getItem(currentItemDescriptor);
129
+ if (!item?.fields) {
130
+ console.warn(
131
+ "Could not load item or fields for Ghost Writer context",
132
+ );
133
+ return;
134
+ }
135
+
136
+ // Find the specific field
137
+ const contextField = item.fields.find(
138
+ (f) => f.id === GHOST_WRITER_CONTEXT_FIELD_ID,
139
+ );
140
+ const initialContext = contextField?.value; // Or contextField?.rawValue
141
+
142
+ if (initialContext && typeof initialContext === "string") {
143
+ setContext(initialContext);
144
+ } else {
145
+ // Set to empty if field doesn't exist or is not a string
146
+ setContext("");
147
+ }
148
+ } catch (error) {
149
+ console.error("Error loading Ghost Writer context:", error);
150
+ setContext(""); // Reset context on error
151
+ }
152
+ };
153
+
154
+ loadContext();
155
+ // Depend on the item descriptor and the repository instance
156
+ }, [editContext?.currentItemDescriptor, editContext?.itemsRepository]);
157
+
158
+ // Debounced function to save context to Sitecore
159
+ const debouncedSaveContext = useDebouncedCallback((newContext: string) => {
160
+ if (
161
+ !editContext ||
162
+ !editContext.currentItemDescriptor ||
163
+ !editContext.operations
164
+ ) {
165
+ return;
166
+ }
167
+ const { currentItemDescriptor, operations } = editContext;
168
+
169
+ console.log("Debounced save triggered for context:", newContext);
170
+ try {
171
+ operations.editField({
172
+ field: {
173
+ item: currentItemDescriptor,
174
+ fieldId: GHOST_WRITER_CONTEXT_FIELD_ID,
175
+ },
176
+ value: newContext,
177
+ });
178
+ } catch (error) {
179
+ console.error("Error saving Ghost Writer context:", error);
180
+ }
181
+ }, 1000);
182
+
183
+ // Handle context change and trigger debounced save
184
+ const handleContextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
185
+ const newContext = e.target.value;
186
+ setContext(newContext);
187
+ debouncedSaveContext(newContext);
188
+ };
189
+
190
+ // Function to check if AI can help
191
+ const checkIfAiCanHelp = async () => {
192
+ if (!editContext) return;
193
+
194
+ const tasks = [
195
+ {
196
+ name: "spell-check",
197
+ title: "Spell Check",
198
+ description: "Fix spelling mistakes and grammar errors.",
199
+ },
200
+ {
201
+ name: "find-images",
202
+ title: "Find Images",
203
+ description: "Search for matching images and add them to the page.",
204
+ },
205
+ {
206
+ name: "improve-page",
207
+ title: "Improve Page",
208
+ description:
209
+ "If there are no empty fields, you can suggest improvements to the page.",
210
+ },
211
+ {
212
+ name: "support-me",
213
+ title: "Support Me",
214
+ description:
215
+ "Watch for instructions in brackets. Do what I ask you to do and remove the brackets.",
216
+ },
217
+ ];
218
+
219
+ console.log("Starting AI check");
220
+ setIsThinking(true);
221
+
222
+ // Create a new user message for the current context
223
+ const userMessage: AiTerminalMessage = {
224
+ id: Date.now(),
225
+ content: context,
226
+ role: "user",
227
+ name: "user",
228
+ tool_calls: [],
229
+ };
230
+
231
+ // Add user message to our history display
232
+ const updatedMessages = [...messages, userMessage];
233
+ setMessages(updatedMessages);
234
+
235
+ setThinking({
236
+ responseText: "Your Ghost Writer is thinking...",
237
+ editOperations: [],
238
+ numInputTokens: 0,
239
+ numOutputTokens: 0,
240
+ numCachedTokens: 0,
241
+ messages: updatedMessages,
242
+ state: "thinking",
243
+ });
244
+
245
+ lastOpIndexRef.current = 0;
246
+ setNewMessages([]);
247
+
248
+ const lockedComponent =
249
+ editContext.selection.length === 1 &&
250
+ " The user is editing the component with the id " +
251
+ editContext.selection[0] +
252
+ " and is locked. Do not edit this component.";
253
+
254
+ try {
255
+ // Only send the current context as a message to executePrompt
256
+ const response = await executePrompt(
257
+ [
258
+ {
259
+ content:
260
+ "We are together editing this page. You are working in the background and can help us with the following tasks: " +
261
+ tasks.map((t) => `- ${t.title}: ${t.description}`).join("\n") +
262
+ " Do not interfere with my current work. Dont ask me what you should do. If you are confident your change is welcome, just do it. Do not report progress or changes, just do them. " +
263
+ "Do not write empty values into fields. Do not replace a field value with the same value. If you are not sure what the topic is, dont do anything. Do not confirm the instructions, just follow them." +
264
+ lockedComponent +
265
+ " If you feel you need more information and you cannot find it yourself using the search, ask me specific questions about the topic. " +
266
+ " Use the get-component-props-and-placeholders tool to find out which components can be inserted where",
267
+
268
+ role: "user",
269
+ name: "user",
270
+ },
271
+ ],
272
+ editContext,
273
+ createEditorAiContext,
274
+ {
275
+ addAllContent: true,
276
+ profile: "ghostwriter",
277
+ },
278
+ undefined,
279
+ "gpt-4.1",
280
+ handleAiResponse,
281
+ );
282
+
283
+ if (response) {
284
+ handleAiResponse(response);
285
+ }
286
+ } catch (error) {
287
+ console.error("Error executing Ghost Writer AI prompt:", error);
288
+ if (isMountedRef.current) {
289
+ // Check if still mounted before setting state
290
+ setThinking({
291
+ responseText: "Error occurred while thinking.",
292
+ editOperations: [],
293
+ numInputTokens: 0,
294
+ numOutputTokens: 0,
295
+ numCachedTokens: 0,
296
+ messages: updatedMessages,
297
+ state: "error",
298
+ toolCalls: [],
299
+ });
300
+ }
301
+ } finally {
302
+ if (isMountedRef.current) {
303
+ setIsThinking(false);
304
+ console.log("AI check finished");
305
+ }
306
+ }
307
+ };
308
+
309
+ // Handle AI response as it streams in
310
+ const handleAiResponse = (response: Response) => {
311
+ if (!isMountedRef.current) return; // Don't update state if unmounted
312
+
313
+ // Merge new messages from the response with newMessagesRef.current based on message ID
314
+ const currentNewMessages = [...newMessagesRef.current];
315
+
316
+ if (response.messages && response.messages.length > 0) {
317
+ response.messages.forEach((newMsg) => {
318
+ // Check if message with this ID already exists in newMessages
319
+ const existingMsgIndex = currentNewMessages.findIndex(
320
+ (m) => m.id === newMsg.id,
321
+ );
322
+
323
+ if (existingMsgIndex >= 0) {
324
+ // Update existing message
325
+ currentNewMessages[existingMsgIndex] = newMsg;
326
+ } else {
327
+ // Add new message
328
+ currentNewMessages.push(newMsg);
329
+ }
330
+ });
331
+ }
332
+
333
+ // Update newMessages state and ref
334
+ setNewMessages(currentNewMessages);
335
+ setMessages([...messages, ...currentNewMessages]);
336
+
337
+ // Merge the new response messages with our existing messages
338
+ const updatedMessages = [...messages];
339
+ if (response.messages && response.messages.length > 0) {
340
+ response.messages.forEach((msg) => {
341
+ updatedMessages.push(msg);
342
+ });
343
+ }
344
+ console.log("Updated messages:", updatedMessages);
345
+ setMessages(updatedMessages);
346
+
347
+ // Update the response with our combined messages
348
+ const updatedResponse = {
349
+ ...response,
350
+ messages: updatedMessages,
351
+ };
352
+
353
+ setThinking(updatedResponse); // Update the displayed thinking state
354
+
355
+ // Apply new operations incrementally
356
+ if (
357
+ editContext &&
358
+ response.editOperations &&
359
+ response.editOperations.length > lastOpIndexRef.current
360
+ ) {
361
+ const newOps = response.editOperations.slice(lastOpIndexRef.current);
362
+ // applyAiOperations(newOps);
363
+ lastOpIndexRef.current = response.editOperations.length;
364
+ }
365
+ };
366
+
367
+ // // Apply operations suggested by the AI
368
+ // const applyAiOperations = (operations: EditOperation[]) => {
369
+ // if (!editContext || !isMountedRef.current) return;
370
+
371
+ // const isEditTextFieldOp = (op: EditOperation): op is EditFieldOperation => {
372
+ // if (op.type !== "edit-field") return false;
373
+ // const editFieldOp = op as EditFieldOperation;
374
+ // return (
375
+ // !!editFieldOp.fieldType && editFieldOp.fieldType.indexOf("text") !== -1
376
+ // );
377
+ // };
378
+
379
+ // operations.forEach((op: EditOperation) => {
380
+ // console.log("Applying AI operation:", op.type, op);
381
+ // if (isEditTextFieldOp(op)) {
382
+ // if (op.itemId && op.mainItem) {
383
+ // // Check mainItem exists
384
+ // const fieldDescriptor = {
385
+ // item: {
386
+ // ...op.mainItem,
387
+ // id: op.itemId,
388
+ // },
389
+ // fieldId: op.fieldId,
390
+ // };
391
+ // try {
392
+ // editContext.itemsRepository.updateFieldValue(
393
+ // fieldDescriptor,
394
+ // op.user ?? { name: "GhostWriter", ai: true },
395
+ // false,
396
+ // op.value,
397
+ // );
398
+ // } catch (error) {
399
+ // console.error(
400
+ // "Error applying updateFieldValue:",
401
+ // error,
402
+ // fieldDescriptor,
403
+ // op.value,
404
+ // );
405
+ // }
406
+ // } else {
407
+ // console.warn(
408
+ // "Skipping edit-field op due to missing itemId or mainItem:",
409
+ // op,
410
+ // );
411
+ // }
412
+ // } else if (op.type === "add-component") {
413
+ // // Add component logic might need more details from the operation
414
+ // // Example: editContext.operations.addComponent(...)
415
+ // console.warn(
416
+ // "Add-component operation application not fully implemented.",
417
+ // );
418
+ // }
419
+ // });
420
+
421
+ // // Debounce refresh request if needed
422
+ // editContext.requestRefresh("immediate");
423
+ // };
424
+
425
+ return (
426
+ <div className="ghost-writer flex h-full flex-col gap-4 p-4">
427
+ <div className="flex items-center justify-between">
428
+ <h2 className="text-lg font-semibold">Ghost Writer</h2>
429
+ <label className="flex cursor-pointer items-center">
430
+ <input
431
+ type="checkbox"
432
+ checked={isMonitoring}
433
+ onChange={() => setIsMonitoring(!isMonitoring)}
434
+ className="mr-2"
435
+ />
436
+ <span>Enable Ghost Writer</span>
437
+ </label>
438
+ </div>
439
+
440
+ <div className="background-context">
441
+ <label className="mb-2 block font-medium">Background Context</label>
442
+ <textarea
443
+ value={context}
444
+ onChange={handleContextChange}
445
+ placeholder="Provide some background context about this page to help the AI understand its purpose..."
446
+ className="h-32 w-full resize-none rounded border p-2"
447
+ disabled={isThinking}
448
+ />
449
+ </div>
450
+
451
+ <div className="ai-thinking flex-1 overflow-auto text-sm">
452
+ <label className="mb-2 block font-medium">AI Thinking</label>
453
+ <div className="max-h-[400px] min-h-[200px] overflow-y-auto rounded border bg-gray-50 p-3 text-xs">
454
+ {messages.length > 0 ? (
455
+ <AiResponseMessage
456
+ messages={messages}
457
+ editOperations={thinking?.editOperations || []}
458
+ finished={
459
+ !thinking ||
460
+ thinking.state === "finished" ||
461
+ thinking.state === "error"
462
+ }
463
+ />
464
+ ) : (
465
+ <div className="text-gray-500 italic">
466
+ {isMonitoring
467
+ ? "Ghost Writer is active and monitoring for changes..."
468
+ : "Enable Ghost Writer to get AI assistance while you edit"}
469
+ </div>
470
+ )}
471
+ {thinking && thinking.state === "thinking" && (
472
+ <div className="mt-2 text-blue-500">
473
+ Your Ghost Writer is thinking...
474
+ </div>
475
+ )}
476
+ </div>
477
+ </div>
478
+ </div>
479
+ );
480
+ }
@@ -0,0 +1,108 @@
1
+ import { EditContextType } from "../client/editContext";
2
+ import {
3
+ Component,
4
+ FullItem,
5
+ ItemDescriptor,
6
+ Page,
7
+ Placeholder,
8
+ } from "../pageModel";
9
+
10
+ export type AiPageModel = {
11
+ components: AiComponent[];
12
+ message?: string;
13
+ };
14
+
15
+ export type AiComponent = {
16
+ id: string;
17
+ name: string;
18
+ type: string;
19
+ fields: AiField[];
20
+ placeholder?: string;
21
+ children?: AiComponent[];
22
+ };
23
+
24
+ export type AiField = {
25
+ name: string;
26
+ value: string;
27
+ type: string;
28
+ };
29
+
30
+ const collectDatasourceItemDescriptors = (
31
+ component: Component,
32
+ allDatasourceItemDescriptors: ItemDescriptor[],
33
+ ): void => {
34
+ if (component.datasourceItem) {
35
+ allDatasourceItemDescriptors.push(component.datasourceItem);
36
+ }
37
+
38
+ component.placeholders.forEach((placeholder) => {
39
+ if (placeholder.components) {
40
+ placeholder.components.forEach((c) => {
41
+ collectDatasourceItemDescriptors(c, allDatasourceItemDescriptors);
42
+ });
43
+ }
44
+ });
45
+ };
46
+
47
+ const mapComponent = (allDatasourceItems: FullItem[], component: Component) => {
48
+ const item = allDatasourceItems.find((item) => item.id === component.id);
49
+ console.log("Map component: ", component, allDatasourceItems);
50
+
51
+ const fields: AiField[] =
52
+ item?.fields
53
+ .filter((x) =>
54
+ component.datasourceItem?.renderedFieldIds.find((y) => y === x.id),
55
+ )
56
+ .map((field) => ({
57
+ name: field.name || "unknown",
58
+ value: field.rawValue as string,
59
+ type: field.type,
60
+ })) ?? [];
61
+
62
+ return {
63
+ id: component.id,
64
+ name: component.name || "unknown",
65
+ type: component.type,
66
+ fields: fields,
67
+ children: mapPlaceholders(component, allDatasourceItems),
68
+ };
69
+ };
70
+
71
+ const mapPlaceholders = (
72
+ component: Component,
73
+ allDatasourceItems: FullItem[],
74
+ ) => {
75
+ let components: AiComponent[] = [];
76
+ component.placeholders.forEach((p) => {
77
+ const placeholderComponents = mapPlaceholder(p, allDatasourceItems);
78
+ components = [...components, ...placeholderComponents];
79
+ });
80
+ return components;
81
+ };
82
+
83
+ const mapPlaceholder = (
84
+ placeholder: Placeholder,
85
+ allDatasourceItems: FullItem[],
86
+ ) => {
87
+ return placeholder.components.map((c) => mapComponent(allDatasourceItems, c));
88
+ };
89
+
90
+ export const convertToAiPageModel = async (
91
+ page: Page,
92
+ editContext: EditContextType,
93
+ ): Promise<AiPageModel> => {
94
+ const allDatasourceItemDescriptors: ItemDescriptor[] = [];
95
+
96
+ collectDatasourceItemDescriptors(
97
+ page.rootComponent,
98
+ allDatasourceItemDescriptors,
99
+ );
100
+
101
+ const allDatasourceItems = await editContext.itemsRepository.getItems(
102
+ allDatasourceItemDescriptors,
103
+ );
104
+
105
+ return {
106
+ components: mapPlaceholders(page.rootComponent, allDatasourceItems),
107
+ };
108
+ };
@@ -0,0 +1,18 @@
1
+ import { EditContextType } from "../client/editContext";
2
+
3
+ export function createEditorAiContext({
4
+ editContext,
5
+ }: {
6
+ editContext: EditContextType;
7
+ }) {
8
+ const aiPromptUrl = editContext.configuration.services.aiService.promptUrl;
9
+
10
+ return {
11
+ endpoint: aiPromptUrl,
12
+ promptData: {
13
+ itemid: editContext.currentItemDescriptor?.id,
14
+ language: editContext.currentItemDescriptor?.language,
15
+ version: editContext.currentItemDescriptor?.version,
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,44 @@
1
+ import { Dialog } from "primereact/dialog";
2
+ import { DialogProps } from "./editContext";
3
+ import { useImperativeHandle, useState, forwardRef, useEffect } from "react";
4
+ import { buildDate, version } from "../../revision";
5
+
6
+ export const AboutDialog = forwardRef<DialogProps<void>, DialogProps<void>>(
7
+ (props, ref) => {
8
+ const [visible, setVisible] = useState(false);
9
+
10
+ useImperativeHandle(ref, () => ({
11
+ onClose: (result: any) => {
12
+ setVisible(false);
13
+ props.onClose(result);
14
+ },
15
+ }));
16
+
17
+ useEffect(() => {
18
+ setVisible(true);
19
+ }, []);
20
+
21
+ return (
22
+ <Dialog
23
+ visible={visible}
24
+ onHide={() => {
25
+ setVisible(false);
26
+ props.onClose(null);
27
+ }}
28
+ header="About AI Workbench"
29
+ style={{ width: "400px" }}
30
+ >
31
+ <div className="flex flex-col gap-4 p-2">
32
+ <div>
33
+ Build: <span className="font-bold"> {version}</span> from{" "}
34
+ <span className="font-bold">{buildDate}</span>
35
+ </div>
36
+ <div className="mt-4 text-sm text-gray-600">
37
+ &copy; {new Date().getFullYear()} canvas Reply GmbH. All rights
38
+ reserved.
39
+ </div>
40
+ </div>
41
+ </Dialog>
42
+ );
43
+ },
44
+ );