@alpaca-editor/core 1.0.4147 → 1.0.4149

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.
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React, { useState, useMemo, memo } from "react";
2
2
  import { JsonView, defaultStyles } from "react-json-view-lite";
3
3
  import "react-json-view-lite/dist/index.css";
4
4
 
@@ -159,20 +159,33 @@ const normalizeToolCall = (
159
159
  return toolCall;
160
160
  };
161
161
 
162
- // Helper function to render JSON or text
163
- const renderJsonOrText = (json: string | object) => {
164
- // If it's already an object, use it directly
165
- if (typeof json === "object" && json !== null) {
162
+ // Memoized JSON viewer component to prevent re-renders during streaming
163
+ const MemoizedJsonView = memo(
164
+ ({ data }: { data: any }) => {
166
165
  return (
167
166
  <div className="font-mono text-xs" style={{ fontSize: "12px" }}>
168
167
  <JsonLightThemeStyles />
169
168
  <JsonView
170
- data={json}
169
+ data={data}
171
170
  shouldExpandNode={(level) => level < 2}
172
171
  style={darkJsonStyles}
173
172
  />
174
173
  </div>
175
174
  );
175
+ },
176
+ (prevProps, nextProps) => {
177
+ // Only re-render if the data actually changed (deep comparison by stringifying)
178
+ return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data);
179
+ },
180
+ );
181
+
182
+ MemoizedJsonView.displayName = "MemoizedJsonView";
183
+
184
+ // Helper function to parse JSON string to object
185
+ const parseJsonString = (json: string | object): any | null => {
186
+ // If it's already an object, return it directly
187
+ if (typeof json === "object" && json !== null) {
188
+ return json;
176
189
  }
177
190
 
178
191
  // Convert to string if not already
@@ -194,16 +207,7 @@ const renderJsonOrText = (json: string | object) => {
194
207
  }
195
208
  }
196
209
 
197
- return (
198
- <div className="font-mono text-xs" style={{ fontSize: "12px" }}>
199
- <JsonLightThemeStyles />
200
- <JsonView
201
- data={parsed}
202
- shouldExpandNode={(level) => level < 2}
203
- style={darkJsonStyles}
204
- />
205
- </div>
206
- );
210
+ return parsed;
207
211
  } catch (e) {
208
212
  console.log("JSON parse failed:", e, "Trying to handle as string...");
209
213
 
@@ -211,29 +215,32 @@ const renderJsonOrText = (json: string | object) => {
211
215
  try {
212
216
  const unescaped = jsonString.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
213
217
  const parsed = JSON.parse(unescaped);
214
-
215
- return (
216
- <div className="font-mono text-xs" style={{ fontSize: "12px" }}>
217
- <JsonLightThemeStyles />
218
- <JsonView
219
- data={parsed}
220
- shouldExpandNode={(level) => level < 2}
221
- style={darkJsonStyles}
222
- />
223
- </div>
224
- );
218
+ return parsed;
225
219
  } catch (e2) {
226
220
  console.log("Unescaping also failed:", e2);
227
- // If all parsing fails, display as plain text
228
- return (
229
- <div className="font-mono text-xs break-words whitespace-pre-wrap text-gray-700">
230
- {jsonString}
231
- </div>
232
- );
221
+ // If all parsing fails, return null to indicate we should display as plain text
222
+ return null;
233
223
  }
234
224
  }
235
225
  };
236
226
 
227
+ // Helper function to render JSON or text
228
+ const renderJsonOrText = (json: string | object) => {
229
+ const parsed = parseJsonString(json);
230
+
231
+ if (parsed !== null) {
232
+ return <MemoizedJsonView data={parsed} />;
233
+ }
234
+
235
+ // If parsing failed, display as plain text
236
+ const jsonString = typeof json === "string" ? json : String(json);
237
+ return (
238
+ <div className="font-mono text-xs break-words whitespace-pre-wrap text-gray-700">
239
+ {jsonString}
240
+ </div>
241
+ );
242
+ };
243
+
237
244
  // Expandable panel component
