@alpaca-editor/core 1.0.3997 → 1.0.4007

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 (171) hide show
  1. package/dist/client-components/index.d.ts +1 -0
  2. package/dist/client-components/index.js +1 -0
  3. package/dist/client-components/index.js.map +1 -1
  4. package/dist/components/ui/calendar.d.ts +7 -0
  5. package/dist/components/ui/calendar.js +62 -0
  6. package/dist/components/ui/calendar.js.map +1 -0
  7. package/dist/config/config.js +11 -0
  8. package/dist/config/config.js.map +1 -1
  9. package/dist/editor/ContentTree.d.ts +2 -1
  10. package/dist/editor/ContentTree.js +2 -2
  11. package/dist/editor/ContentTree.js.map +1 -1
  12. package/dist/editor/FieldList.js +37 -10
  13. package/dist/editor/FieldList.js.map +1 -1
  14. package/dist/editor/FieldListField.js +21 -10
  15. package/dist/editor/FieldListField.js.map +1 -1
  16. package/dist/editor/client/EditorClient.js +5 -3
  17. package/dist/editor/client/EditorClient.js.map +1 -1
  18. package/dist/editor/client/itemsRepository.js +57 -9
  19. package/dist/editor/client/itemsRepository.js.map +1 -1
  20. package/dist/editor/client/pageModelBuilder.js +1 -1
  21. package/dist/editor/client/pageModelBuilder.js.map +1 -1
  22. package/dist/editor/commands/itemCommands.js +1 -1
  23. package/dist/editor/commands/itemCommands.js.map +1 -1
  24. package/dist/editor/field-types/DateFieldEditor.d.ts +5 -0
  25. package/dist/editor/field-types/DateFieldEditor.js +93 -0
  26. package/dist/editor/field-types/DateFieldEditor.js.map +1 -0
  27. package/dist/editor/field-types/ImageFieldEditor.js +1 -1
  28. package/dist/editor/field-types/ImageFieldEditor.js.map +1 -1
  29. package/dist/editor/field-types/InternalLinkFieldEditor.js +29 -9
  30. package/dist/editor/field-types/InternalLinkFieldEditor.js.map +1 -1
  31. package/dist/editor/field-types/LinkFieldEditor.js +1 -1
  32. package/dist/editor/field-types/LinkFieldEditor.js.map +1 -1
  33. package/dist/editor/field-types/MultiLineText.js +3 -1
  34. package/dist/editor/field-types/MultiLineText.js.map +1 -1
  35. package/dist/editor/field-types/PictureFieldEditor.js +1 -1
  36. package/dist/editor/field-types/PictureFieldEditor.js.map +1 -1
  37. package/dist/editor/field-types/SingleLineText.js +3 -1
  38. package/dist/editor/field-types/SingleLineText.js.map +1 -1
  39. package/dist/editor/field-types/TreeListEditor.js +90 -17
  40. package/dist/editor/field-types/TreeListEditor.js.map +1 -1
  41. package/dist/editor/field-types/richtext/components/ReactSlate.d.ts +3 -3
  42. package/dist/editor/field-types/richtext/components/ReactSlate.js +195 -172
  43. package/dist/editor/field-types/richtext/components/ReactSlate.js.map +1 -1
  44. package/dist/editor/fieldTypes.d.ts +1 -1
  45. package/dist/editor/media-selector/AiImageSearch.js +2 -1
  46. package/dist/editor/media-selector/AiImageSearch.js.map +1 -1
  47. package/dist/editor/media-selector/AiImageSearchPrompt.js +2 -1
  48. package/dist/editor/media-selector/AiImageSearchPrompt.js.map +1 -1
  49. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js +1 -1
  50. package/dist/editor/page-editor-chrome/PlaceholderDropZone.js.map +1 -1
  51. package/dist/editor/page-viewer/DeviceToolbar.js +3 -2
  52. package/dist/editor/page-viewer/DeviceToolbar.js.map +1 -1
  53. package/dist/editor/page-viewer/MiniMap.js +6 -1
  54. package/dist/editor/page-viewer/MiniMap.js.map +1 -1
  55. package/dist/editor/services/aiService.d.ts +9 -1
  56. package/dist/editor/services/aiService.js.map +1 -1
  57. package/dist/editor/sidebar/Insert.js +1 -1
  58. package/dist/editor/sidebar/Insert.js.map +1 -1
  59. package/dist/editor/sidebar/MainContentTree.d.ts +1 -1
  60. package/dist/editor/sidebar/MainContentTree.js +19 -7
  61. package/dist/editor/sidebar/MainContentTree.js.map +1 -1
  62. package/dist/editor/ui/Icons.js +1 -1
  63. package/dist/editor/ui/Icons.js.map +1 -1
  64. package/dist/editor/ui/ItemNameDialogNew.js +14 -10
  65. package/dist/editor/ui/ItemNameDialogNew.js.map +1 -1
  66. package/dist/editor/ui/PerfectTree.d.ts +2 -0
  67. package/dist/editor/ui/PerfectTree.js +1 -1
  68. package/dist/editor/ui/PerfectTree.js.map +1 -1
  69. package/dist/editor/ui/Spinner.js +1 -1
  70. package/dist/editor/ui/Spinner.js.map +1 -1
  71. package/dist/editor/utils.d.ts +1 -0
  72. package/dist/editor/utils.js +3 -0
  73. package/dist/editor/utils.js.map +1 -1
  74. package/dist/editor/views/CompareView.js.map +1 -1
  75. package/dist/page-wizard/PageWizard.d.ts +5 -1
  76. package/dist/page-wizard/PageWizard.js.map +1 -1
  77. package/dist/page-wizard/WizardSteps.js +1 -0
  78. package/dist/page-wizard/WizardSteps.js.map +1 -1
  79. package/dist/page-wizard/steps/ComponentTypesSelector.d.ts +2 -1
  80. package/dist/page-wizard/steps/ComponentTypesSelector.js +27 -90
  81. package/dist/page-wizard/steps/ComponentTypesSelector.js.map +1 -1
  82. package/dist/page-wizard/steps/ContentStep.js +118 -27
  83. package/dist/page-wizard/steps/ContentStep.js.map +1 -1
  84. package/dist/page-wizard/steps/FindItemsStep.d.ts +2 -0
  85. package/dist/page-wizard/steps/FindItemsStep.js +293 -0
  86. package/dist/page-wizard/steps/FindItemsStep.js.map +1 -0
  87. package/dist/page-wizard/steps/ImagesStep.js +9 -1
  88. package/dist/page-wizard/steps/ImagesStep.js.map +1 -1
  89. package/dist/page-wizard/steps/LayoutStep.js +24 -14
  90. package/dist/page-wizard/steps/LayoutStep.js.map +1 -1
  91. package/dist/page-wizard/steps/MetaDataStep.js +21 -11
  92. package/dist/page-wizard/steps/MetaDataStep.js.map +1 -1
  93. package/dist/page-wizard/steps/SelectStep.js +38 -31
  94. package/dist/page-wizard/steps/SelectStep.js.map +1 -1
  95. package/dist/page-wizard/steps/StructureStep.d.ts +2 -0
  96. package/dist/page-wizard/steps/StructureStep.js +156 -0
  97. package/dist/page-wizard/steps/StructureStep.js.map +1 -0
  98. package/dist/page-wizard/steps/schema.js +4 -4
  99. package/dist/page-wizard/steps/schema.js.map +1 -1
  100. package/dist/page-wizard/steps/usePageCreator.d.ts +2 -1
  101. package/dist/page-wizard/steps/usePageCreator.js +62 -8
  102. package/dist/page-wizard/steps/usePageCreator.js.map +1 -1
  103. package/dist/page-wizard/usePageWizard.js +2 -0
  104. package/dist/page-wizard/usePageWizard.js.map +1 -1
  105. package/dist/page-wizard/utils/dataAccessor.d.ts +39 -0
  106. package/dist/page-wizard/utils/dataAccessor.js +222 -0
  107. package/dist/page-wizard/utils/dataAccessor.js.map +1 -0
  108. package/dist/revision.d.ts +2 -2
  109. package/dist/revision.js +2 -2
  110. package/dist/splash-screen/RecentPages.js +14 -3
  111. package/dist/splash-screen/RecentPages.js.map +1 -1
  112. package/dist/styles.css +180 -2
  113. package/package.json +3 -1
  114. package/src/client-components/index.ts +1 -3
  115. package/src/components/ui/calendar.tsx +175 -0
  116. package/src/config/config.tsx +11 -1
  117. package/src/editor/ContentTree.tsx +4 -1
  118. package/src/editor/FieldList.tsx +44 -18
  119. package/src/editor/FieldListField.tsx +98 -23
  120. package/src/editor/client/EditorClient.tsx +6 -3
  121. package/src/editor/client/itemsRepository.ts +74 -10
  122. package/src/editor/client/pageModelBuilder.ts +1 -1
  123. package/src/editor/commands/itemCommands.tsx +2 -3
  124. package/src/editor/field-types/DateFieldEditor.tsx +132 -0
  125. package/src/editor/field-types/ImageFieldEditor.tsx +1 -1
  126. package/src/editor/field-types/InternalLinkFieldEditor.tsx +99 -73
  127. package/src/editor/field-types/LinkFieldEditor.tsx +2 -2
  128. package/src/editor/field-types/MultiLineText.tsx +5 -1
  129. package/src/editor/field-types/PictureFieldEditor.tsx +1 -1
  130. package/src/editor/field-types/SingleLineText.tsx +5 -1
  131. package/src/editor/field-types/TreeListEditor.tsx +149 -56
  132. package/src/editor/field-types/richtext/components/ReactSlate.tsx +537 -446
  133. package/src/editor/fieldTypes.ts +1 -1
  134. package/src/editor/media-selector/AiImageSearch.tsx +2 -1
  135. package/src/editor/media-selector/AiImageSearchPrompt.tsx +2 -1
  136. package/src/editor/page-editor-chrome/PlaceholderDropZone.tsx +1 -1
  137. package/src/editor/page-viewer/DeviceToolbar.tsx +17 -5
  138. package/src/editor/page-viewer/MiniMap.tsx +7 -2
  139. package/src/editor/services/aiService.ts +10 -1
  140. package/src/editor/sidebar/Insert.tsx +1 -1
  141. package/src/editor/sidebar/MainContentTree.tsx +27 -14
  142. package/src/editor/ui/Icons.tsx +16 -5
  143. package/src/editor/ui/ItemNameDialogNew.tsx +42 -30
  144. package/src/editor/ui/PerfectTree.tsx +3 -1
  145. package/src/editor/ui/Spinner.tsx +3 -1
  146. package/src/editor/utils.ts +4 -0
  147. package/src/editor/views/CompareView.tsx +1 -1
  148. package/src/page-wizard/PageWizard.tsx +5 -1
  149. package/src/page-wizard/WizardSteps.tsx +1 -0
  150. package/src/page-wizard/steps/ComponentTypesSelector.tsx +34 -115
  151. package/src/page-wizard/steps/ContentStep.tsx +149 -34
  152. package/src/page-wizard/steps/FindItemsStep.tsx +546 -0
  153. package/src/page-wizard/steps/ImagesStep.tsx +13 -1
  154. package/src/page-wizard/steps/LayoutStep.tsx +27 -16
  155. package/src/page-wizard/steps/MetaDataStep.tsx +26 -13
  156. package/src/page-wizard/steps/SelectStep.tsx +61 -32
  157. package/src/page-wizard/steps/StructureStep.tsx +350 -0
  158. package/src/page-wizard/steps/schema.ts +4 -4
  159. package/src/page-wizard/steps/usePageCreator.ts +84 -9
  160. package/src/page-wizard/usePageWizard.ts +2 -0
  161. package/src/page-wizard/utils/dataAccessor.ts +275 -0
  162. package/src/revision.ts +2 -2
  163. package/src/splash-screen/RecentPages.tsx +22 -3
  164. package/dist/editor/ai/GhostWriter.d.ts +0 -1
  165. package/dist/editor/ai/GhostWriter.js +0 -347
  166. package/dist/editor/ai/GhostWriter.js.map +0 -1
  167. package/dist/editor/component-designer/ComponentDesignerAiTerminal.d.ts +0 -1
  168. package/dist/editor/component-designer/ComponentDesignerAiTerminal.js +0 -7
  169. package/dist/editor/component-designer/ComponentDesignerAiTerminal.js.map +0 -1
  170. /package/src/editor/ai/{GhostWriter.tsx → GhostWriter.tsx_} +0 -0
  171. /package/src/editor/component-designer/{ComponentDesignerAiTerminal.tsx → ComponentDesignerAiTerminal.tsx_} +0 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Utility for flexible data access in wizard steps
