From 6612692d98ac5b335a0cf9c94805517b85eea862 Mon Sep 17 00:00:00 2001 From: Akhileshrangani4 Date: Sat, 30 Nov 2024 20:56:56 -0500 Subject: [PATCH] feat: introduce apply button functionality (v0.1) ### Summary - Added a new "Apply" button to code snippets provided by the AI assistant. - The button is designed to seamlessly merge the AI-generated snippet into the relevant file in the editor. ### Current Issues 1. **Sticky Accept/Decline Buttons:** These activate for every snippet instead of being limited to the relevant snippet. 2. **Discard Button:** Currently non-functional. 3. **Highlight Inconsistencies:** The green-red code highlights for old and new code are inconsistent. ### To Do - Implement a toast notification when the "Apply" button is pressed on an irrelevant tab to prevent code application errors. ### Workflow Implemented 1. The "Apply" button is added alongside "Copy" and "Reply" for AI-generated code snippets. 2. Upon clicking "Apply," the code snippet and relevant file content (active file) are sent to a secondary model (GPT-4O). 3. The system prompt for GPT-4O instructs it to merge the snippet with the file content: - Ensure the original file functionality remains intact. - Integrate the code snippet seamlessly. 4. The output from GPT-4O is injected directly into the code editor. 5. Changes are visually highlighted: - Green for new code. - Red for removed code. 6. Highlights remain until the user explicitly accepts or discards the changes. --- frontend/app/api/merge/route.ts | 67 ++++++++++++++++ frontend/app/globals.css | 20 +++++ .../components/editor/AIChat/ApplyButton.tsx | 77 +++++++++++++++++++ .../components/editor/AIChat/ChatMessage.tsx | 14 +++- frontend/components/editor/AIChat/index.tsx | 13 +++- .../editor/AIChat/lib/markdownComponents.tsx | 63 ++++++++++++++- .../components/editor/AIChat/types/index.ts | 9 +++ frontend/components/editor/index.tsx | 53 +++++++++++++ frontend/package-lock.json | 60 +++++++++++++++ frontend/package.json | 1 + 10 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 frontend/app/api/merge/route.ts create mode 100644 frontend/components/editor/AIChat/ApplyButton.tsx 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 && } diff --git a/frontend/components/editor/AIChat/lib/markdownComponents.tsx b/frontend/components/editor/AIChat/lib/markdownComponents.tsx index e6e1418..a0c5554 100644 --- a/frontend/components/editor/AIChat/lib/markdownComponents.tsx +++ b/frontend/components/editor/AIChat/lib/markdownComponents.tsx @@ -1,15 +1,23 @@ -import { CornerUpLeft } from "lucide-react" +import { Check, CornerUpLeft, X } from "lucide-react" +import monaco from "monaco-editor" import { Components } from "react-markdown" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" import { Button } from "../../../ui/button" +import ApplyButton from "../ApplyButton" import { stringifyContent } from "./chatUtils" // Create markdown components for chat message component export const createMarkdownComponents = ( renderCopyButton: (text: any) => JSX.Element, renderMarkdownElement: (props: any) => JSX.Element, - askAboutCode: (code: any) => void + askAboutCode: (code: any) => void, + activeFileName: string, + activeFileContent: string, + editorRef: any, + handleApplyCode: (mergedCode: string) => void, + mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection, + setMergeDecorationsCollection?: (collection: undefined) => void ): Components => ({ code: ({ node, @@ -33,6 +41,57 @@ export const createMarkdownComponents = (
{renderCopyButton(children)}
+ {!mergeDecorationsCollection ? ( + + ) : ( + <> + +
+ + + )} +