@alpaca-editor/core 1.0.3938 → 1.0.3939

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 (75) hide show
  1. package/dist/editor/ContentTree.js +12 -8
  2. package/dist/editor/ContentTree.js.map +1 -1
  3. package/dist/editor/ContextMenu.d.ts +1 -1
  4. package/dist/editor/ContextMenu.js +17 -3
  5. package/dist/editor/ContextMenu.js.map +1 -1
  6. package/dist/editor/FieldActionsOverlay.d.ts +17 -0
  7. package/dist/editor/FieldActionsOverlay.js +148 -0
  8. package/dist/editor/FieldActionsOverlay.js.map +1 -0
  9. package/dist/editor/FieldHistory.d.ts +2 -1
  10. package/dist/editor/FieldHistory.js +11 -8
  11. package/dist/editor/FieldHistory.js.map +1 -1
  12. package/dist/editor/FieldListField.js +14 -17
  13. package/dist/editor/FieldListField.js.map +1 -1
  14. package/dist/editor/PictureEditor.js +28 -2
  15. package/dist/editor/PictureEditor.js.map +1 -1
  16. package/dist/editor/Titlebar.js +18 -9
  17. package/dist/editor/Titlebar.js.map +1 -1
  18. package/dist/editor/ai/AiTerminal.js +27 -41
  19. package/dist/editor/ai/AiTerminal.js.map +1 -1
  20. package/dist/editor/client/EditorClient.js +48 -18
  21. package/dist/editor/client/EditorClient.js.map +1 -1
  22. package/dist/editor/client/editContext.d.ts +1 -1
  23. package/dist/editor/client/editContext.js.map +1 -1
  24. package/dist/editor/client/itemsRepository.js +126 -90
  25. package/dist/editor/client/itemsRepository.js.map +1 -1
  26. package/dist/editor/menubar/BrowseHistory.js +3 -4
  27. package/dist/editor/menubar/BrowseHistory.js.map +1 -1
  28. package/dist/editor/menubar/PageSelector.js +37 -9
  29. package/dist/editor/menubar/PageSelector.js.map +1 -1
  30. package/dist/editor/page-editor-chrome/FieldActionIndicator.js +1 -1
  31. package/dist/editor/page-editor-chrome/FieldActionIndicator.js.map +1 -1
  32. package/dist/editor/page-viewer/PageViewerFrame.js +98 -2
  33. package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
  34. package/dist/editor/pageModel.d.ts +14 -0
  35. package/dist/editor/reviews/Comment.js +3 -2
  36. package/dist/editor/reviews/Comment.js.map +1 -1
  37. package/dist/editor/services/editService.d.ts +1 -1
  38. package/dist/editor/services/editService.js +2 -1
  39. package/dist/editor/services/editService.js.map +1 -1
  40. package/dist/editor/ui/Icons.js +1 -1
  41. package/dist/editor/ui/Icons.js.map +1 -1
  42. package/dist/editor/ui/ItemList.d.ts +16 -0
  43. package/dist/editor/ui/ItemList.js +19 -0
  44. package/dist/editor/ui/ItemList.js.map +1 -0
  45. package/dist/editor/ui/ItemSearch.js +2 -12
  46. package/dist/editor/ui/ItemSearch.js.map +1 -1
  47. package/dist/editor/ui/SimpleTabs.js +1 -1
  48. package/dist/editor/ui/SimpleTabs.js.map +1 -1
  49. package/dist/revision.d.ts +2 -2
  50. package/dist/revision.js +2 -2
  51. package/dist/styles.css +3 -8
  52. package/package.json +1 -1
  53. package/src/editor/ContentTree.tsx +15 -12
  54. package/src/editor/ContextMenu.tsx +20 -2
  55. package/src/editor/FieldActionsOverlay.tsx +307 -0
  56. package/src/editor/FieldHistory.tsx +9 -8
  57. package/src/editor/FieldListField.tsx +29 -29
  58. package/src/editor/PictureEditor.tsx +66 -1
  59. package/src/editor/Titlebar.tsx +22 -11
  60. package/src/editor/ai/AiTerminal.tsx +42 -53
  61. package/src/editor/client/EditorClient.tsx +62 -18
  62. package/src/editor/client/editContext.ts +5 -1
  63. package/src/editor/client/itemsRepository.ts +151 -115
  64. package/src/editor/menubar/BrowseHistory.tsx +7 -16
  65. package/src/editor/menubar/PageSelector.tsx +91 -66
  66. package/src/editor/page-editor-chrome/FieldActionIndicator.tsx +1 -1
  67. package/src/editor/page-viewer/PageViewerFrame.tsx +143 -0
  68. package/src/editor/pageModel.ts +12 -0
  69. package/src/editor/reviews/Comment.tsx +5 -6
  70. package/src/editor/services/editService.ts +2 -0
  71. package/src/editor/ui/Icons.tsx +1 -0
  72. package/src/editor/ui/ItemList.tsx +76 -0
  73. package/src/editor/ui/ItemSearch.tsx +9 -46
  74. package/src/editor/ui/SimpleTabs.tsx +1 -1
  75. package/src/revision.ts +2 -2