238
245
  const ExpandablePanel = ({
239
246
  title,
@@ -277,44 +284,74 @@ const ExpandablePanel = ({
277
284
  };
278
285
 
279
286
  // Helper function to create expandable tool call details
280
- const ToolCallDetails = ({
281
- toolCall,
282
- result,
283
- }: {
284
- toolCall: BaseToolCall;
285
- result?: string;
286
- }) => {
287
- const hasError = toolCall.function?.error;
288
- const hasOutput = result || hasError;
287
+ const ToolCallDetails = memo(
288
+ ({ toolCall, result }: { toolCall: BaseToolCall; result?: string }) => {
289
+ const hasError = toolCall.function?.error;
290
+ const hasOutput = result || hasError;
289
291
 
290
- return (
291
- <div className="mt-2 ml-4 overflow-hidden rounded-lg border border-gray-200/80 bg-gradient-to-br from-gray-50 to-gray-50/30 shadow-sm">
292
- <ExpandablePanel title="Input" defaultExpanded={!hasOutput}>
293
- <div className="rounded-md border border-gray-200 bg-white p-3 text-xs shadow-sm">
294
- {renderJsonOrText(toolCall.function?.arguments || "")}
295
- </div>
296
- </ExpandablePanel>
297
-
298
- {hasOutput && (
299
- <ExpandablePanel
300
- title={hasError ? "Error" : "Output"}
301
- defaultExpanded={true}
302
- >
303
- {hasError ? (
304
- <div className="rounded-md border-l-4 border-red-500 bg-red-50/80 p-3 text-xs text-red-700 shadow-sm">
305
- <div className="mb-1.5 font-semibold">Tool Error:</div>
306
- <div className="text-red-600">{toolCall.function?.error}</div>
307
- </div>
308
- ) : (
309
- <div className="rounded-md border border-gray-200 bg-white p-3 text-xs shadow-sm">
310
- {renderJsonOrText(result || "")}
311
- </div>
312
- )}
292
+ // Memoize parsed input data to prevent re-parsing on every render
293
+ const parsedInput = useMemo(() => {
294
+ return parseJsonString(toolCall.function?.arguments || "");
295
+ }, [toolCall.function?.arguments]);
296
+
297
+ // Memoize parsed output data to prevent re-parsing on every render
298
+ const parsedOutput = useMemo(() => {
299
+ return parseJsonString(result || "");
300
+ }, [result]);
301
+
302
+ return (
303
+ <div className="mt-2 ml-4 overflow-hidden rounded-lg border border-gray-200/80 bg-gradient-to-br from-gray-50 to-gray-50/30 shadow-sm">
304
+ <ExpandablePanel title="Input" defaultExpanded={!hasOutput}>
305
+ <div className="rounded-md border border-gray-200 bg-white p-3 text-xs shadow-sm">
306
+ {parsedInput !== null ? (
307
+ <MemoizedJsonView data={parsedInput} />
308
+ ) : (
309
+ <div className="font-mono text-xs break-words whitespace-pre-wrap text-gray-700">
310
+ {toolCall.function?.arguments || ""}
311
+ </div>
312
+ )}
313
+ </div>
313
314
  </ExpandablePanel>
314
- )}
315
- </div>
316
- );
317
- };
315
+
316
+ {hasOutput && (
317
+ <ExpandablePanel
318
+ title={hasError ? "Error" : "Output"}
319
+ defaultExpanded={true}
320
+ >
321
+ {hasError ? (
322
+ <div className="rounded-md border-l-4 border-red-500 bg-red-50/80 p-3 text-xs text-red-700 shadow-sm">
323
+ <div className="mb-1.5 font-semibold">Tool Error:</div>
324
+ <div className="text-red-600">{toolCall.function?.error}</div>
325
+ </div>
326
+ ) : (
327
+ <div className="rounded-md border border-gray-200 bg-white p-3 text-xs shadow-sm">
328
+ {parsedOutput !== null ? (
329
+ <MemoizedJsonView data={parsedOutput} />
330
+ ) : (
331
+ <div className="font-mono text-xs break-words whitespace-pre-wrap text-gray-700">
332
+ {result || ""}
333
+ </div>
334
+ )}
335
+ </div>
336
+ )}
337
+ </ExpandablePanel>
338
+ )}
339
+ </div>
340
+ );
341
+ },
342
+ (prevProps, nextProps) => {
343
+ // Only re-render if the data actually changed
344
+ return (
345
+ prevProps.toolCall.id === nextProps.toolCall.id &&
346
+ prevProps.toolCall.function?.arguments ===
347
+ nextProps.toolCall.function?.arguments &&
348
+ prevProps.result === nextProps.result &&
349
+ prevProps.toolCall.function?.error === nextProps.toolCall.function?.error
350
+ );
351
+ },
352
+ );
353
+
354
+ ToolCallDetails.displayName = "ToolCallDetails";
318
355
 
319
356
  export function ToolCallDisplay({
320
357
  toolCalls,
@@ -728,41 +728,120 @@ export function ComponentTree({}) {
728
728
  return { placeholder: null, indexInPlaceholder: 0 };
729
729
  };
730
730
 
731
- // Handle drag over node
732
- const handleDragOverZone = (
733
- dragOverNode: CustomTreeNode | null,
734
- index: number,
735
- event: React.DragEvent,
736
- ): boolean => {
737
- if (!editContext?.dragObject) return false;
738
- const { placeholder } = resolveDropContext(dragOverNode, index);
739
- if (!placeholder) return false;
740
- const isValid = isValidPlaceholder(placeholder, editContext.dragObject);
741
- event.dataTransfer.dropEffect = isValid ? "move" : "none";
742
- return isValid;
743
- };
731
+ // Handle drag over node (memoized)
732
+ const handleDragOverZone = useCallback(
733
+ (
734
+ dragOverNode: TreeNode | null,
735
+ index: number,
736
+ event: React.DragEvent,
737
+ ): boolean => {
738
+ if (!editContext?.dragObject) return false;
739
+ const { placeholder } = resolveDropContext(
740
+ dragOverNode as CustomTreeNode | null,
741
+ index,
742
+ );
743
+ if (!placeholder) return false;
744
+ const isValid = isValidPlaceholder(placeholder, editContext.dragObject);
745
+ event.dataTransfer.dropEffect = isValid ? "move" : "none";
746
+ return isValid;
747
+ },
748
+ [editContext?.dragObject],
749
+ );
744
750
 
745
- // Handle drop on node
746
- const handleDropZone = (
747
- droppedOnNode: CustomTreeNode | null,
748
- index: number,
749
- event: React.DragEvent,
750
- ): void => {
751
- // When dropping on a node (index < 0), use legacy behavior
752
- if (index < 0) {
753
- const placeholder = getPlaceholder(droppedOnNode);
751
+ // Handle drop on node (memoized)
752
+ const handleDropZone = useCallback(
753
+ (
754
+ droppedOnNode: TreeNode | null,
755
+ index: number,
756
+ event: React.DragEvent,
757
+ ): void => {
758
+ // When dropping on a node (index < 0), use legacy behavior
759
+ if (index < 0) {
760
+ const placeholder = getPlaceholder(
761
+ droppedOnNode as CustomTreeNode | null,
762
+ );
763
+ if (!placeholder) return;
764
+ editContext!.droppedInPlaceholder(placeholder.key, index);
765
+ return;
766
+ }
767
+
768
+ const { placeholder, indexInPlaceholder } = resolveDropContext(
769
+ droppedOnNode as CustomTreeNode | null,
770
+ index,
771
+ );
754
772
  if (!placeholder) return;
755
- editContext!.droppedInPlaceholder(placeholder.key, index);
756
- return;
757
- }
773
+ editContext!.droppedInPlaceholder(placeholder.key, indexInPlaceholder);
774
+ },
775
+ [editContext],
776
+ );
758
777
 
759
- const { placeholder, indexInPlaceholder } = resolveDropContext(
760
- droppedOnNode,
761
- index,
762
- );
763
- if (!placeholder) return;
764
- editContext!.droppedInPlaceholder(placeholder.key, indexInPlaceholder);
765
- };
778
+ // Memoize isValidDropZone callback
779
+ const handleIsValidDropZone = useCallback(
780
+ (parent: TreeNode | null, index: number): boolean => {
781
+ const { placeholder } = resolveDropContext(
782
+ parent as CustomTreeNode | null,
783
+ index,
784
+ );
785
+ if (!placeholder) return false;
786
+ if (!editContext?.dragObject) return false;
787
+ const result = isValidPlaceholder(placeholder, editContext.dragObject);
788
+ return result;
789
+ },
790
+ [editContext?.dragObject],
791
+ );
792
+
793
+ // Memoize onStartDrag callback
794
+ const handleStartDrag = useCallback(
795
+ (data: {
796
+ node: TreeNode;
797
+ event: React.DragEvent;
798
+ isMultiSelect: boolean;
799
+ }) => {
800
+ const component = data.node.data as Component;
801
+
802
+ // Initialize drag data to make sure dataTransfer is set
803
+ data.event.dataTransfer.setData("text/plain", data.node.key);
804
+ // Align with FrameMenu: include componentId and allowed effect
805
+ data.event.dataTransfer.setData("componentId", component.id);
806
+ data.event.dataTransfer.effectAllowed = "copyMove";
807
+
808
+ // Only create drag object for components with datasourceItem
809
+ if (!component?.datasourceItem) return;
810
+ setTimeout(() => {
811
+ const language = editContext!.page!.item.language;
812
+ const version = editContext!.page!.item.version;
813
+ const selectedIds =
814
+ (editContext?.selection?.length || 0) > 1 &&
815
+ editContext!.selection.includes(component.id)
816
+ ? editContext!.selection
817
+ : [component.id];
818
+ const componentsToDrag = selectedIds.map((id) => ({
819
+ id,
820
+ language,
821
+ version,
822
+ }));
823
+
824
+ editContext!.dragStart({
825
+ type: "component",
826
+ typeId: component.typeId,
827
+ templateId: component.datasourceItem?.templateId,
828
+ name: component.name,
829
+ components: componentsToDrag,
830
+ });
831
+ }, 50);
832
+ // Prevent any upstream handlers from interfering
833
+ data.event.stopPropagation();
834
+ },
835
+ [editContext],
836
+ );
837
+
838
+ // Memoize onDragEnd callback
839
+ const handleDragEnd = useCallback(
840
+ (event: React.DragEvent | null) => {
841
+ editContext!.dragEnd();
842
+ },
843
+ [editContext],
844
+ );
766
845
 
767
846
  if (!page) {
768
847
  if (editContext?.contentEditorItem?.hasLayout) {
@@ -829,65 +908,11 @@ export function ComponentTree({}) {
829
908
  onSelect={handleTreeSelection}
830
909
  onContextMenu={handleTreeContextMenu}
831
910
  enableDragAndDrop={true}
832
- onDragOverZone={(parent, index, event) =>
833
- handleDragOverZone(parent as CustomTreeNode | null, index, event)
834
- }
835
- isValidDropZone={(parent, index) => {
836
- const { placeholder } = resolveDropContext(
837
- parent as CustomTreeNode | null,
838
- index,
839
- );
840
- if (!placeholder) return false;
841
- if (!editContext?.dragObject) return false;
842
- const result = isValidPlaceholder(
843
- placeholder,
844
- editContext.dragObject,
845
- );
846
-
847
- return result;
848
- }}
849
- onStartDrag={(data) => {
850
- const component = data.node.data as Component;
851
-
852
- // Initialize drag data to make sure dataTransfer is set
853
- data.event.dataTransfer.setData("text/plain", data.node.key);
854
- // Align with FrameMenu: include componentId and allowed effect
855
- data.event.dataTransfer.setData("componentId", component.id);
856
- data.event.dataTransfer.effectAllowed = "copyMove";
857
-
858
- // Only create drag object for components with datasourceItem
859
- if (!component?.datasourceItem) return;
860
- setTimeout(() => {
861
- const language = editContext!.page!.item.language;
862
- const version = editContext!.page!.item.version;
863
- const selectedIds =
864
- (editContext?.selection?.length || 0) > 1 &&
865
- editContext!.selection.includes(component.id)
866
- ? editContext!.selection
867
- : [component.id];
868
- const componentsToDrag = selectedIds.map((id) => ({
869
- id,
870
- language,
871
- version,
872
- }));
873
-
874
- editContext!.dragStart({
875
- type: "component",
876
- typeId: component.typeId,
877
- templateId: component.datasourceItem?.templateId,
878
- name: component.name,
879
- components: componentsToDrag,
880
- });
881
- }, 50);
882
- // Prevent any upstream handlers from interfering
883
- data.event.stopPropagation();
884
- }}
885
- onDragEnd={(event) => {
886
- editContext!.dragEnd();
887
- }}
888
- onDrop={(parent, index, event) =>
889
- handleDropZone(parent as CustomTreeNode | null, index, event)
890
- }
911
+ onDragOverZone={handleDragOverZone}
912
+ isValidDropZone={handleIsValidDropZone}
913
+ onStartDrag={handleStartDrag}
914
+ onDragEnd={handleDragEnd}
915
+ onDrop={handleDropZone}
891
916
  renderNode={(node) => renderNode(node as CustomTreeNode)}
892
917
  />
893
918
  </div>
@@ -945,12 +945,16 @@ export const PerfectTree = <T,>({
945
945
  onDragOverZone,
946
946
  onDrop,
947
947
  onDragEnd,
948
- onStartDrag,
948
+ handleStartDragWithTimeout,
949
949
  onDoubleClick,
950
+ onContextMenu,
950
951
  handleSelect,
951
952
  handleToggle,
952
953
  enhancedRenderNode,
953
954
  searchTerm,
955
+ enableDragAndDrop,
956
+ multiDragNoun,
957
+ isValidDropZone,
954
958
  ],
955
959
  );
956
960
 
@@ -1053,4 +1057,49 @@ export const PerfectTree = <T,>({
1053
1057
  );
1054
1058
  };
1055
1059
 
1056
- export default memo(PerfectTree);
1060
+ // Custom comparison function for memo to prevent unnecessary re-renders
1061
+ // when callback props change but actual data hasn't
1062
+ const arePropsEqual = <T,>(
1063
+ prevProps: Readonly<TreeProps<T>>,
1064
+ nextProps: Readonly<TreeProps<T>>,
1065
+ ): boolean => {
1066
+ // Compare primitive and array props
1067
+ if (
1068
+ prevProps.nodes !== nextProps.nodes ||
1069
+ prevProps.isDragging !== nextProps.isDragging ||
1070
+ prevProps.enableDragAndDrop !== nextProps.enableDragAndDrop ||
1071
+ prevProps.scrollToSelected !== nextProps.scrollToSelected ||
1072
+ prevProps.enableKeyboardSearch !== nextProps.enableKeyboardSearch ||
1073
+ prevProps.searchClearDelay !== nextProps.searchClearDelay ||
1074
+ prevProps.disableAutoSelectOnExpand !==
1075
+ nextProps.disableAutoSelectOnExpand ||
1076
+ prevProps.multiDragNoun !== nextProps.multiDragNoun
1077
+ ) {
1078
+ return false;
1079
+ }
1080
+
1081
+ // Compare arrays with shallow equality
1082
+ if (
1083
+ prevProps.selectedKeys?.length !== nextProps.selectedKeys?.length ||
1084
+ prevProps.selectedKeys?.some(
1085
+ (key, i) => key !== nextProps.selectedKeys?.[i],
1086
+ )
1087
+ ) {
1088
+ return false;
1089
+ }
1090
+
1091
+ if (
1092
+ prevProps.expandedKeys?.length !== nextProps.expandedKeys?.length ||
1093
+ prevProps.expandedKeys?.some(
1094
+ (key, i) => key !== nextProps.expandedKeys?.[i],
1095
+ )
1096
+ ) {
1097
+ return false;
1098
+ }
1099
+
1100
+ // For callback props, we assume they are stable if memoized in parent
1101
+ // If they change on every render, the parent should memoize them with useCallback
1102
+ return true;
1103
+ };
1104
+
1105
+ export default memo(PerfectTree, arePropsEqual) as typeof PerfectTree;
package/src/revision.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const version = "1.0.4147";
2
- export const buildDate = "2025-10-06 22:12:00";
1
+ export const version = "1.0.4149";
2
+ export const buildDate = "2025-10-07 09:38:14";