diff --git a/backend/ai/src/index.ts b/backend/ai/src/index.ts index 7d6d7e2..ff4635f 100644 --- a/backend/ai/src/index.ts +++ b/backend/ai/src/index.ts @@ -1,4 +1,5 @@ import { Anthropic } from "@anthropic-ai/sdk"; +import { MessageParam } from "@anthropic-ai/sdk/src/resources/messages.js"; export interface Env { ANTHROPIC_API_KEY: string; @@ -6,69 +7,112 @@ export interface Env { export default { async fetch(request: Request, env: Env): Promise { - if (request.method !== "GET") { + // Handle CORS preflight requests + if (request.method === "OPTIONS") { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); + } + + if (request.method !== "GET" && request.method !== "POST") { return new Response("Method Not Allowed", { status: 405 }); } - const url = new URL(request.url); - // const fileName = url.searchParams.get("fileName"); - // const line = url.searchParams.get("line"); - const instructions = url.searchParams.get("instructions"); - const code = url.searchParams.get("code"); + let body; + let isEditCodeWidget = false; + if (request.method === "POST") { + body = await request.json() as { messages: unknown; context: unknown; activeFileContent: string }; + } else { + const url = new URL(request.url); + const fileName = url.searchParams.get("fileName") || ""; + const code = url.searchParams.get("code") || ""; + const line = url.searchParams.get("line") || ""; + const instructions = url.searchParams.get("instructions") || ""; - const prompt = ` -Make the following changes to the code below: -- ${instructions} + body = { + messages: [{ role: "human", content: instructions }], + context: `File: ${fileName}\nLine: ${line}\nCode:\n${code}`, + activeFileContent: code, + }; + isEditCodeWidget = true; + } -Return the complete code chunk. Do not refer to other code files. Do not add code before or after the chunk. Start your reponse with \`\`\`, and end with \`\`\`. Do not include any other text. + const messages = body.messages; + const context = body.context; + const activeFileContent = body.activeFileContent; + if (!Array.isArray(messages) || messages.length === 0) { + return new Response("Invalid or empty messages", { status: 400 }); + } + + let systemMessage; + if (isEditCodeWidget) { + systemMessage = `You are an AI code editor. Your task is to modify the given code based on the user's instructions. Only output the modified code, without any explanations or markdown formatting. The code should be a direct replacement for the existing code. + +Context: +${context} + +Active File Content: +${activeFileContent} + +Instructions: ${messages[0].content} + +Respond only with the modified code that can directly replace the existing code.`; + } else { + systemMessage = `You are an intelligent programming assistant. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example: + +\`\`\`python +print("Hello, World!") \`\`\` -${code} -\`\`\` -`; -console.log(prompt); - try { +Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point. + +${context ? `Context:\n${context}\n` : ''} +${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ''}`; + } + + const anthropicMessages = messages.map(msg => ({ + role: msg.role === 'human' ? 'user' : 'assistant', + content: msg.content + })) as MessageParam[]; + + try { const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); - interface TextBlock { - type: "text"; - text: string; - } - - interface ToolUseBlock { - type: "tool_use"; - tool_use: { - // Add properties if needed - }; - } - - type ContentBlock = TextBlock | ToolUseBlock; - - function getTextContent(content: ContentBlock[]): string { - for (const block of content) { - if (block.type === "text") { - return block.text; - } - } - return "No text content found"; - } - - const response = await anthropic.messages.create({ + const stream = await anthropic.messages.create({ model: "claude-3-5-sonnet-20240620", max_tokens: 1024, - messages: [{ role: "user", content: prompt }], + system: systemMessage, + messages: anthropicMessages, + stream: true, }); - const message = response.content as ContentBlock[]; - const textBlockContent = getTextContent(message); + const encoder = new TextEncoder(); - const pattern = /```[a-zA-Z]*\n([\s\S]*?)\n```/; - const match = textBlockContent.match(pattern); + const streamResponse = new ReadableStream({ + async start(controller) { + for await (const chunk of stream) { + if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') { + const bytes = encoder.encode(chunk.delta.text); + controller.enqueue(bytes); + } + } + controller.close(); + }, + }); - const codeContent = match ? match[1] : "Error: Could not extract code."; - - return new Response(JSON.stringify({ "response": codeContent })) + return new Response(streamResponse, { + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + }, + }); } catch (error) { console.error("Error:", error); return new Response("Internal Server Error", { status: 500 }); diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index fb8bec1..5c0ba89 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -815,12 +815,28 @@ io.on("connection", async (socket) => { generateCodePromise, ]); - const json = await generateCodeResponse.json(); + if (!generateCodeResponse.ok) { + throw new Error(`HTTP error! status: ${generateCodeResponse.status}`); + } - callback({ response: json.response, success: true }); + const reader = generateCodeResponse.body?.getReader(); + const decoder = new TextDecoder(); + let result = ''; + + if (reader) { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + result += decoder.decode(value, { stream: true }); + } + } + + // The result should now contain only the modified code + callback({ response: result.trim(), success: true }); } catch (e: any) { console.error("Error generating code:", e); io.emit("error", `Error: code generation. ${e.message ?? e}`); + callback({ response: "Error generating code. Please try again.", success: false }); } } ); diff --git a/frontend/components/editor/AIChat/ChatInput.tsx b/frontend/components/editor/AIChat/ChatInput.tsx new file mode 100644 index 0000000..a40e0b5 --- /dev/null +++ b/frontend/components/editor/AIChat/ChatInput.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Button } from '../../ui/button'; +import { Send, StopCircle } from 'lucide-react'; + +interface ChatInputProps { + input: string; + setInput: (input: string) => void; + isGenerating: boolean; + handleSend: () => void; + handleStopGeneration: () => void; +} + +export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) { + return ( +
+ setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()} + className="flex-grow p-2 border rounded-lg min-w-0 bg-input" + placeholder="Type your message..." + disabled={isGenerating} + /> + {isGenerating ? ( + + ) : ( + + )} +
+ ); +} diff --git a/frontend/components/editor/AIChat/ChatMessage.tsx b/frontend/components/editor/AIChat/ChatMessage.tsx new file mode 100644 index 0000000..7eac365 --- /dev/null +++ b/frontend/components/editor/AIChat/ChatMessage.tsx @@ -0,0 +1,201 @@ +import React, { useState } from 'react'; +import { Button } from '../../ui/button'; +import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import remarkGfm from 'remark-gfm'; +import { copyToClipboard, stringifyContent } from './lib/chatUtils'; + +interface MessageProps { + message: { + role: 'user' | 'assistant'; + content: string; + context?: string; + }; + setContext: (context: string | null) => void; + setIsContextExpanded: (isExpanded: boolean) => void; +} + +export default function ChatMessage({ message, setContext, setIsContextExpanded }: MessageProps) { + const [expandedMessageIndex, setExpandedMessageIndex] = useState(null); + const [copiedText, setCopiedText] = useState(null); + + const renderCopyButton = (text: any) => ( + + ); + + const askAboutCode = (code: any) => { + const contextString = stringifyContent(code); + setContext(`Regarding this code:\n${contextString}`); + setIsContextExpanded(false); + }; + + const renderMarkdownElement = (props: any) => { + const { node, children } = props; + const content = stringifyContent(children); + + return ( +
+
+ {renderCopyButton(content)} + +
+ {React.createElement(node.tagName, { + ...props, + className: `${props.className || ''} hover:bg-transparent rounded p-1 transition-colors` + }, children)} +
+ ); + }; + + return ( +
+
+ {message.role === 'user' && ( +
+ {renderCopyButton(message.content)} + +
+ )} + {message.context && ( +
+
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)} + > + + Context + + {expandedMessageIndex === 0 ? ( + + ) : ( + + )} +
+ {expandedMessageIndex === 0 && ( +
+
+ {renderCopyButton(message.context.replace(/^Regarding this code:\n/, ''))} +
+ {(() => { + const code = message.context.replace(/^Regarding this code:\n/, ''); + const match = /language-(\w+)/.exec(code); + const language = match ? match[1] : 'typescript'; + return ( +
+