victor HF Staff commited on
Commit
a450a2d
·
1 Parent(s): efeb0ba

refactor: Apply AI diffs server-side instead of client-side

Browse files
server.js CHANGED
@@ -402,6 +402,31 @@ function applyDiffs(originalHtml, aiResponseContent) {
402
  return currentHtml;
403
  }
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  // --- AI Interaction Route ---
406
  app.post("/api/ask-ai", async (req, res) => {
407
  const { prompt, html, previousPrompt } = req.body;
 
402
  return currentHtml;
403
  }
404
 
405
+
406
+ // --- Endpoint to Apply Diffs Server-Side ---
407
+ app.post("/api/apply-diffs", (req, res) => {
408
+ const { originalHtml, aiResponseContent } = req.body;
409
+
410
+ if (typeof originalHtml !== 'string' || typeof aiResponseContent !== 'string') {
411
+ return res.status(400).json({ ok: false, message: "Missing or invalid originalHtml or aiResponseContent." });
412
+ }
413
+
414
+ try {
415
+ console.log("[Apply Diffs] Received request to apply diffs.");
416
+ const modifiedHtml = applyDiffs(originalHtml, aiResponseContent);
417
+ console.log("[Apply Diffs] Diffs applied successfully.");
418
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
419
+ res.status(200).send(modifiedHtml);
420
+ } catch (error: any) {
421
+ console.error("[Apply Diffs] Error applying diffs:", error);
422
+ res.status(400).json({ // Use 400 for client-side correctable errors (bad diff format)
423
+ ok: false,
424
+ message: error.message || "Failed to apply AI suggestions.",
425
+ });
426
+ }
427
+ });
428
+
429
+
430
  // --- AI Interaction Route ---
431
  app.post("/api/ask-ai", async (req, res) => {
432
  const { prompt, html, previousPrompt } = req.body;
src/components/App.tsx CHANGED
@@ -19,7 +19,7 @@ function App() {
19
  const preview = useRef<HTMLDivElement>(null);
20
  const editor = useRef<HTMLDivElement>(null);
21
  const resizer = useRef<HTMLDivElement>(null);
22
- const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
23
 
24
  const [isResizing, setIsResizing] = useState(false);
25
  const [error, setError] = useState(false);
@@ -148,9 +148,7 @@ function App() {
148
  setHtml(defaultHTML);
149
  setError(false);
150
  removeHtmlStorage();
151
- editorRef.current?.revealLine(
152
- editorRef.current?.getModel()?.getLineCount() ?? 0
153
- );
154
  }
155
  }}
156
  >
@@ -189,18 +187,17 @@ function App() {
189
  setHtml(newValue);
190
  setError(false);
191
  }}
192
- onMount={(editor) => (editorRef.current = editor)}
193
  />
194
  <AskAI
195
  html={html}
196
- setHtml={setHtml} // Still needed for full HTML updates
197
- editorRef={editorRef} // Pass the editor ref
198
  isAiWorking={isAiWorking}
199
  setisAiWorking={setisAiWorking}
200
  onScrollToBottom={() => {
201
- editorRef.current?.revealLine(
202
- editorRef.current?.getModel()?.getLineCount() ?? 0
203
- );
204
  }}
205
  />
206
  </div>
 
19
  const preview = useRef<HTMLDivElement>(null);
20
  const editor = useRef<HTMLDivElement>(null);
21
  const resizer = useRef<HTMLDivElement>(null);
22
+ // Removed editorRef
23
 
24
  const [isResizing, setIsResizing] = useState(false);
25
  const [error, setError] = useState(false);
 
148
  setHtml(defaultHTML);
149
  setError(false);
150
  removeHtmlStorage();
151
+ // Removed editorRef scroll logic
 
 
152
  }
153
  }}
154
  >
 
187
  setHtml(newValue);
188
  setError(false);
189
  }}
190
+ // Removed onMount for editorRef
191
  />
192
  <AskAI
193
  html={html}
194
+ setHtml={setHtml} // Used for both full and diff updates now
195
+ // Removed editorRef prop
196
  isAiWorking={isAiWorking}
197
  setisAiWorking={setisAiWorking}
198
  onScrollToBottom={() => {
199
+ // Consider if scrolling is still needed here, maybe based on html length change?
200
+ // For now, removing the direct editor scroll.
 
201
  }}
