@alpaca-editor/core 1.0.4013 → 1.0.4014

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 (76) hide show
  1. package/dist/editor/Terminal.d.ts +6 -0
  2. package/dist/editor/Terminal.js +10 -4
  3. package/dist/editor/Terminal.js.map +1 -1
  4. package/dist/editor/ai/Agents.js +133 -13
  5. package/dist/editor/ai/Agents.js.map +1 -1
  6. package/dist/editor/ai/AiTerminal.d.ts +2 -1
  7. package/dist/editor/ai/AiTerminal.js +86 -139
  8. package/dist/editor/ai/AiTerminal.js.map +1 -1
  9. package/dist/editor/media-selector/AiImageSearch.js +2 -1
  10. package/dist/editor/media-selector/AiImageSearch.js.map +1 -1
  11. package/dist/editor/media-selector/AiImageSearchPrompt.js +2 -1
  12. package/dist/editor/media-selector/AiImageSearchPrompt.js.map +1 -1
  13. package/dist/editor/page-editor-chrome/useInlineAICompletion.js +4 -2
  14. package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
  15. package/dist/editor/services/agentService.d.ts +59 -0
  16. package/dist/editor/services/agentService.js +26 -0
  17. package/dist/editor/services/agentService.js.map +1 -0
  18. package/dist/editor/services/aiService.d.ts +22 -4
  19. package/dist/editor/services/aiService.js +131 -20
  20. package/dist/editor/services/aiService.js.map +1 -1
  21. package/dist/editor/services/contextService.js +6 -4
  22. package/dist/editor/services/contextService.js.map +1 -1
  23. package/dist/editor/sidebar/SidebarView.js +2 -5
  24. package/dist/editor/sidebar/SidebarView.js.map +1 -1
  25. package/dist/editor/sidebar/ViewSelector.js +1 -1
  26. package/dist/editor/sidebar/ViewSelector.js.map +1 -1
  27. package/dist/editor/ui/SimpleIconButton.d.ts +2 -2
  28. package/dist/editor/ui/SimpleIconButton.js +5 -3
  29. package/dist/editor/ui/SimpleIconButton.js.map +1 -1
  30. package/dist/editor/utils/jsonCleaner.d.ts +8 -0
  31. package/dist/editor/utils/jsonCleaner.js +76 -0
  32. package/dist/editor/utils/jsonCleaner.js.map +1 -0
  33. package/dist/editor/views/CompareView.js +2 -2
  34. package/dist/editor/views/CompareView.js.map +1 -1
  35. package/dist/page-wizard/steps/ContentStep.js +7 -2
  36. package/dist/page-wizard/steps/ContentStep.js.map +1 -1
  37. package/dist/page-wizard/steps/FindItemsStep.js +4 -1
  38. package/dist/page-wizard/steps/FindItemsStep.js.map +1 -1
  39. package/dist/page-wizard/steps/ImagesStep.js +13 -7
  40. package/dist/page-wizard/steps/ImagesStep.js.map +1 -1
  41. package/dist/page-wizard/steps/LayoutStep.d.ts +1 -1
  42. package/dist/page-wizard/steps/LayoutStep.js +22 -20
  43. package/dist/page-wizard/steps/LayoutStep.js.map +1 -1
  44. package/dist/page-wizard/steps/MetaDataStep.js +8 -15
  45. package/dist/page-wizard/steps/MetaDataStep.js.map +1 -1
  46. package/dist/page-wizard/steps/SelectStep.js +9 -4
  47. package/dist/page-wizard/steps/SelectStep.js.map +1 -1
  48. package/dist/page-wizard/steps/StructureStep.js +3 -1
  49. package/dist/page-wizard/steps/StructureStep.js.map +1 -1
  50. package/dist/revision.d.ts +2 -2
  51. package/dist/revision.js +2 -2
  52. package/dist/styles.css +10 -11
  53. package/package.json +1 -1
  54. package/src/editor/Terminal.tsx +12 -3
  55. package/src/editor/ai/Agents.tsx +212 -16
  56. package/src/editor/ai/AiTerminal.tsx +113 -173
  57. package/src/editor/ai/GhostWriter.tsx_ +3 -3
  58. package/src/editor/media-selector/AiImageSearch.tsx +2 -2
  59. package/src/editor/media-selector/AiImageSearchPrompt.tsx +2 -2
  60. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +5 -5
  61. package/src/editor/services/agentService.ts +83 -0
  62. package/src/editor/services/aiService.ts +176 -33
  63. package/src/editor/services/contextService.ts +5 -6
  64. package/src/editor/sidebar/SidebarView.tsx +10 -6
  65. package/src/editor/sidebar/ViewSelector.tsx +2 -2
  66. package/src/editor/ui/SimpleIconButton.tsx +20 -14
  67. package/src/editor/utils/jsonCleaner.ts +92 -0
  68. package/src/editor/views/CompareView.tsx +2 -2
  69. package/src/page-wizard/steps/ContentStep.tsx +10 -9
  70. package/src/page-wizard/steps/FindItemsStep.tsx +7 -5
  71. package/src/page-wizard/steps/ImagesStep.tsx +16 -11
  72. package/src/page-wizard/steps/LayoutStep.tsx +24 -28
  73. package/src/page-wizard/steps/MetaDataStep.tsx +11 -21
  74. package/src/page-wizard/steps/SelectStep.tsx +11 -8
  75. package/src/page-wizard/steps/StructureStep.tsx +4 -5
  76. package/src/revision.ts +2 -2
