diff --git a/backend/ai/src/index.ts b/backend/ai/src/index.ts index 7d6d7e2..f20687b 100644 --- a/backend/ai/src/index.ts +++ b/backend/ai/src/index.ts @@ -6,69 +6,61 @@ export interface Env { export default { async fetch(request: Request, env: Env): Promise { + // 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") { 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"); + const context = url.searchParams.get("context"); - const prompt = ` -Make the following changes to the code below: -- ${instructions} + if (!instructions) { + return new Response("Missing instructions parameter", { status: 400 }); + } -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 prompt = `You are an intelligent programming assistant. Please respond to the following request: +${instructions} + +${context ? `Context:\n${context}\n` : ''} + +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.`; + + 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({ - model: "claude-3-5-sonnet-20240620", + model: "claude-3-opus-20240229", max_tokens: 1024, messages: [{ role: "user", content: prompt }], }); - const message = response.content as ContentBlock[]; - const textBlockContent = getTextContent(message); + const assistantResponse = response.content[0].type === 'text' ? response.content[0].text : ''; - const pattern = /```[a-zA-Z]*\n([\s\S]*?)\n```/; - const match = textBlockContent.match(pattern); - - const codeContent = match ? match[1] : "Error: Could not extract code."; - - return new Response(JSON.stringify({ "response": codeContent })) + // When sending the response, include CORS headers + return new Response(JSON.stringify({ "response": assistantResponse }), { + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + }); } catch (error) { console.error("Error:", error); return new Response("Internal Server Error", { status: 500 }); diff --git a/frontend/components/editor/AIChat.tsx b/frontend/components/editor/AIChat.tsx index 6fe7ed3..d89ef7e 100644 --- a/frontend/components/editor/AIChat.tsx +++ b/frontend/components/editor/AIChat.tsx @@ -1,62 +1,334 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Button } from '../ui/button'; -import { Send } from 'lucide-react'; +import { Send, StopCircle, Copy, Check, ChevronDown, ChevronUp, X, CornerUpLeft, Loader2 } 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 LoadingDots from '../ui/LoadingDots'; interface Message { role: 'user' | 'assistant'; content: string; + context?: string; } export default function AIChat() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); + const [isGenerating, setIsGenerating] = useState(false); const chatContainerRef = useRef(null); + const abortControllerRef = useRef(null); + const [copiedIndex, setCopiedIndex] = useState(null); + const [context, setContext] = useState(null); + const [isContextExpanded, setIsContextExpanded] = useState(false); + const [expandedMessageIndex, setExpandedMessageIndex] = useState(null); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (chatContainerRef.current) { - chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; - } + scrollToBottom(); }, [messages]); - const handleSend = async () => { - if (input.trim() === '') return; + const scrollToBottom = () => { + if (chatContainerRef.current) { + setTimeout(() => { + chatContainerRef.current?.scrollTo({ + top: chatContainerRef.current.scrollHeight, + behavior: 'smooth' + }); + }, 100); + } + }; - const newMessage: Message = { role: 'user', content: input }; + const handleSend = async () => { + if (input.trim() === '' && !context) return; + + const newMessage: Message = { + role: 'user', + content: input, + context: context || undefined + }; setMessages(prev => [...prev, newMessage]); setInput(''); + setIsContextExpanded(false); + setIsGenerating(true); + setIsLoading(true); // Set loading state to true - // TODO: Implement actual API call to LLM here - // For now, we'll just simulate a response - setTimeout(() => { - const assistantMessage: Message = { role: 'assistant', content: 'This is a simulated response from the AI.' }; + abortControllerRef.current = new AbortController(); + + try { + const queryParams = new URLSearchParams({ + instructions: input, + ...(context && { context }) // Include context only if it exists + }); + const response = await fetch(`http://127.0.0.1:8787/api?${queryParams}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + signal: abortControllerRef.current.signal, + }); + + if (!response.ok) { + throw new Error('Failed to get AI response'); + } + + const data = await response.json(); + const assistantMessage: Message = { role: 'assistant', content: '' }; setMessages(prev => [...prev, assistantMessage]); - }, 1000); + setIsLoading(false); // Set loading state to false once we start receiving the response + + // Simulate text generation + for (let i = 0; i <= data.response.length; i++) { + if (abortControllerRef.current.signal.aborted) { + break; + } + setMessages(prev => { + const updatedMessages = [...prev]; + updatedMessages[updatedMessages.length - 1].content = data.response.slice(0, i); + return updatedMessages; + }); + await new Promise(resolve => setTimeout(resolve, 20)); + } + } catch (error: any) { + if (error.name === 'AbortError') { + console.log('Generation aborted'); + } else { + console.error('Error fetching AI response:', error); + const errorMessage: Message = { role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' }; + setMessages(prev => [...prev, errorMessage]); + } + } finally { + setIsGenerating(false); + setIsLoading(false); // Ensure loading state is set to false + abortControllerRef.current = null; + } + }; + + const handleStopGeneration = () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + + const copyToClipboard = (text: string, index: number) => { + navigator.clipboard.writeText(text).then(() => { + setCopiedIndex(index); + setTimeout(() => setCopiedIndex(null), 1000); // Reset after 1 seconds + }); + }; + + const askAboutCode = (code: string) => { + setContext(`Regarding this code:\n${code}`); + setIsContextExpanded(false); + }; + + const removeContext = () => { + setContext(null); + setIsContextExpanded(false); }; return ( -
+
+ CHAT
- {messages.map((message, index) => ( -
-
- {message.content} + {messages.map((message, messageIndex) => ( +
+
+ {message.context && ( +
+
setExpandedMessageIndex(expandedMessageIndex === messageIndex ? null : messageIndex)} + > + + Context + + {expandedMessageIndex === messageIndex ? ( + + ) : ( + + )} +
+ {expandedMessageIndex === messageIndex && ( +
+
+ +
+ {(() => { + // need to fix the language detection + const code = message.context!.replace(/^Regarding this code:\n/, ''); + const match = /language-(\w+)/.exec(code); + const language = match ? match[1] : 'typescript'; + return ( +
+ + {code} + +
+ ); + })()} +
+ )} +
+ )} + {message.role === 'assistant' ? ( + +
+ {language} +
+
+ + +
+
+ + {String(children).replace(/\n$/, '')} + +
+
+ ) : ( + + {children} + + ); + }, + p({children}) { + return

{children}

; + }, + ul({children}) { + return
    {children}
; + }, + ol({children}) { + return
    {children}
; + }, + li({children}) { + return
  • {children}
  • ; + }, + }} + > + {message.content} + + ) : ( +
    {message.content}
    + )}
    ))} + {isLoading && ( + + )}
    -
    -
    - + {context && ( +
    +
    +
    setIsContextExpanded(!isContextExpanded)} + > + + Context + +
    +
    + {isContextExpanded ? ( + setIsContextExpanded(false)} /> + ) : ( + setIsContextExpanded(true)} /> + )} + +
    +
    + {isContextExpanded && ( +