3
+ * Allows both simple property names and JavaScript expressions
4
+ */
5
+
6
+ export interface DataAccessResult<T = any> {
7
+ value: T | undefined;
8
+ error?: string;
9
+ }
10
+
11
+ /**
12
+ * Safely evaluates JavaScript expressions or property accessors against a data object
13
+ * @param inputProperties - Either a simple property name, a JavaScript expression, or multiple expressions separated by newlines
14
+ * @param data - The data object to evaluate against
15
+ * @returns The evaluated value(s) or undefined with an error message
16
+ */
17
+ export function evaluateDataExpression<T = any>(
18
+ inputProperties: string,
19
+ data: any,
20
+ ): DataAccessResult<T> {
21
+ if (!inputProperties) {
22
+ return { value: undefined };
23
+ }
24
+
25
+ try {
26
+ // Split by newlines and filter out empty lines
27
+ const expressionLines = inputProperties
28
+ .split("\n")
29
+ .map((line) => line.trim())
30
+ .filter((line) => line.length > 0);
31
+
32
+ // If no valid expressions found, return undefined
33
+ if (expressionLines.length === 0) {
34
+ return { value: undefined };
35
+ }
36
+
37
+ // If only one expression, return single result
38
+ if (expressionLines.length === 1) {
39
+ const firstExpression = expressionLines[0];
40
+ if (firstExpression) {
41
+ return evaluateSingleExpression<T>(firstExpression, data);
42
+ }
43
+ }
44
+
45
+ // Multiple expressions - evaluate each and collect results
46
+ const results: any[] = [];
47
+ const errors: string[] = [];
48
+
49
+ for (const expression of expressionLines) {
50
+ const result = evaluateSingleExpression(expression, data);
51
+
52
+ if (result.error) {
53
+ errors.push(`Line "${expression}": ${result.error}`);
54
+ } else if (result.value !== undefined) {
55
+ // Only add non-undefined results
56
+ if (Array.isArray(result.value)) {
57
+ results.push(...result.value);
58
+ } else {
59
+ results.push(result.value);
60
+ }
61
+ }
62
+ }
63
+
64
+ // Return combined results or errors
65
+ if (errors.length > 0 && results.length === 0) {
66
+ return {
67
+ value: undefined,
68
+ error: `All expressions failed:\n${errors.join("\n")}`,
69
+ };
70
+ }
71
+
72
+ return {
73
+ value: results as T,
74
+ error:
75
+ errors.length > 0
76
+ ? `Some expressions failed:\n${errors.join("\n")}`
77
+ : undefined,
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ value: undefined,
82
+ error: `Failed to evaluate expressions "${inputProperties}": ${error instanceof Error ? error.message : String(error)}`,
83
+ };
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Evaluates expressions and maps them to object properties
89
+ * Supports explicit property names (name:expression) and implicit property names (derived from expression)
90
+ * @param inputProperties - Multiple expressions separated by newlines, optionally with explicit property names
91
+ * @param data - The data object to evaluate against
92
+ * @returns Object with properties mapped from expressions
93
+ */
94
+ export function evaluateDataExpressionToObject(
95
+ inputProperties: string,
96
+ data: any,
97
+ ): DataAccessResult<Record<string, any>> {
98
+ if (!inputProperties || !inputProperties.trim()) {
99
+ return { value: {} };
100
+ }
101
+
102
+ try {
103
+ // Split by newlines and filter out empty lines
104
+ const expressionLines = inputProperties
105
+ .split("\n")
106
+ .map((line) => line.trim())
107
+ .filter((line) => line.length > 0);
108
+
109
+ if (expressionLines.length === 0) {
110
+ return { value: {} };
111
+ }
112
+
113
+ const result: Record<string, any> = {};
114
+ const errors: string[] = [];
115
+
116
+ for (const line of expressionLines) {
117
+ // Check if line contains explicit property name (name:expression format)
118
+ const colonIndex = line.indexOf(":");
119
+ let propertyName: string;
120
+ let expression: string;
121
+
122
+ if (colonIndex > 0 && colonIndex < line.length - 1) {
123
+ // Explicit property name format: "name:expression"
124
+ propertyName = line.substring(0, colonIndex).trim();
125
+ expression = line.substring(colonIndex + 1).trim();
126
+ } else {
127
+ // Implicit property name - derive from expression
128
+ expression = line;
129
+ propertyName = derivePropertyNameFromExpression(expression);
130
+ }
131
+
132
+ // Evaluate the expression
133
+ const evalResult = evaluateSingleExpression(expression, data);
134
+
135
+ if (evalResult.error) {
136
+ errors.push(
137
+ `Property "${propertyName}" (${expression}): ${evalResult.error}`,
138
+ );
139
+ } else {
140
+ result[propertyName] = evalResult.value;
141
+ }
142
+ }
143
+
144
+ return {
145
+ value: result,
146
+ error: errors.length > 0 ? errors.join("\n") : undefined,
147
+ };
148
+ } catch (error) {
149
+ return {
150
+ value: {},
151
+ error: `Failed to evaluate expressions "${inputProperties}": ${error instanceof Error ? error.message : String(error)}`,
152
+ };
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Derives a property name from an expression
158
+ * Examples:
159
+ * - "selectedItems" -> "selectedItems"
160
+ * - "data.selectedItems" -> "selectedItems"
161
+ * - "data.user.profile.name" -> "name"
162
+ * - "data.items.filter(x => x.active)" -> "items"
163
+ */
164
+ function derivePropertyNameFromExpression(expression: string): string {
165
+ // Remove "data." prefix if present
166
+ let cleaned = expression.replace(/^data\./, "");
167
+
168
+ // For method calls like "items.filter(...)", take the base property
169
+ const methodMatch = cleaned.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\./);
170
+ if (methodMatch && methodMatch[1]) {
171
+ return methodMatch[1];
172
+ }
173
+
174
+ // For property access like "user.profile.name", take the last property
175
+ const parts = cleaned.split(".");
176
+ const lastPart = parts[parts.length - 1];
177
+
178
+ if (lastPart) {
179
+ // Extract just the property name (before any method calls or array access)
180
+ const propertyMatch = lastPart.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
181
+ if (propertyMatch && propertyMatch[1]) {
182
+ return propertyMatch[1];
183
+ }
184
+ }
185
+
186
+ // Fallback to the first valid identifier in the expression
187
+ const identifierMatch = cleaned.match(/([a-zA-Z_$][a-zA-Z0-9_$]*)/);
188
+ return identifierMatch && identifierMatch[1] ? identifierMatch[1] : "result";
189
+ }
190
+
191
+ /**
192
+ * Evaluates a single expression
193
+ */
194
+ function evaluateSingleExpression<T = any>(
195
+ expression: string,
196
+ data: any,
197
+ ): DataAccessResult<T> {
198
+ try {
199
+ // Check if it's a simple property name (no special characters that would indicate JS code)
200
+ const isSimpleProperty = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(expression);
201
+
202
+ if (isSimpleProperty) {
203
+ // Simple property access
204
+ return { value: data[expression] };
205
+ }
206
+
207
+ // It's a JavaScript expression
208
+ // If it doesn't start with "data.", prepend it automatically
209
+ let processedExpression = expression;
210
+ if (!expression.trim().startsWith("data.")) {
211
+ processedExpression = `data.${expression}`;
212
+ }
213
+
214
+ // Create a safe evaluation context
215
+ const evalFunction = new Function(
216
+ "data",
217
+ `
218
+ try {
219
+ return ${processedExpression};
220
+ } catch (error) {
221
+ throw new Error('Expression evaluation failed: ' + error.message);
222
+ }
223
+ `,
224
+ );
225
+
226
+ const result = evalFunction(data);
227
+ return { value: result };
228
+ } catch (error) {
229
+ return {
230
+ value: undefined,
231
+ error: `Failed to evaluate expression "${expression}": ${error instanceof Error ? error.message : String(error)}`,
232
+ };
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Sets a value in the data object using either a simple property name or a path
238
+ * For now, this only supports simple property names since setting via expressions is complex
239
+ * @param propertyName - The property name to set
240
+ * @param value - The value to set
241
+ * @param data - The current data object
242
+ * @returns Updated data object
243
+ */
244
+ export function setDataProperty(
245
+ propertyName: string,
246
+ value: any,
247
+ data: any,
248
+ ): any {
249
+ // For now, only support simple property names for setting
250
+ const isSimpleProperty = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propertyName);
251
+
252
+ if (!isSimpleProperty) {
253
+ console.warn(
254
+ `Setting data via expressions is not supported yet. Using property name as-is: ${propertyName}`,
255
+ );
256
+ }
257
+
258
+ return {
259
+ ...data,
260
+ [propertyName]: value,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Hook for using flexible data access in wizard steps
266
+ * @param expression - The expression or property name to read from
267
+ * @param data - The current data object
268
+ * @returns Object with the evaluated value and any error
269
+ */
270
+ export function useDataExpression<T = any>(
271
+ expression: string,
272
+ data: any,
273
+ ): DataAccessResult<T> {
274
+ return evaluateDataExpression<T>(expression, data);
275
+ }
package/src/revision.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const version = "1.0.3997";
2
- export const buildDate = "2025-07-14 01:46:35";
1
+ export const version = "1.0.4007";
2
+ export const buildDate = "2025-07-17 20:54:07";
@@ -10,14 +10,25 @@ export function RecentPages() {
10
10
  const editContext = useEditContext();
11
11
 
12
12
  const [history, setHistory] = useState<FullItem[]>();
13
+ const [isLoading, setIsLoading] = useState(false);
13
14
  const [selectedPage, setSelectedPage] = useState<HistoryEntry | null>(null);
14
15
 
15
16
  const recentPages = editContext?.browseHistory || [];
16
17
 
17
18
  useEffect(() => {
18
19
  const fetchItems = async () => {
19
- const pages = await editContext?.itemsRepository.getItems(recentPages);
20
- setHistory(pages);
20
+ if (recentPages.length === 0) {
21
+ setHistory([]);
22
+ return;
23
+ }
24
+
25
+ setIsLoading(true);
26
+ try {
27
+ const pages = await editContext?.itemsRepository.getItems(recentPages);
28
+ setHistory(pages);
29
+ } finally {
30
+ setIsLoading(false);
31
+ }
21
32
  };
22
33
  fetchItems();
23
34
  }, [editContext?.itemsRepository, recentPages]);
@@ -105,7 +116,15 @@ export function RecentPages() {
105
116
  ))}
106
117
  </tbody>
107
118
  </table>
108
- {(!history || history.length === 0) && (
119
+ {isLoading && (
120
+ <div className="flex h-full flex-col items-center justify-center p-6 text-center">
121
+ <i className="pi pi-spin pi-spinner text-lg text-gray-400"></i>
122
+ <div className="mt-2 text-sm text-gray-500">
123
+ Loading recent pages...
124
+ </div>
125
+ </div>
126
+ )}
127
+ {!isLoading && (!history || history.length === 0) && (
109
128
  <div className="p-6 text-center text-sm text-gray-500">
110
129
  No recent pages found
111
130
  </div>
@@ -1 +0,0 @@
1
- export declare function GhostWriter(): import("react/jsx-runtime").JSX.Element;
@@ -1,347 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useRef } from "react";
3
- import { useEditContext } from "../client/editContext";
4
- import { AiResponseMessage } from "./AiResponseMessage";
5
- import { createEditorAiContext } from "./editorAiContext";
6
- import { executePrompt } from "../services/aiService";
7
- import { useDebouncedCallback } from "use-debounce";
8
- // Simple debounce function
9
- function debounce(func, waitFor) {
10
- let timeoutId = null;
11
- return (...args) => {
12
- if (timeoutId !== null) {
13
- clearTimeout(timeoutId);
14
- }
15
- timeoutId = setTimeout(() => func(...args), waitFor);
16
- };
17
- }
18
- const GHOST_WRITER_CONTEXT_FIELD_ID = "5B14129E-E14F-4C17-A7D2-0FCB27601A18".toLowerCase();
19
- export function GhostWriter() {
20
- const editContext = useEditContext();
21
- const [context, setContext] = useState("");
22
- const [messages, setMessages] = useState([]);
23
- const [thinking, setThinking] = useState(null);
24
- const [isMonitoring, setIsMonitoring] = useState(false);
25
- const [isThinking, setIsThinking] = useState(false);
26
- const lastOpIndexRef = useRef(0);
27
- const isMountedRef = useRef(false); // To prevent running on initial mount
28
- const [newMessages, setNewMessages] = useState([]);
29
- const newMessagesRef = useRef([]);
30
- useEffect(() => {
31
- newMessagesRef.current = newMessages;
32
- }, [newMessages]);
33
- // Memoized debounced function
34
- const debouncedCheckIfAiCanHelp = useDebouncedCallback(() => {
35
- // Check if component is still mounted and conditions are met
36
- if (isMountedRef.current && isMonitoring && !isThinking && editContext) {
37
- console.log("Debounced check triggered");
38
- checkIfAiCanHelp();
39
- }
40
- }, 3000); // Wait 3 seconds
41
- // Effect to monitor edit history length for changes
42
- useEffect(() => {
43
- if (!editContext || !isMonitoring) {
44
- isMountedRef.current = false; // Reset mount status if disabled/no context
45
- return;
46
- }
47
- // Set mounted status only after initial render and when enabled
48
- isMountedRef.current = true;
49
- // Trigger debounce when edit history changes *after* initial mount
50
- // Only trigger if the last edit was NOT made by the AI
51
- if (isMountedRef.current && editContext?.editHistory) {
52
- const history = editContext.editHistory;
53
- if (history.length > 0) {
54
- const lastOp = history[0];
55
- // Ensure lastOp exists and check the user.ai flag
56
- if (lastOp && !lastOp.user?.ai) {
57
- console.log("User edit detected, calling debounce for Ghost Writer check");
58
- debouncedCheckIfAiCanHelp();
59
- }
60
- else {
61
- console.log("AI edit detected, skipping Ghost Writer check");
62
- }
63
- }
64
- }
65
- // Cleanup function to set mounted status to false
66
- return () => {
67
- isMountedRef.current = false;
68
- };
69
- }, [editContext?.editHistory, isMonitoring, debouncedCheckIfAiCanHelp]); // Watch history length and monitoring status
70
- // Load initial context from Sitecore field and reset state when item changes
71
- useEffect(() => {
72
- if (!editContext ||
73
- !editContext.currentItemDescriptor ||
74
- !editContext.itemsRepository) {
75
- return;
76
- }
77
- const { currentItemDescriptor, itemsRepository } = editContext;
78
- // Reset all state when item changes
79
- setMessages([]);
80
- setThinking(null);
81
- setIsThinking(false);
82
- setNewMessages([]);
83
- lastOpIndexRef.current = 0;
84
- newMessagesRef.current = [];
85
- isMountedRef.current = false;
86
- const loadContext = async () => {
87
- try {
88
- // Get the full item first
89
- const item = await itemsRepository.getItem(currentItemDescriptor);
90
- if (!item?.fields) {
91
- console.warn("Could not load item or fields for Ghost Writer context");
92
- return;
93
- }
94
- // Find the specific field
95
- const contextField = item.fields.find((f) => f.id === GHOST_WRITER_CONTEXT_FIELD_ID);
96
- const initialContext = contextField?.value; // Or contextField?.rawValue
97
- if (initialContext && typeof initialContext === "string") {
98
- setContext(initialContext);
99
- }
100
- else {
101
- // Set to empty if field doesn't exist or is not a string
102
- setContext("");
103
- }
104
- }
105
- catch (error) {
106
- console.error("Error loading Ghost Writer context:", error);
107
- setContext(""); // Reset context on error
108
- }
109
- };
110
- loadContext();
111
- // Depend on the item descriptor and the repository instance
112
- }, [editContext?.currentItemDescriptor, editContext?.itemsRepository]);
113
- // Debounced function to save context to Sitecore
114
- const debouncedSaveContext = useDebouncedCallback((newContext) => {
115
- if (!editContext ||
116
- !editContext.currentItemDescriptor ||
117
- !editContext.operations) {
118
- return;
119
- }
120
- const { currentItemDescriptor, operations } = editContext;
121
- console.log("Debounced save triggered for context:", newContext);
122
- try {
123
- operations.editField({
124
- field: {
125
- item: currentItemDescriptor,
126
- fieldId: GHOST_WRITER_CONTEXT_FIELD_ID,
127
- },
128
- value: newContext,
129
- });
130
- }
131
- catch (error) {
132
- console.error("Error saving Ghost Writer context:", error);
133
- }
134
- }, 1000);
135
- // Handle context change and trigger debounced save
136
- const handleContextChange = (e) => {
137
- const newContext = e.target.value;
138
- setContext(newContext);
139
- debouncedSaveContext(newContext);
140
- };
141
- // Function to check if AI can help
142
- const checkIfAiCanHelp = async () => {
143
- if (!editContext)
144
- return;
145
- const tasks = [
146
- {
147
- name: "spell-check",
148
- title: "Spell Check",
149
- description: "Fix spelling mistakes and grammar errors.",
150
- },
151
- {
152
- name: "find-images",
153
- title: "Find Images",
154
- description: "Search for matching images and add them to the page.",
155
- },
156
- {
157
- name: "improve-page",
158
- title: "Improve Page",
159
- description: "If there are no empty fields, you can suggest improvements to the page.",
160
- },
161
- {
162
- name: "support-me",
163
- title: "Support Me",
164
- description: "Watch for instructions in brackets. Do what I ask you to do and remove the brackets.",
165
- },
166
- ];
167
- console.log("Starting AI check");
168
- setIsThinking(true);
169
- // Create a new user message for the current context
170
- const userMessage = {
171
- id: Date.now(),
172
- content: context,
173
- role: "user",
174
- name: "user",
175
- tool_calls: [],
176
- };
177
- // Add user message to our history display
178
- const updatedMessages = [...messages, userMessage];
179
- setMessages(updatedMessages);
180
- setThinking({
181
- responseText: "Your Ghost Writer is thinking...",
182
- editOperations: [],
183
- numInputTokens: 0,
184
- numOutputTokens: 0,
185
- numCachedTokens: 0,
186
- messages: updatedMessages,
187
- state: "thinking",
188
- });
189
- lastOpIndexRef.current = 0;
190
- setNewMessages([]);
191
- const lockedComponent = editContext.selection.length === 1 &&
192
- " The user is editing the component with the id " +
193
- editContext.selection[0] +
194
- " and is locked. Do not edit this component.";
195
- try {
196
- // Only send the current context as a message to executePrompt
197
- const response = await executePrompt([
198
- {
199
- content: "We are together editing this page. You are working in the background and can help us with the following tasks: " +
200
- tasks.map((t) => `- ${t.title}: ${t.description}`).join("\n") +
201
- " 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. " +
202
- "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." +
203
- lockedComponent +
204
- " If you feel you need more information and you cannot find it yourself using the search, ask me specific questions about the topic. " +
205
- " Use the get-component-props-and-placeholders tool to find out which components can be inserted where",
206
- role: "user",
207
- name: "user",
208
- },
209
- ], editContext, createEditorAiContext, {
210
- addAllContent: true,
211
- profile: "ghostwriter",
212
- }, undefined, "gpt-4.1", handleAiResponse);
213
- if (response) {
214
- handleAiResponse(response);
215
- }
216
- }
217
- catch (error) {
218
- console.error("Error executing Ghost Writer AI prompt:", error);
219
- if (isMountedRef.current) {
220
- // Check if still mounted before setting state
221
- setThinking({
222
- responseText: "Error occurred while thinking.",
223
- editOperations: [],
224
- numInputTokens: 0,
225
- numOutputTokens: 0,
226
- numCachedTokens: 0,
227
- messages: updatedMessages,
228
- state: "error",
229
- toolCalls: [],
230
- });
231
- }
232
- }
233
- finally {
234
- if (isMountedRef.current) {
235
- setIsThinking(false);
236
- console.log("AI check finished");
237
- }
238
- }
239
- };
240
- // Handle AI response as it streams in
241
- const handleAiResponse = (response) => {
242
- if (!isMountedRef.current)
243
- return; // Don't update state if unmounted
244
- // Merge new messages from the response with newMessagesRef.current based on message ID
245
- const currentNewMessages = [...newMessagesRef.current];
246
- if (response.messages && response.messages.length > 0) {
247
- response.messages.forEach((newMsg) => {
248
- // Check if message with this ID already exists in newMessages
249
- const existingMsgIndex = currentNewMessages.findIndex((m) => m.id === newMsg.id);
250
- if (existingMsgIndex >= 0) {
251
- // Update existing message
252
- currentNewMessages[existingMsgIndex] = newMsg;
253
- }
254
- else {
255
- // Add new message
256
- currentNewMessages.push(newMsg);
257
- }
258
- });
259
- }
260
- // Update newMessages state and ref
261
- setNewMessages(currentNewMessages);
262
- setMessages([...messages, ...currentNewMessages]);
263
- // Merge the new response messages with our existing messages
264
- const updatedMessages = [...messages];
265
- if (response.messages && response.messages.length > 0) {
266
- response.messages.forEach((msg) => {
267
- updatedMessages.push(msg);
268
- });
269
- }
270
- console.log("Updated messages:", updatedMessages);
271
- setMessages(updatedMessages);
272
- // Update the response with our combined messages
273
- const updatedResponse = {
274
- ...response,
275
- messages: updatedMessages,
276
- };
277
- setThinking(updatedResponse); // Update the displayed thinking state
278
- // Apply new operations incrementally
279
- if (editContext &&
280
- response.editOperations &&
281
- response.editOperations.length > lastOpIndexRef.current) {
282
- const newOps = response.editOperations.slice(lastOpIndexRef.current);
283
- // applyAiOperations(newOps);
284
- lastOpIndexRef.current = response.editOperations.length;
285
- }
286
- };
287
- // // Apply operations suggested by the AI
288
- // const applyAiOperations = (operations: EditOperation[]) => {
289
- // if (!editContext || !isMountedRef.current) return;
290
- // const isEditTextFieldOp = (op: EditOperation): op is EditFieldOperation => {
291
- // if (op.type !== "edit-field") return false;
292
- // const editFieldOp = op as EditFieldOperation;
293
- // return (
294
- // !!editFieldOp.fieldType && editFieldOp.fieldType.indexOf("text") !== -1
295
- // );
296
- // };
297
- // operations.forEach((op: EditOperation) => {
298
- // console.log("Applying AI operation:", op.type, op);
299
- // if (isEditTextFieldOp(op)) {
300
- // if (op.itemId && op.mainItem) {
301
- // // Check mainItem exists
302
- // const fieldDescriptor = {
303
- // item: {
304
- // ...op.mainItem,
305
- // id: op.itemId,
306
- // },
307
- // fieldId: op.fieldId,
308
- // };
309
- // try {
310
- // editContext.itemsRepository.updateFieldValue(
311
- // fieldDescriptor,
312
- // op.user ?? { name: "GhostWriter", ai: true },
313
- // false,
314
- // op.value,
315
- // );
316
- // } catch (error) {
317
- // console.error(
318
- // "Error applying updateFieldValue:",
319
- // error,
320
- // fieldDescriptor,
321
- // op.value,
322
- // );
323
- // }
324
- // } else {
325
- // console.warn(
326
- // "Skipping edit-field op due to missing itemId or mainItem:",
327
- // op,
328
- // );
329
- // }
330
- // } else if (op.type === "add-component") {
331
- // // Add component logic might need more details from the operation
332
- // // Example: editContext.operations.addComponent(...)
333
- // console.warn(
334
- // "Add-component operation application not fully implemented.",
335
- // );
336
- // }
337
- // });
338
- // // Debounce refresh request if needed
339
- // editContext.requestRefresh("immediate");
340
- // };
341
- return (_jsxs("div", { className: "ghost-writer flex h-full flex-col gap-4 p-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h2", { className: "text-lg font-semibold", children: "Ghost Writer" }), _jsxs("label", { className: "flex cursor-pointer items-center", children: [_jsx("input", { type: "checkbox", checked: isMonitoring, onChange: () => setIsMonitoring(!isMonitoring), className: "mr-2" }), _jsx("span", { children: "Enable Ghost Writer" })] })] }), _jsxs("div", { className: "background-context", children: [_jsx("label", { className: "mb-2 block font-medium", children: "Background Context" }), _jsx("textarea", { value: context, onChange: handleContextChange, placeholder: "Provide some background context about this page to help the AI understand its purpose...", className: "h-32 w-full resize-none rounded border p-2", disabled: isThinking })] }), _jsxs("div", { className: "ai-thinking flex-1 overflow-auto text-sm", children: [_jsx("label", { className: "mb-2 block font-medium", children: "AI Thinking" }), _jsxs("div", { className: "max-h-[400px] min-h-[200px] overflow-y-auto rounded border p-3 text-xs", children: [messages.length > 0 ? (_jsx(AiResponseMessage, { messages: messages, editOperations: thinking?.editOperations || [], finished: !thinking ||
342
- thinking.state === "finished" ||
343
- thinking.state === "error" })) : (_jsx("div", { className: "text-gray-500 italic", children: isMonitoring
344
- ? "Ghost Writer is active and monitoring for changes..."
345
- : "Enable Ghost Writer to get AI assistance while you edit" })), thinking && thinking.state === "thinking" && (_jsx("div", { className: "mt-2 text-blue-500", children: "Your Ghost Writer is thinking..." }))] })] })] }));
346
- }
347
- //# sourceMappingURL=GhostWriter.js.map