@@ -44,10 +44,10 @@ export function AiImageSearchPrompt({
44
44
  "Reply with search terms only!",
45
45
  name: "user",
46
46
  role: "user",
47
+ id: crypto.randomUUID(), // Use proper UUID instead of Date.now()
47
48
  },
48
49
  ],
49
- editContext,
50
- createEditorAiContext,
50
+ { editContext, createAiContext: createEditorAiContext },
51
51
  { allowedFunctions: ["get-content"], addContextContent: true },
52
52
  { signal: abortController.signal },
53
53
  );
@@ -350,11 +350,13 @@ ONLY provide the completion text (what comes after the user's text), never repea
350
350
  name: "system",
351
351
  role: "system",
352
352
  content: systemPrompt,
353
+ id: crypto.randomUUID(), // Use proper UUID instead of Date.now()
353
354
  },
354
355
  {
355
356
  name: "user",
356
357
  role: "user",
357
358
  content: `Complete this text. ONLY provide the completion (do not repeat my text): ${contentUpToCursor}`,
359
+ id: crypto.randomUUID(), // Use proper UUID instead of Date.now()
358
360
  },
359
361
  ];
360
362
 
@@ -365,8 +367,7 @@ ONLY provide the completion text (what comes after the user's text), never repea
365
367
  try {
366
368
  const result = await executePrompt(
367
369
  messages,
368
- editContext,
369
- () => ({
370
+ {
370
371
  endpoint: "/alpaca/editor/ai/complete",
371
372
  promptData: {
372
373
  itemid: itemId,
@@ -376,10 +377,9 @@ ONLY provide the completion text (what comes after the user's text), never repea
376
377
  fieldname: fieldName,
377
378
  content: contentUpToCursor,
378
379
  },
379
- }),
380
- {},
380
+ },
381
+ { model: "gpt-4.1-nano" },
381
382
  { signal },
382
- "gpt-4.1-nano",
383
383
  );
384
384
 
385
385
  return result;
@@ -0,0 +1,83 @@
1
+ import { get } from "./serviceHelper";
2
+
3
+ export interface AgentChat {
4
+ id: string;
5
+ name: string;
6
+ sessionId: string;
7
+ userId: string;
8
+ userDisplayName: string;
9
+ itemId: string;
10
+ itemPath: string;
11
+ language: string;
12
+ version: number;
13
+ profileId?: string;
14
+ profileName: string;
15
+ model: string;
16
+ status: "active" | "completed" | "error" | "abandoned";
17
+ createdDate: string;
18
+ updatedDate: string;
19
+ lastMessageDate?: string;
20
+ totalTokensUsed: number;
21
+ totalInputTokens: number;
22
+ totalOutputTokens: number;
23
+ messageCount: number;
24
+ metadata?: string;
25
+ messages?: AgentChatMessage[];
26
+ }
27
+
28
+ export interface AgentChatMessage {
29
+ id: string;
30
+ agentId: string;
31
+ messageIndex: number;
32
+ role: string;
33
+ content: string;
34
+ name: string;
35
+ toolCallId?: string;
36
+ functionName?: string;
37
+ functionArguments?: string;
38
+ functionResult?: any;
39
+ functionError?: string;
40
+ isCompleted: boolean;
41
+ model?: string;
42
+ tokensUsed: number;
43
+ inputTokens: number;
44
+ outputTokens: number;
45
+ responseTimeMs?: number;
46
+ createdDate: string;
47
+ editOperations?: any[];
48
+ longRunningTaskIds?: string[];
49
+ metadata?: string;
50
+ }
51
+
52
+ /**
53
+ * Gets all agents for the current user
54
+ */
55
+ export async function getAgents(
56
+ status?: "active" | "completed" | "error" | "abandoned",
57
+ limit = 100
58
+ ) {
59
+ const url = `/alpaca/editor/ai/GetAgents${status ? `?status=${status}` : ""}${limit ? `${status ? "&" : "?"}limit=${limit}` : ""}`;
60
+ const response = await get<AgentChat[]>(url);
61
+ return response;
62
+ }
63
+
64
+ /**
65
+ * Gets a specific agent with its messages
66
+ */
67
+ export async function getAgent(agentId: string) {
68
+ const url = `/alpaca/editor/ai/GetAgent?agentId=${agentId}`;
69
+ const response = await get<AgentChat>(url);
70
+ return response;
71
+ }
72
+
73
+ /**
74
+ * Gets chat history for the current user
75
+ */
76
+ export async function getChatHistory(
77
+ status?: "active" | "completed" | "error" | "abandoned",
78
+ limit = 50
79
+ ) {
80
+ const url = `/alpaca/editor/ai/GetChatHistory${status ? `?status=${status}` : ""}${limit ? `${status ? "&" : "?"}limit=${limit}` : ""}`;
81
+ const response = await get<AgentChat[]>(url);
82
+ return response;
83
+ }
@@ -1,6 +1,7 @@
1
1
  import { AiContext } from "../ai/AiTerminal";
2
2
  import { EditContextType } from "../client/editContext";
3
3
  import { FieldDescriptor, ItemDescriptor } from "../pageModel";
4
+ import { JsonCleaner } from "../utils/jsonCleaner";
4
5
 
5
6
  import { ExecutionResult, get, post } from "./serviceHelper";
6
7
 
@@ -32,6 +33,9 @@ type Message = {
32
33
  content: string;
33
34
  name: string;
34
35
  role: string;
36
+ id: string; // Changed from number to string to support UUIDs
37
+ tool_calls?: any[]; // Optional tool_calls for compatibility
38
+ tool_call_id?: string; // Optional tool_call_id for compatibility
35
39
  };
36
40
 
37
41
  export interface ExecutePromptResponse {
@@ -41,49 +45,117 @@ export interface ExecutePromptResponse {
41
45
  numOutputTokens: number;
42
46
  numCachedTokens: number;
43
47
  state: string;
48
+ // New properties for incremental updates
49
+ isIncremental?: boolean;
50
+ deltaContent?: string;
51
+ previousContentLength?: number;
52
+ totalContentLength?: number;
44
53
  }
45
54
 
55
+ // Unified options interface that supports both calling patterns
56
+ export interface ExecutePromptOptions {
57
+ // Common options
58
+ sessionId?: string | null;
59
+ model?: string | null;
60
+
61
+ // AiTerminal style options
62
+ profileId?: string;
63
+ selection?: string[];
64
+ selectedText?: string | null;
65
+ agentId?: string;
66
+ addSelectedComponents?: boolean;
67
+
68
+ // Other callers style options
69
+ allowedFunctions?: string[];
70
+ addContextContent?: boolean;
71
+ addAllContent?: boolean;
72
+ profile?: string;
73
+ }
74
+
75
+ // Unified function signature
46
76
  export async function executePrompt(
47
77
  messages: Message[],
48
- editContext: EditContextType,
49
- createAiContext: ({ editContext }: { editContext: any }) => AiContext,
50
- promptOptions: {
51
- allowedFunctions?: string[];
52
- addContextContent?: boolean;
53
- addAllContent?: boolean;
54
- profile?: string;
55
- },
56
-
57
- options?: RequestInit,
58
- model?: string,
78
+ context:
79
+ | AiContext
80
+ | {
81
+ editContext: EditContextType;
82
+ createAiContext: ({ editContext }: { editContext: any }) => AiContext;
83
+ },
84
+ options: ExecutePromptOptions = {},
85
+ requestOptions?: RequestInit,
59
86
  callback?: (response: any) => void,
60
87
  ): Promise<ExecutePromptResponse | null> {
61
- const context = createAiContext({ editContext });
88
+ let endpoint: string;
89
+ let requestBody: any;
90
+ let finalRequestOptions: RequestInit;
62
91
 
63
- const response = await fetch(context.endpoint, {
64
- method: "POST",
65
- body: JSON.stringify({
66
- messages,
67
- ...context.promptData,
68
- sessionId: editContext.sessionId,
69
- ...promptOptions,
70
- model,
71
- }),
92
+ // Handle context - either direct AiContext or need to create it
93
+ let aiContext: AiContext;
94
+ if ("endpoint" in context) {
95
+ // Direct AiContext (AiTerminal style)
96
+ aiContext = context;
97
+ } else {
98
+ // Create context (other callers style)
99
+ aiContext = context.createAiContext({ editContext: context.editContext });
100
+ // Default sessionId from editContext if not provided
101
+ if (options.sessionId === undefined) {
102
+ options.sessionId = context.editContext.sessionId;
103
+ }
104
+ }
105
+
106
+ endpoint = aiContext.endpoint;
72
107
 
108
+ // Build unified request body
109
+ requestBody = {
110
+ messages,
111
+ ...aiContext.promptData,
112
+ sessionId: options.sessionId,
113
+ model: options.model,
114
+
115
+ // Include all provided options
116
+ ...options,
117
+ };
118
+
119
+ finalRequestOptions = {
120
+ method: "POST",
121
+ body: JSON.stringify(requestBody),
73
122
  credentials: "include",
74
123
  headers: {
75
124
  "Content-Type": "application/json",
76
125
  },
77
- ...options,
78
- });
126
+ ...requestOptions,
127
+ };
128
+
129
+ const response = await fetch(endpoint, finalRequestOptions);
130
+
131
+ if (!response.ok) {
132
+ // Always return rich error format
133
+ const text = await response.text();
134
+ return {
135
+ messages: [
136
+ {
137
+ content: "There was an error processing your request: " + text,
138
+ name: "assistant",
139
+ role: "assistant",
140
+ id: crypto.randomUUID(),
141
+ tool_calls: [],
142
+ },
143
+ ],
144
+ editOperations: [],
145
+ numInputTokens: 0,
146
+ numOutputTokens: 0,
147
+ numCachedTokens: 0,
148
+ state: "error",
149
+ };
150
+ }
79
151
 
80
152
  if (!response?.body) return null;
81
153
 
82
154
  const reader = response.body.getReader();
83
155
  const decoder = new TextDecoder();
84
156
  let buffer = "";
85
-
86
157
  let result = null;
158
+ let accumulatedContent = "";
87
159
 
88
160
  while (true) {
89
161
  const { done, value } = await reader.read();
@@ -92,21 +164,58 @@ export async function executePrompt(
92
164
  break;
93
165
  }
94
166
 
95
- buffer += decoder.decode(value, { stream: true }); // 'stream: true' ensures that any incomplete multi-byte characters aren't malformed.
96
-
97
- // Split the buffer by newline and keep the last partial line for the next iteration.
167
+ buffer += decoder.decode(value, { stream: true });
98
168
  const lines = buffer.split("\n");
99
169
 
100
170
  if (lines.length > 0) {
101
- buffer = lines.pop() || ""; // Incomplete line (if any) is kept for the next iteration.
171
+ buffer = lines.pop() || "";
102
172
 
103
173
  for (let line of lines) {
104
- if (line.trim() === "") continue; // Skip empty lines if any.
174
+ if (line.trim() === "") continue;
105
175
 
106
176
  try {
107
177
  const jsonData = JSON.parse(line);
108
- callback?.(jsonData);
109
- result = jsonData;
178
+
179
+ if (jsonData.isIncremental) {
180
+ if (jsonData.deltaContent) {
181
+ accumulatedContent += jsonData.deltaContent;
182
+ }
183
+
184
+ // Always apply JsonCleaner for streaming responses
185
+ const cleanupResult = JsonCleaner.cleanupJson(accumulatedContent);
186
+
187
+ const incrementalResponse = {
188
+ ...jsonData,
189
+ messages: [
190
+ {
191
+ content: cleanupResult.cleanedJson,
192
+ role: "assistant",
193
+ name: "assistant",
194
+ id: Date.now(),
195
+ tool_calls: [],
196
+ },
197
+ ],
198
+ };
199
+
200
+ callback?.(incrementalResponse);
201
+ result = incrementalResponse;
202
+ } else {
203
+ // Always ensure messages have IDs and tool_calls
204
+ let processedResponse = jsonData;
205
+ if (jsonData.messages) {
206
+ processedResponse = {
207
+ ...jsonData,
208
+ messages: jsonData.messages.map((msg: any, index: number) => ({
209
+ ...msg,
210
+ id: msg.id || Date.now() + index,
211
+ tool_calls: msg.tool_calls || [],
212
+ })),
213
+ };
214
+ }
215
+
216
+ callback?.(processedResponse);
217
+ result = processedResponse;
218
+ }
110
219
  } catch (e) {
111
220
  console.error("Error parsing line:" + line, e);
112
221
  }
@@ -114,11 +223,45 @@ export async function executePrompt(
114
223
  }
115
224
  }
116
225
 
117
- // If there's any remaining content in the buffer after processing all chunks, try to process it.
226
+ // Handle final buffer
118
227
  if (buffer.trim() !== "") {
119
228
  try {
120
229
  const jsonData = JSON.parse(buffer);
121
- result = jsonData;
230
+
231
+ if (jsonData.isIncremental) {
232
+ if (jsonData.deltaContent) {
233
+ accumulatedContent += jsonData.deltaContent;
234
+ }
235
+
236
+ const cleanupResult = JsonCleaner.cleanupJson(accumulatedContent);
237
+
238
+ result = {
239
+ ...jsonData,
240
+ messages: [
241
+ {
242
+ content: cleanupResult.cleanedJson,
243
+ role: "assistant",
244
+ name: "assistant",
245
+ id: Date.now(),
246
+ tool_calls: [],
247
+ },
248
+ ],
249
+ };
250
+ } else {
251
+ // Always ensure messages have IDs
252
+ if (jsonData.messages) {
253
+ result = {
254
+ ...jsonData,
255
+ messages: jsonData.messages.map((msg: any, index: number) => ({
256
+ ...msg,
257
+ id: msg.id || Date.now() + index,
258
+ tool_calls: msg.tool_calls || [],
259
+ })),
260
+ };
261
+ } else {
262
+ result = jsonData;
263
+ }
264
+ }
122
265
  } catch (e) {
123
266
  console.error("Error parsing the final buffer content:", e);
124
267
  }
@@ -51,28 +51,27 @@ export async function generatePageContext(
51
51
  role: "system",
52
52
  content:
53
53
  "You are a content summarization tool. You have access to a get-content function that can extract text content from the current page. Use this function to get the page content, then create a concise abstract in exactly 100 tokens or less. Focus on the main topics, purpose, and key information. Be specific and informative. Your summary will be fed into a text completion prompt, so make it as concise as possible.",
54
+ id: crypto.randomUUID(), // Use proper UUID instead of Date.now()
54
55
  },
55
56
  ];
56
57
 
57
58
  const result = await executePrompt(
58
59
  messages,
59
- editContext,
60
- () => ({
60
+ {
61
61
  endpoint: "/alpaca/editor/ai/prompt",
62
62
  promptData: {
63
63
  itemid: editContext.contentEditorItem!.id,
64
64
  language: editContext.contentEditorItem!.language,
65
65
  version: editContext.contentEditorItem!.version,
66
66
  },
67
- allowedFunctions: ["get-content"],
68
- }),
67
+ },
69
68
  {
70
69
  profile: "context-generation",
71
70
  addContextContent: false,
72
71
  addAllContent: false,
72
+ model: "gpt-4o-mini", // Use a fast, efficient model for context generation
73
+ allowedFunctions: ["get-content"],
73
74
  },
74
- undefined,
75
- "gpt-4o-mini", // Use a fast, efficient model for context generation
76
75
  );
77
76
 
78
77
  console.log("result", result);
@@ -52,10 +52,6 @@ export function SidebarView({
52
52
  );
53
53
  };
54
54
 
55
- if (!active) {
56
- return <div className="hidden h-full" />;
57
- }
58
-
59
55
  // Single panel - no splitter needed
60
56
  if (resolvedPanels.length === 1) {
61
57
  const panel = resolvedPanels[0];
@@ -63,7 +59,11 @@ export function SidebarView({
63
59
 
64
60
  return (
65
61
  <div
66
- className={cn("h-full", detached ? "p-2" : "border-gray-3 border-r")}
62
+ className={cn(
63
+ "h-full",
64
+ detached ? "p-2" : "border-gray-3 border-r",
65
+ !active ? "hidden" : ""
66
+ )}
67
67
  >
68
68
  <div
69
69
  className={cn(
@@ -111,7 +111,11 @@ export function SidebarView({
111
111
  );
112
112
 
113
113
  return (
114
- <div className={cn("h-full", detached ? "p-2" : "")}>
114
+ <div className={cn(
115
+ "h-full",
116
+ detached ? "p-2" : "",
117
+ !active ? "hidden" : ""
118
+ )}>
115
119
  <Splitter
116
120
  panels={splitterPanels}
117
121
  direction="vertical"
@@ -140,8 +140,8 @@ export function ViewSelector() {
140
140
  <VerticalDotsIcon />
141
141
  </Button>
142
142
  </PopoverTrigger>
143
- </TooltipTrigger>
144
- <TooltipContent>More views</TooltipContent>
143
+ </TooltipTrigger >
144
+ <TooltipContent side="right">More views</TooltipContent>
145
145
  </Tooltip>
146
146
  <PopoverContent
147
147
  className="w-56 p-2"
@@ -5,9 +5,22 @@ import {
5
5
  TooltipTrigger,
6
6
  } from "../../components/ui/tooltip";
7
7
  import { cn } from "../../lib/utils";
8
- import { MouseEventHandler } from "react";
8
+ import { MouseEventHandler, forwardRef } from "react";
9
9
 
10
- export function SimpleIconButton({
10
+ export const SimpleIconButton = forwardRef<
11
+ HTMLButtonElement,
12
+ {
13
+ onClick: MouseEventHandler;
14
+ className?: string;
15
+ icon?: React.ReactNode;
16
+ label: string;
17
+ disabled?: boolean;
18
+ id?: string;
19
+ size?: "large" | "small";
20
+ selected?: boolean;
21
+ children?: React.ReactNode;
22
+ }
23
+ >(({
11
24
  onClick,
12
25
  className,
13
26
  icon,
@@ -17,21 +30,12 @@ export function SimpleIconButton({
17
30
  id,
18
31
  selected,
19
32
  children,
20
- }: {
21
- onClick: MouseEventHandler;
22
- className?: string;
23
- icon?: React.ReactNode;
24
- label: string;
25
- disabled?: boolean;
26
- id?: string;
27
- size?: "large" | "small";
28
- selected?: boolean;
29
- children?: React.ReactNode;
30
- }) {
33
+ }, ref) => {
31
34
  return (
32
35
  <Tooltip>
33
36
  <TooltipTrigger asChild>
34
37
  <button
38
+ ref={ref}
35
39
  id={id}
36
40
  disabled={disabled}
37
41
  className={cn(
@@ -54,4 +58,6 @@ export function SimpleIconButton({
54
58
  <TooltipContent>{label}</TooltipContent>
55
59
  </Tooltip>
56
60
  );
57
- }
61
+ });
62
+
63
+ SimpleIconButton.displayName = "SimpleIconButton";
@@ -0,0 +1,92 @@
1
+ export interface JsonCleanupResult {
2
+ cleanedJson: string;
3
+ isComplete: boolean;
4
+ }
5
+
6
+ export class JsonCleaner {
7
+ public static cleanupJson(json: string): JsonCleanupResult {
8
+ // Handle dangling commas
9
+ json = json.replace(/,}/g, "}");
10
+ json = json.replace(/,]/g, "]");
11
+
12
+ const stringResult = this.correctUnendedStrings(json);
13
+ json = stringResult.correctedJson;
14
+ let isComplete = stringResult.isComplete;
15
+
16
+ // Process the JSON string character by character
17
+ let isInString = false;
18
+ let isEscape = false;
19
+ const openStack: string[] = [];
20
+
21
+ for (let i = 0; i < json.length; i++) {
22
+ const c = json[i];
23
+
24
+ if (c === '"' && !isEscape) {
25
+ isInString = !isInString;
26
+ }
27
+
28
+ if (!isInString) {
29
+ switch (c) {
30
+ case '{':
31
+ openStack.push(c);
32
+ break;
33
+ case '}':
34
+ if (openStack.length > 0) {
35
+ openStack.pop();
36
+ }
37
+ break;
38
+ case '[':
39
+ openStack.push(c);
40
+ break;
41
+ case ']':
42
+ if (openStack.length > 0) {
43
+ openStack.pop();
44
+ }
45
+ break;
46
+ }
47
+ }
48
+
49
+ isEscape = isInString && c === '\\' && !isEscape;
50
+ }
51
+
52
+ // Append missing closing brackets
53
+ while (openStack.length > 0) {
54
+ const open = openStack.pop();
55
+ if (open === '{') {
56
+ json += "}";
57
+ } else if (open === '[') {
58
+ json += "]";
59
+ }
60
+ isComplete = false;
61
+ }
62
+
63
+ return {
64
+ cleanedJson: json,
65
+ isComplete: isComplete
66
+ };
67
+ }
68
+
69
+ private static correctUnendedStrings(json: string): { correctedJson: string; isComplete: boolean } {
70
+ let isComplete = true;
71
+ let isInString = false;
72
+ let isEscape = false;
73
+
74
+ for (let i = 0; i < json.length; i++) {
75
+ if (json[i] === '"' && !isEscape) {
76
+ isInString = !isInString;
77
+ }
78
+
79
+ isEscape = isInString && json[i] === '\\' && !isEscape;
80
+ }
81
+
82
+ if (isInString) {
83
+ json += '[...]"';
84
+ isComplete = false;
85
+ }
86
+
87
+ return {
88
+ correctedJson: json,
89
+ isComplete: isComplete
90
+ };
91
+ }
92
+ }
@@ -115,7 +115,7 @@ export function CompareView() {
115
115
  }
116
116
 
117
117
  const originalSelector = (
118
- <div className="flex flex-1 flex-shrink-0 items-center justify-center border-b bg-white p-1 text-sm text-gray-600">
118
+ <div className=" gap-1 flex flex-1 flex-shrink-0 items-center justify-center border-b bg-white p-1 text-sm text-gray-600">
119
119
  <LanguageSelector
120
120
  selectedLanguage={editContext.contentEditorItem?.descriptor?.language}
121
121
  showAllLanguagesSwitch={false}
@@ -149,7 +149,7 @@ export function CompareView() {
149
149
  );
150
150
 
151
151
  const compareToSelector = (
152
- <div className="flex flex-1 flex-shrink-0 items-center justify-center border-b bg-white p-1 text-sm text-gray-600">
152
+ <div className="gap-1 flex flex-1 flex-shrink-0 items-center justify-center border-b bg-white p-1 text-sm text-gray-600">
153
153
  <LanguageSelector
154
154
  selectedLanguage={compareTo?.language}
155
155
  showAllLanguagesSwitch={false}