202
  />
203
  </div>
src/components/ask-ai/ask-ai.tsx CHANGED
@@ -1,9 +1,9 @@
1
- import { useState } from "react"; // Removed useRef import
2
  import { RiSparkling2Fill } from "react-icons/ri";
3
  import { GrSend } from "react-icons/gr";
4
  import classNames from "classnames";
5
  import { toast } from "react-toastify";
6
- import { editor } from "monaco-editor"; // Import editor type
7
 
8
  import Login from "../login/login";
9
  import { defaultHTML } from "../../utils/consts";
@@ -11,172 +11,35 @@ import SuccessSound from "./../../assets/success.mp3";
11
 
12
  function AskAI({
13
  html, // Current full HTML content (used for initial request and context)
14
- setHtml, // Used only for full updates now
15
  onScrollToBottom, // Used for full updates
16
  isAiWorking,
17
  setisAiWorking,
18
- editorRef, // Pass the editor instance ref
19
  }: {
20
  html: string;
21
  setHtml: (html: string) => void;
22
  onScrollToBottom: () => void;
23
  isAiWorking: boolean;
24
  setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
25
- editorRef: React.RefObject<editor.IStandaloneCodeEditor | null>; // Add editorRef prop
26
  }) {
27
  const [open, setOpen] = useState(false);
28
  const [prompt, setPrompt] = useState("");
29
  const [hasAsked, setHasAsked] = useState(false);
30
  const [previousPrompt, setPreviousPrompt] = useState("");
31
- // Removed unused diffBuffer state: const [diffBuffer, setDiffBuffer] = useState("");
32
  const audio = new Audio(SuccessSound);
33
  audio.volume = 0.5;
34
 
35
- // --- Diff Constants ---
36
- const SEARCH_START = "<<<<<<< SEARCH";
37
- const DIVIDER = "=======";
38
- const REPLACE_END = ">>>>>>> REPLACE";
39
-
40
- // --- Diff Applying Logic ---
41
-
42
- /**
43
- * Applies a single parsed diff block to the Monaco editor.
44
- */
45
- const applyMonacoDiff = (
46
- original: string,
47
- updated: string,
48
- editorInstance: editor.IStandaloneCodeEditor
49
- ) => {
50
- const model = editorInstance.getModel();
51
- if (!model) {
52
- console.error("Monaco model not available for applying diff.");
53
- toast.error("Editor model not found, cannot apply change.");
54
- return false; // Indicate failure
55
- }
56
-
57
- // Monaco's findMatches can be sensitive. Let's try a simple search first.
58
- // We need to be careful about potential regex characters in the original block.
59
- // Escape basic regex characters for the search string.
60
- const escapedOriginal = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61
-
62
- // Find the first occurrence. Might need more robust logic for multiple identical blocks.
63
- const matches = model.findMatches(
64
- escapedOriginal,
65
- false, // isRegex
66
- false, // matchCase
67
- false, // wordSeparators
68
- null, // searchScope
69
- true, // captureMatches
70
- 1 // limitResultCount
71
- );
72
-
73
- if (matches.length > 0) {
74
- const range = matches[0].range;
75
- const editOperation = {
76
- range: range,
77
- text: updated,
78
- forceMoveMarkers: true,
79
- };
80
-
81
- try {
82
- // Use pushEditOperations for better undo/redo integration if needed,
83
- // but executeEdits is simpler for direct replacement.
84
- editorInstance.executeEdits("ai-diff-apply", [editOperation]);
85
- // Scroll to the change
86
- editorInstance.revealRangeInCenter(range, editor.ScrollType.Smooth);
87
- console.log("[Diff Apply] Applied block:", { original, updated });
88
- return true; // Indicate success
89
- } catch (editError) {
90
- console.error("Error applying edit operation:", editError);
91
- toast.error(`Failed to apply change: ${editError}`);
92
- return false; // Indicate failure
93
- }
94
- } else {
95
- console.warn("Could not find SEARCH block in editor:", original);
96
- // Attempt fuzzy match (simple whitespace normalization) as fallback
97
- const normalizedOriginal = original.replace(/\s+/g, ' ').trim();
98
- const editorContent = model.getValue();
99
- const normalizedContent = editorContent.replace(/\s+/g, ' ').trim();
100
- const startIndex = normalizedContent.indexOf(normalizedOriginal);
101
-
102
- if (startIndex !== -1) {
103
- console.warn("Applying diff using fuzzy whitespace match.");
104
- // This is tricky - need to map normalized index back to original positions
105
- // For now, let's just log and skip applying this specific block
106
- toast.warn("Could not precisely locate change, skipping one diff block.");
107
- // TODO: Implement more robust fuzzy matching if needed
108
- } else {
109
- toast.error("Could not locate the code block to change. AI might be referencing outdated code.");
110
- }
111
- return false; // Indicate failure
112
- }
113
- };
114
-
115
- /**
116
- * Processes the accumulated diff buffer, parsing and applying complete blocks.
117
- */
118
- const processDiffBuffer = (
119
- currentBuffer: string,
120
- editorInstance: editor.IStandaloneCodeEditor | null
121
- ): string => {
122
- if (!editorInstance) return currentBuffer; // Don't process if editor isn't ready
123
-
124
- let remainingBuffer = currentBuffer;
125
- let appliedSuccess = true;
126
-
127
- // eslint-disable-next-line no-constant-condition
128
- while (true) {
129
- const searchStartIndex = remainingBuffer.indexOf(SEARCH_START);
130
- if (searchStartIndex === -1) break; // No more potential blocks
131
-
132
- const dividerIndex = remainingBuffer.indexOf(DIVIDER, searchStartIndex);
133
- if (dividerIndex === -1) break; // Incomplete block
134
-
135
- const replaceEndIndex = remainingBuffer.indexOf(REPLACE_END, dividerIndex);
136
- if (replaceEndIndex === -1) break; // Incomplete block
137
-
138
- // Extract the block content
139
- const originalBlockContent = remainingBuffer
140
- .substring(searchStartIndex + SEARCH_START.length, dividerIndex)
141
- .trimEnd(); // Trim potential trailing newline before divider
142
- const updatedBlockContent = remainingBuffer
143
- .substring(dividerIndex + DIVIDER.length, replaceEndIndex)
144
- .trimEnd(); // Trim potential trailing newline before end marker
145
-
146
- // Adjust for newlines potentially trimmed by .trimEnd() if they were intended
147
- const original = originalBlockContent.startsWith('\n') ? originalBlockContent.substring(1) : originalBlockContent;
148
- const updated = updatedBlockContent.startsWith('\n') ? updatedBlockContent.substring(1) : updatedBlockContent;
149
-
150
-
151
- console.log("[Diff Parse] Found block:", { original, updated });
152
-
153
- // Apply the diff
154
- appliedSuccess = applyMonacoDiff(original, updated, editorInstance) && appliedSuccess;
155
-
156
- // Remove the processed block from the buffer
157
- remainingBuffer = remainingBuffer.substring(replaceEndIndex + REPLACE_END.length);
158
- }
159
-
160
- if (!appliedSuccess) {
161
- // If any block failed, maybe stop processing further blocks in this stream?
162
- // Or just let it continue and report errors per block? Let's continue for now.
163
- console.warn("One or more diff blocks failed to apply.");
164
- }
165
-
166
- return remainingBuffer; // Return the part of the buffer that couldn't be processed yet
167
- };
168
-
169
-
170
- // --- Main AI Call Logic ---
171
  // --- Main AI Call Logic ---
172
  const callAi = async () => {
173
  if (isAiWorking || !prompt.trim()) return;
 
174
  setisAiWorking(true);
175
- // Removed setDiffBuffer("") call
176
 
177
  let fullContentResponse = ""; // Used for full HTML mode
 
178
  let lastRenderTime = 0; // For throttling full HTML updates
179
- let currentDiffBuffer = ""; // Local variable for buffer within this call
180
 
181
  try {
182
  const request = await fetch("/api/ask-ai", {
@@ -214,18 +77,41 @@ function AskAI({
214
  const { done, value } = await reader.read();
215
  if (done) {
216
  console.log("[AI Response] Stream finished.");
217
- // Process any remaining buffer content in diff mode
218
- if (responseType === 'diff' && currentDiffBuffer.trim()) {
219
- console.warn("[AI Response] Processing remaining diff buffer after stream end:", currentDiffBuffer);
220
- const finalRemaining = processDiffBuffer(currentDiffBuffer, editorRef.current);
221
- if (finalRemaining.trim()) {
222
- console.error("[AI Response] Stream ended with incomplete diff block:", finalRemaining);
223
- toast.error("AI response ended with an incomplete change block.");
224
- }
225
- }
226
- // Final update for full HTML mode
227
- if (responseType === 'full') {
228
- const finalDoc = fullContentResponse.match(/<!DOCTYPE html>[\s\S]*<\/html>/)?.[0];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  if (finalDoc) {
230
  setHtml(finalDoc); // Ensure final complete HTML is set
231
  } else if (fullContentResponse.trim()) {
 
1
+ import { useState } from "react";
2
  import { RiSparkling2Fill } from "react-icons/ri";
3
  import { GrSend } from "react-icons/gr";
4
  import classNames from "classnames";
5
  import { toast } from "react-toastify";
6
+ // Removed monaco editor import
7
 
8
  import Login from "../login/login";
9
  import { defaultHTML } from "../../utils/consts";
 
11
 
12
  function AskAI({
13
  html, // Current full HTML content (used for initial request and context)
14
+ setHtml, // Used for updates (both full and diff-based)
15
  onScrollToBottom, // Used for full updates
16
  isAiWorking,
17
  setisAiWorking,
 
18
  }: {
19
  html: string;
20
  setHtml: (html: string) => void;
21
  onScrollToBottom: () => void;
22
  isAiWorking: boolean;
23
  setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
24
+ // Removed editorRef prop
25
  }) {
26
  const [open, setOpen] = useState(false);
27
  const [prompt, setPrompt] = useState("");
28
  const [hasAsked, setHasAsked] = useState(false);
29
  const [previousPrompt, setPreviousPrompt] = useState("");
 
30
  const audio = new Audio(SuccessSound);
31
  audio.volume = 0.5;
32
 
33
+ // Removed client-side diff parsing/applying logic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  // --- Main AI Call Logic ---
35
  const callAi = async () => {
36
  if (isAiWorking || !prompt.trim()) return;
37
+ const originalHtml = html; // Store the HTML state at the start of the request
38
  setisAiWorking(true);
 
39
 
40
  let fullContentResponse = ""; // Used for full HTML mode
41
+ let accumulatedDiffResponse = ""; // Used for diff mode
42
  let lastRenderTime = 0; // For throttling full HTML updates
 
43
 
44
  try {
45
  const request = await fetch("/api/ask-ai", {
 
77
  const { done, value } = await reader.read();
78
  if (done) {
79
  console.log("[AI Response] Stream finished.");
80
+
81
+ // --- Post-stream processing ---
82
+ if (responseType === 'diff') {
83
+ // Apply diffs server-side
84
+ try {
85
+ console.log("[Diff Apply] Sending original HTML and AI diff response to server...");
86
+ const applyRequest = await fetch("/api/apply-diffs", {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({
90
+ originalHtml: originalHtml, // Send the HTML from the start of the request
91
+ aiResponseContent: accumulatedDiffResponse,
92
+ }),
93
+ });
94
+
95
+ if (!applyRequest.ok) {
96
+ const errorData = await applyRequest.json();
97
+ throw new Error(errorData.message || `Server failed to apply diffs (status ${applyRequest.status})`);
98
+ }
99
+
100
+ const patchedHtml = await applyRequest.text();
101
+ console.log("[Diff Apply] Received patched HTML from server.");
102
+ setHtml(patchedHtml); // Update editor with the final result
103
+ toast.success("AI changes applied");
104
+
105
+ } catch (applyError: any) {
106
+ console.error("Error applying diffs server-side:", applyError);
107
+ toast.error(`Failed to apply AI changes: ${applyError.message}`);
108
+ // Optionally revert to originalHtml? Or leave the editor as is?
109
+ // setHtml(originalHtml); // Uncomment to revert on failure
110
+ }
111
+
112
+ } else {
113
+ // Final update for full HTML mode
114
+ const finalDoc = fullContentResponse.match(/<!DOCTYPE html>[\s\S]*<\/html>/)?.[0];
115
  if (finalDoc) {
116
  setHtml(finalDoc); // Ensure final complete HTML is set
117
  } else if (fullContentResponse.trim()) {