@@ -1,8 +1,20 @@
1
- import { MouseEventHandler, useCallback, useState } from "react";
1
+ import {
2
+ MouseEventHandler,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react";
2
8
  import { useEditContext } from "./client/editContext";
3
9
  import { PictureCropper } from "./PictureCropper";
4
10
  import { PictureField, PictureRawValue } from "./fieldTypes";
5
11
  import { MediaSelectorMode } from "./media-selector/MediaSelector";
12
+ import { loadFieldButtons } from "./services/editService";
13
+ import { FieldButton } from "./pageModel";
14
+ import {
15
+ FieldActionsOverlay,
16
+ FieldActionsOverlayRef,
17
+ } from "./FieldActionsOverlay";
6
18
 
7
19
  export function PictureEditor({
8
20
  field,
@@ -19,8 +31,43 @@ export function PictureEditor({
19
31
  }) {
20
32
  const [showMenu, setShowMenu] = useState(false);
21
33
  const [showCropper, setShowCropper] = useState(false);
34
+ const [fieldButtons, setFieldButtons] = useState<FieldButton[]>([]);
35
+ const fieldActionsOverlay = useRef<FieldActionsOverlayRef>(null);
22
36
  const editContext = useEditContext();
23
37
 
38
+ // Load field buttons when component mounts
39
+ useEffect(() => {
40
+ const loadButtons = async () => {
41
+ if (field.descriptor) {
42
+ try {
43
+ const buttons = await loadFieldButtons(field.descriptor);
44
+ setFieldButtons(buttons);
45
+ } catch (error) {
46
+ console.warn("Failed to load field buttons:", error);
47
+ }
48
+ }
49
+ };
50
+ loadButtons();
51
+ }, [field.descriptor]);
52
+
53
+ // Field action handlers
54
+ const handleActionClick = useCallback(
55
+ async (action: FieldButton): Promise<void> => {
56
+ editContext?.triggerFieldAction(field.descriptor, action);
57
+ },
58
+ [editContext, field.descriptor],
59
+ );
60
+
61
+ const handleParameterizedActionExecute = useCallback(
62
+ async (
63
+ action: FieldButton,
64
+ parameters: Record<string, string>,
65
+ ): Promise<void> => {
66
+ editContext?.triggerFieldAction(field.descriptor, action, parameters);
67
+ },
68
+ [editContext, field.descriptor],
69
+ );
70
+
24
71
  const variant = field.value?.variants?.find((v) => v.name === variantName);
25
72
  const raw = (() => {
26
73
  try {
@@ -174,6 +221,15 @@ export function PictureEditor({
174
221
  className="min-w-[80px]"
175
222
  />
176
223
  )}
224
+ {fieldButtons.length > 0 && (
225
+ <Btn
226
+ label="Actions"
227
+ icon="pi pi-bolt"
228
+ onClick={(e) => fieldActionsOverlay.current?.show(e)}
229
+ testId="field-actions-button"
230
+ className="min-w-[80px]"
231
+ />
232
+ )}
177
233
  </div>
178
234
  )}
179
235
  {showCropper && (
@@ -183,6 +239,15 @@ export function PictureEditor({
183
239
  variantName={variantName}
184
240
  />
185
241
  )}
242
+ <FieldActionsOverlay
243
+ ref={fieldActionsOverlay}
244
+ generatorButtons={fieldButtons}
245
+ onActionClick={handleActionClick}
246
+ onParameterizedActionExecute={handleParameterizedActionExecute}
247
+ currentOverlay={editContext?.currentOverlay}
248
+ fieldId={field.id}
249
+ setCurrentOverlay={editContext?.setCurrentOverlay || (() => {})}
250
+ />
186
251
  </div>
187
252
  );
188
253
  }
@@ -25,21 +25,32 @@ export function Titlebar() {
25
25
 
26
26
  useEffect(() => {
27
27
  const handleClickOutside = (event: MouseEvent) => {
28
- if (
29
- menuOpen &&
30
- menuRef.current &&
31
- buttonRef.current &&
32
- !menuRef.current.contains(event.target as Node) &&
33
- !buttonRef.current.contains(event.target as Node)
34
- ) {
28
+ if (!menuOpen) return;
29
+
30
+ const target = event.target as Element;
31
+ const menuElement = menuRef.current;
32
+ const buttonElement = buttonRef.current;
33
+
34
+ // Check if the click is inside the menu or button
35
+ const isInsideMenu = menuElement && menuElement.contains(target);
36
+ const isInsideButton = buttonElement && buttonElement.contains(target);
37
+
38
+ // Check if the click is inside a PrimeReact overlay (which renders in portals)
39
+ const isInsideOverlay = target.closest(".p-overlaypanel") !== null;
40
+
41
+ // Close menu only if click is outside menu, button, and overlays
42
+ if (!isInsideMenu && !isInsideButton && !isInsideOverlay) {
35
43
  setMenuOpen(false);
36
44
  }
37
45
  };
38
46
 
39
- document.addEventListener("mousedown", handleClickOutside, true);
40
- return () => {
41
- document.removeEventListener("mousedown", handleClickOutside, true);
42
- };
47
+ if (menuOpen) {
48
+ document.addEventListener("click", handleClickOutside, true);
49
+
50
+ return () => {
51
+ document.removeEventListener("click", handleClickOutside, true);
52
+ };
53
+ }
43
54
  }, [menuOpen]);
44
55
 
45
56
  return (
@@ -121,72 +121,60 @@ export function AiTerminal({
121
121
  terminalCallback: (text: React.ReactNode, finished: boolean) => void,
122
122
  isFinished: boolean,
123
123
  ) {
124
- const currentMessages = messagesRef.current;
125
124
  const updatedMessages = response.messages;
126
125
 
127
- // Merge updatedMessages into currentMessages if they exist
126
+ // Replace the conversation history with the authoritative response from AI
128
127
  if (updatedMessages && Array.isArray(updatedMessages)) {
129
- // Ensure each message has the required properties
130
- updatedMessages.forEach((message) => {
131
- const existingMessageIndex = currentMessages.findIndex(
132
- (m) => m.id === message.id,
133
- );
128
+ const formattedMessages = updatedMessages.map((message) => {
134
129
  const formattedContent = message.content
135
130
  .trim()
136
131
  .replaceAll("\n", "<br>")
137
132
  ?.replace(/\*\*(.*?)\*\*/g, "<b>$1</b>");
138
133
 
139
- if (existingMessageIndex !== -1) {
140
- // Update existing message
141
- currentMessages[existingMessageIndex] = {
142
- ...currentMessages[existingMessageIndex],
143
- ...message,
144
- content: message.content,
145
- formattedContent: formattedContent,
146
- tool_calls:
147
- message.tool_calls ||
148
- currentMessages[existingMessageIndex]?.tool_calls ||
149
- [],
150
- };
151
- } else {
152
- // Add new message
153
- currentMessages.push({
154
- ...message,
155
- content: formattedContent,
156
- tool_calls: message.tool_calls || [], // Ensure toolCalls exists
157
- });
158
- }
134
+ return {
135
+ ...message,
136
+ content: message.content,
137
+ formattedContent: formattedContent,
138
+ tool_calls: message.tool_calls || [],
139
+ };
159
140
  });
160
- }
161
141
 
162
- // Update the messages state
163
- setMessages([...currentMessages]);
164
- setResponseMessages([...currentMessages]);
165
-
166
- terminalCallback(
167
- <AiResponseMessage
168
- messages={currentMessages}
169
- editOperations={response.editOperations}
170
- finished={isFinished}
171
- />,
172
- isFinished,
173
- );
142
+ // Update the messages state with the complete conversation from AI
143
+ setMessages([...formattedMessages]);
144
+ setResponseMessages([...formattedMessages]);
145
+
146
+ terminalCallback(
147
+ <AiResponseMessage
148
+ messages={formattedMessages}
149
+ editOperations={response.editOperations}
150
+ finished={isFinished}
151
+ />,
152
+ isFinished,
153
+ );
154
+ } else {
155
+ // Fallback: if no messages in response, keep current state
156
+ terminalCallback(
157
+ <AiResponseMessage
158
+ messages={messagesRef.current}
159
+ editOperations={response.editOperations}
160
+ finished={isFinished}
161
+ />,
162
+ isFinished,
163
+ );
164
+ }
174
165
  }
175
166
 
176
167
  async function commandHandler(
177
168
  text: string,
178
169
  callback: (text: React.ReactNode, finished: boolean) => void,
179
170
  ) {
180
- const newMessages = [
181
- ...messagesRef.current,
182
- {
183
- id: Date.now(), // Add unique id
184
- content: text,
185
- role: "user",
186
- name: "user",
187
- tool_calls: [], // Add empty toolCalls array
188
- },
189
- ];
171
+ const userMessage = {
172
+ id: Date.now(), // Add unique id
173
+ content: text,
174
+ role: "user",
175
+ name: "user",
176
+ tool_calls: [], // Add empty toolCalls array
177
+ };
190
178
 
191
179
  const context = createAiContext({ editContext });
192
180
 
@@ -194,8 +182,8 @@ export function AiTerminal({
194
182
  const selectedText = editContext?.selectedRange?.text || null;
195
183
  if (!activeProfile || !model) return;
196
184
 
197
- setResponseMessages([]);
198
-
185
+ // Build complete message history for API call
186
+ const conversationHistory = [...messagesRef.current, userMessage];
199
187
  const messages = [
200
188
  ...(options?.hiddenSystemPrompt
201
189
  ? [
@@ -208,7 +196,7 @@ export function AiTerminal({
208
196
  },
209
197
  ]
210
198
  : []),
211
- ...newMessages,
199
+ ...conversationHistory,
212
200
  ];
213
201
 
214
202
  const response = await executePrompt(
@@ -318,6 +306,7 @@ export function AiTerminal({
318
306
  ref={terminalRef}
319
307
  onReset={() => {
320
308
  setMessages([]);
309
+ setResponseMessages([]);
321
310
  setResponse(undefined);
322
311
  }}
323
312
  infobar={
@@ -701,29 +701,71 @@ export function EditorClient({
701
701
  }, [user]);
702
702
 
703
703
  useEffect(() => {
704
- let socket: WebSocket = (globalThis as any).editorSocket;
704
+ let reconnectTimeout: NodeJS.Timeout | null = null;
705
+ let reconnectAttempts = 0;
705
706
 
706
- if (
707
- socket &&
708
- (socket.readyState === WebSocket.OPEN ||
709
- socket.readyState === WebSocket.CONNECTING)
710
- )
711
- return;
707
+ const connectWebSocket = () => {
708
+ let socket: WebSocket = (globalThis as any).editorSocket;
709
+
710
+ if (
711
+ socket &&
712
+ (socket.readyState === WebSocket.OPEN ||
713
+ socket.readyState === WebSocket.CONNECTING)
714
+ )
715
+ return;
712
716
 
713
- socket = connectSocket(sessionId);
717
+ socket = connectSocket(sessionId);
714
718
 
715
- // Connection opened
716
- socket.addEventListener("open", () => {
717
- console.log("Connected!");
718
- sendClientInfo();
719
- requestQuota();
720
- //TODO: Load clients
721
- });
719
+ // Connection opened
720
+ socket.addEventListener("open", () => {
721
+ console.log("Connected!");
722
+ reconnectAttempts = 0; // Reset attempts on successful connection
723
+ sendClientInfo();
724
+ requestQuota();
725
+ //TODO: Load clients
726
+ });
727
+
728
+ // Listen for messages
729
+ socket.addEventListener("message", messageHandler);
730
+
731
+ // Handle connection close
732
+ socket.addEventListener("close", (event) => {
733
+ console.log("WebSocket connection closed:", event.code, event.reason);
734
+
735
+ // Only attempt to reconnect if it wasn't a clean close
736
+ if (event.code !== 1000) {
737
+ // Start with 1 second, increase exponentially up to 30 seconds
738
+ const delay = Math.min(
739
+ 1000 * Math.pow(2, Math.min(reconnectAttempts, 5)),
740
+ 30000,
741
+ );
742
+ console.log(
743
+ `Attempting to reconnect in ${delay}ms... (attempt ${reconnectAttempts + 1})`,
744
+ );
745
+
746
+ reconnectTimeout = setTimeout(() => {
747
+ reconnectAttempts++;
748
+ connectWebSocket();
749
+ }, delay);
750
+ }
751
+ });
722
752
 
723
- // Listen for messages
724
- socket.addEventListener("message", messageHandler);
753
+ // Handle connection errors
754
+ socket.addEventListener("error", (error) => {
755
+ console.error("WebSocket error:", error);
756
+ });
725
757
 
726
- (globalThis as any).editorSocket = socket;
758
+ (globalThis as any).editorSocket = socket;
759
+ };
760
+
761
+ connectWebSocket();
762
+
763
+ // Cleanup function
764
+ return () => {
765
+ if (reconnectTimeout) {
766
+ clearTimeout(reconnectTimeout);
767
+ }
768
+ };
727
769
  }, []);
728
770
 
729
771
  useEffect(() => {
@@ -1790,6 +1832,7 @@ export function EditorClient({
1790
1832
  triggerFieldAction: async (
1791
1833
  fieldDescriptor: FieldDescriptor,
1792
1834
  actionButton: FieldButton,
1835
+ parameters?: Record<string, string>,
1793
1836
  ) => {
1794
1837
  const field = await itemsRepository.getField(fieldDescriptor);
1795
1838
 
@@ -1831,6 +1874,7 @@ export function EditorClient({
1831
1874
  actionButton.action,
1832
1875
  editContext!.sessionId,
1833
1876
  selectedRange?.text || "",
1877
+ parameters || {},
1834
1878
  (data: any) => {
1835
1879
  op.message = data.responseText;
1836
1880
  setActiveFieldActions((prevFieldActions) => [
@@ -235,7 +235,11 @@ export type EditContextType = {
235
235
  unlockField: (field: FieldDescriptor) => void;
236
236
 
237
237
  readonly: boolean;
238
- triggerFieldAction: (field: FieldDescriptor, action: FieldButton) => void;
238
+ triggerFieldAction: (
239
+ field: FieldDescriptor,
240
+ action: FieldButton,
241
+ parameters?: Record<string, string>,
242
+ ) => void;
239
243
  activeFieldActions: FieldAction[];
240
244
  showContextMenu(e: any, menuItems: MenuItem[]): void;
241
245
  setCurrentOverlay: React.Dispatch<React.SetStateAction<any>>;