diff --git a/frontend/app/api/merge/route.ts b/frontend/app/api/merge/route.ts
new file mode 100644
index 0000000..c17eab9
--- /dev/null
+++ b/frontend/app/api/merge/route.ts
@@ -0,0 +1,67 @@
+import OpenAI from "openai"
+
+const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+})
+
+export async function POST(request: Request) {
+ try {
+ const { originalCode, newCode, fileName } = await request.json()
+
+ const systemPrompt = `You are a code merging assistant. Your task is to merge the new code snippet with the original file content while:
+1. Preserving the original file's functionality
+2. Ensuring proper integration of the new code
+3. Maintaining consistent style and formatting
+4. Resolving any potential conflicts
+5. Output ONLY the raw code without any:
+ - Code fence markers (\`\`\`)
+ - Language identifiers (typescript, javascript, etc.)
+ - Explanations or comments
+ - Markdown formatting
+
+The output should be the exact code that will replace the existing code, nothing more and nothing less.`
+
+ const mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}`
+
+ const response = await openai.chat.completions.create({
+ model: "gpt-4o",
+ messages: [
+ { role: "system", content: systemPrompt },
+ { role: "user", content: mergedCode },
+ ],
+ prediction: {
+ type: "content",
+ content: mergedCode,
+ },
+ stream: true,
+ })
+
+ // Clean and stream response
+ const encoder = new TextEncoder()
+ return new Response(
+ new ReadableStream({
+ async start(controller) {
+ let buffer = ""
+ for await (const chunk of response) {
+ if (chunk.choices[0]?.delta?.content) {
+ buffer += chunk.choices[0].delta.content
+ // Clean any code fence markers that might appear in the stream
+ const cleanedContent = buffer
+ .replace(/^```[\w-]*\n|```\s*$/gm, "") // Remove code fences
+ .replace(/^(javascript|typescript|python|html|css)\n/gm, "") // Remove language identifiers
+ controller.enqueue(encoder.encode(cleanedContent))
+ buffer = ""
+ }
+ }
+ controller.close()
+ },
+ })
+ )
+ } catch (error) {
+ console.error("Merge error:", error)
+ return new Response(
+ error instanceof Error ? error.message : "Failed to merge code",
+ { status: 500 }
+ )
+ }
+}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 15f1ccf..643614b 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -175,3 +175,23 @@
.tab-scroll::-webkit-scrollbar {
display: none;
}
+
+.added-line-decoration {
+ background-color: rgba(0, 255, 0, 0.1);
+}
+
+.removed-line-decoration {
+ background-color: rgba(255, 0, 0, 0.1);
+}
+
+.added-line-glyph {
+ background-color: #28a745;
+ width: 4px !important;
+ margin-left: 3px;
+}
+
+.removed-line-glyph {
+ background-color: #dc3545;
+ width: 4px !important;
+ margin-left: 3px;
+}
diff --git a/frontend/components/editor/AIChat/ApplyButton.tsx b/frontend/components/editor/AIChat/ApplyButton.tsx
new file mode 100644
index 0000000..f122339
--- /dev/null
+++ b/frontend/components/editor/AIChat/ApplyButton.tsx
@@ -0,0 +1,77 @@
+import { Check, Loader2 } from "lucide-react"
+import { useState } from "react"
+import { toast } from "sonner"
+import { Button } from "../../ui/button"
+
+interface ApplyButtonProps {
+ code: string
+ activeFileName: string
+ activeFileContent: string
+ editorRef: { current: any }
+ onApply: (mergedCode: string) => void
+}
+
+export default function ApplyButton({
+ code,
+ activeFileName,
+ activeFileContent,
+ editorRef,
+ onApply,
+}: ApplyButtonProps) {
+ const [isApplying, setIsApplying] = useState(false)
+
+ const handleApply = async () => {
+ setIsApplying(true)
+ try {
+ const response = await fetch("/api/merge", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ originalCode: activeFileContent,
+ newCode: String(code),
+ fileName: activeFileName,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(await response.text())
+ }
+
+ const reader = response.body?.getReader()
+ const decoder = new TextDecoder()
+ let mergedCode = ""
+
+ if (reader) {
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+ mergedCode += decoder.decode(value, { stream: true })
+ }
+ }
+ onApply(mergedCode.trim())
+ } catch (error) {
+ console.error("Error applying code:", error)
+ toast.error(
+ error instanceof Error ? error.message : "Failed to apply code changes"
+ )
+ } finally {
+ setIsApplying(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/components/editor/AIChat/ChatMessage.tsx b/frontend/components/editor/AIChat/ChatMessage.tsx
index 59050bf..caded21 100644
--- a/frontend/components/editor/AIChat/ChatMessage.tsx
+++ b/frontend/components/editor/AIChat/ChatMessage.tsx
@@ -13,6 +13,12 @@ export default function ChatMessage({
setContext,
setIsContextExpanded,
socket,
+ handleApplyCode,
+ activeFileName,
+ activeFileContent,
+ editorRef,
+ mergeDecorationsCollection,
+ setMergeDecorationsCollection,
}: MessageProps) {
// State for expanded message index
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
@@ -104,7 +110,13 @@ export default function ChatMessage({
const components = createMarkdownComponents(
renderCopyButton,
renderMarkdownElement,
- askAboutCode
+ askAboutCode,
+ activeFileName,
+ activeFileContent,
+ editorRef,
+ handleApplyCode,
+ mergeDecorationsCollection,
+ setMergeDecorationsCollection
)
return (
diff --git a/frontend/components/editor/AIChat/index.tsx b/frontend/components/editor/AIChat/index.tsx
index dcaa780..d9d57d4 100644
--- a/frontend/components/editor/AIChat/index.tsx
+++ b/frontend/components/editor/AIChat/index.tsx
@@ -1,6 +1,6 @@
import { useSocket } from "@/context/SocketContext"
import { TFile } from "@/lib/types"
-import { X, ChevronDown } from "lucide-react"
+import { ChevronDown, X } from "lucide-react"
import { nanoid } from "nanoid"
import { useEffect, useRef, useState } from "react"
import LoadingDots from "../../ui/LoadingDots"
@@ -18,6 +18,9 @@ export default function AIChat({
lastCopiedRangeRef,
files,
templateType,
+ handleApplyCode,
+ mergeDecorationsCollection,
+ setMergeDecorationsCollection,
}: AIChatProps) {
// Initialize socket and messages
const { socket } = useSocket()
@@ -176,7 +179,7 @@ export default function AIChat({
const fileExt = tab.name.split(".").pop() || "txt"
return {
...tab,
- content: `\`\`\`${fileExt}\n${activeFileContent}\n\`\`\``
+ content: `\`\`\`${fileExt}\n${activeFileContent}\n\`\`\``,
}
}
return tab
@@ -214,6 +217,12 @@ export default function AIChat({
setContext={setContext}
setIsContextExpanded={setIsContextExpanded}
socket={socket}
+ handleApplyCode={handleApplyCode}
+ activeFileName={activeFileName}
+ activeFileContent={activeFileContent}
+ editorRef={editorRef}
+ mergeDecorationsCollection={mergeDecorationsCollection}
+ setMergeDecorationsCollection={setMergeDecorationsCollection}
/>
))}
{isLoading &&