Compare commits
9 Commits
main
...
ai-codegen
Author | SHA1 | Date | |
---|---|---|---|
|
a9c5db92ff | ||
|
2c9f130a37 | ||
|
fac1404e14 | ||
|
2317cf49e9 | ||
|
24332794f1 | ||
|
a8b8a25e4c | ||
|
88058ca710 | ||
|
7f6e2bf62d | ||
|
b48b08a274 |
@ -128,7 +128,7 @@ export class FileManager {
|
|||||||
// Copy all files from the project to the container
|
// Copy all files from the project to the container
|
||||||
const promises = this.fileData.map(async (file) => {
|
const promises = this.fileData.map(async (file) => {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(this.dirName, file.id)
|
const filePath = path.posix.join(this.dirName, file.id)
|
||||||
const parentDirectory = path.dirname(filePath)
|
const parentDirectory = path.dirname(filePath)
|
||||||
if (!this.sandbox.files.exists(parentDirectory)) {
|
if (!this.sandbox.files.exists(parentDirectory)) {
|
||||||
await this.sandbox.files.makeDir(parentDirectory)
|
await this.sandbox.files.makeDir(parentDirectory)
|
||||||
|
@ -95,7 +95,7 @@ export default function Dashboard({
|
|||||||
</Button> */}
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<a target="_blank" href="https://github.com/ishaan1013/sandbox">
|
<a target="_blank" href="https://github.com/jamesmurdza/sandbox">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="justify-start w-full font-normal text-muted-foreground"
|
className="justify-start w-full font-normal text-muted-foreground"
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { Send, StopCircle } from "lucide-react"
|
import { Send, StopCircle, Image as ImageIcon, Paperclip } from "lucide-react"
|
||||||
import { Button } from "../../ui/button"
|
import { Button } from "../../ui/button"
|
||||||
|
import { useEffect } from "react"
|
||||||
interface ChatInputProps {
|
import { TFile, TFolder } from "@/lib/types"
|
||||||
input: string
|
import { ALLOWED_FILE_TYPES } from "./types"
|
||||||
setInput: (input: string) => void
|
import { looksLikeCode } from "./lib/chatUtils"
|
||||||
isGenerating: boolean
|
import { ChatInputProps } from "./types"
|
||||||
handleSend: () => void
|
|
||||||
handleStopGeneration: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChatInput({
|
export default function ChatInput({
|
||||||
input,
|
input,
|
||||||
@ -15,37 +12,228 @@ export default function ChatInput({
|
|||||||
isGenerating,
|
isGenerating,
|
||||||
handleSend,
|
handleSend,
|
||||||
handleStopGeneration,
|
handleStopGeneration,
|
||||||
|
onImageUpload,
|
||||||
|
addContextTab,
|
||||||
|
activeFileName,
|
||||||
|
editorRef,
|
||||||
|
lastCopiedRangeRef,
|
||||||
|
contextTabs,
|
||||||
|
onRemoveTab,
|
||||||
|
textareaRef,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
|
|
||||||
|
// Auto-resize textarea as content changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = 'auto'
|
||||||
|
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'
|
||||||
|
}
|
||||||
|
}, [input])
|
||||||
|
|
||||||
|
// Handle keyboard events for sending messages
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSend(true) // Send with full context
|
||||||
|
} else if (!e.shiftKey && !isGenerating) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSend(false)
|
||||||
|
}
|
||||||
|
} else if (e.key === "Backspace" && input === "" && contextTabs.length > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
// Remove the last context tab
|
||||||
|
const lastTab = contextTabs[contextTabs.length - 1]
|
||||||
|
onRemoveTab(lastTab.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle paste events for image and code
|
||||||
|
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||||
|
// Handle image paste
|
||||||
|
const items = Array.from(e.clipboardData.items);
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type.startsWith('image/')) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (!file) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert image to base64 string for context tab title and timestamp
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64String = reader.result as string;
|
||||||
|
addContextTab(
|
||||||
|
"image",
|
||||||
|
`Image ${new Date().toLocaleTimeString('en-US', {
|
||||||
|
hour12: true,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).replace(/(\d{2}):(\d{2})/, '$1:$2')}`,
|
||||||
|
base64String
|
||||||
|
);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing pasted image:', error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get text from clipboard
|
||||||
|
const text = e.clipboardData.getData('text');
|
||||||
|
|
||||||
|
// If text doesn't contain newlines or doesn't look like code, let it paste normally
|
||||||
|
if (!text || !text.includes('\n') || !looksLikeCode(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
const editor = editorRef.current;
|
||||||
|
const currentSelection = editor?.getSelection();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
// TODO: FIX THIS: even when i paste the outside code, it shows the active file name,it works when no tabs are open, just does not work when the tab is open
|
||||||
|
|
||||||
|
// If selection exists in editor, use file name and line numbers
|
||||||
|
if (currentSelection && !currentSelection.isEmpty()) {
|
||||||
|
addContextTab(
|
||||||
|
"code",
|
||||||
|
`${activeFileName} (${currentSelection.startLineNumber}-${currentSelection.endLineNumber})`,
|
||||||
|
text,
|
||||||
|
{ start: currentSelection.startLineNumber, end: currentSelection.endLineNumber }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have stored line range from a copy operation in the editor
|
||||||
|
if (lastCopiedRangeRef.current) {
|
||||||
|
const range = lastCopiedRangeRef.current;
|
||||||
|
addContextTab(
|
||||||
|
"code",
|
||||||
|
`${activeFileName} (${range.startLine}-${range.endLine})`,
|
||||||
|
text,
|
||||||
|
{ start: range.startLine, end: range.endLine }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For code pasted from outside the editor
|
||||||
|
addContextTab(
|
||||||
|
"code",
|
||||||
|
`Pasted Code (1-${lines.length})`,
|
||||||
|
text,
|
||||||
|
{ start: 1, end: lines.length }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image upload from local machine via input
|
||||||
|
const handleImageUpload = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = 'image/*'
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
|
if (file) onImageUpload(file)
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to flatten the file tree
|
||||||
|
const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => {
|
||||||
|
return items.reduce((acc: TFile[], item) => {
|
||||||
|
if (item.type === "file") {
|
||||||
|
acc.push(item)
|
||||||
|
} else {
|
||||||
|
acc.push(...getAllFiles(item.children))
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle file upload from local machine via input
|
||||||
|
const handleFileUpload = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = '.txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf'
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
|
if (file) {
|
||||||
|
if (!(file.type in ALLOWED_FILE_TYPES)) {
|
||||||
|
alert('Unsupported file type. Please upload text, code, or PDF files.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
addContextTab("file", file.name, reader.result as string)
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2 min-w-0">
|
<div className="space-y-2">
|
||||||
<input
|
<div className="flex space-x-2 min-w-0">
|
||||||
type="text"
|
<textarea
|
||||||
value={input}
|
ref={textareaRef}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
value={input}
|
||||||
onKeyPress={(e) => e.key === "Enter" && !isGenerating && handleSend()}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Type your message..."
|
onPaste={handlePaste}
|
||||||
disabled={isGenerating}
|
className="flex-grow p-2 border rounded-lg min-w-0 bg-input resize-none overflow-hidden"
|
||||||
/>
|
placeholder="Type your message..."
|
||||||
{isGenerating ? (
|
|
||||||
<Button
|
|
||||||
onClick={handleStopGeneration}
|
|
||||||
variant="destructive"
|
|
||||||
size="icon"
|
|
||||||
className="h-10 w-10"
|
|
||||||
>
|
|
||||||
<StopCircle className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={handleSend}
|
|
||||||
disabled={isGenerating}
|
disabled={isGenerating}
|
||||||
size="icon"
|
rows={1}
|
||||||
className="h-10 w-10"
|
/>
|
||||||
|
{/* Render stop generation button */}
|
||||||
|
{isGenerating ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleStopGeneration}
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
>
|
||||||
|
<StopCircle className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleSend(false)}
|
||||||
|
disabled={isGenerating}
|
||||||
|
size="icon"
|
||||||
|
className="h-10 w-10"
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
{/* Render file upload button */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-2 sm:px-3"
|
||||||
|
onClick={handleFileUpload}
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4" />
|
<Paperclip className="h-3 w-3 sm:mr-1" />
|
||||||
|
<span className="hidden sm:inline">File</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
{/* Render image upload button */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-2 sm:px-3"
|
||||||
|
onClick={handleImageUpload}
|
||||||
|
>
|
||||||
|
<ImageIcon className="h-3 w-3 sm:mr-1" />
|
||||||
|
<span className="hidden sm:inline">Image</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,32 +1,29 @@
|
|||||||
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
|
import { Check, Copy, CornerUpLeft } from "lucide-react"
|
||||||
import React, { useState } from "react"
|
import React, { useState } from "react"
|
||||||
import ReactMarkdown from "react-markdown"
|
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 remarkGfm from "remark-gfm"
|
||||||
import { Button } from "../../ui/button"
|
import { Button } from "../../ui/button"
|
||||||
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
||||||
|
import ContextTabs from "./ContextTabs"
|
||||||
interface MessageProps {
|
import { createMarkdownComponents } from './lib/markdownComponents'
|
||||||
message: {
|
import { MessageProps } from "./types"
|
||||||
role: "user" | "assistant"
|
|
||||||
content: string
|
|
||||||
context?: string
|
|
||||||
}
|
|
||||||
setContext: (context: string | null) => void
|
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ChatMessage({
|
export default function ChatMessage({
|
||||||
message,
|
message,
|
||||||
setContext,
|
setContext,
|
||||||
setIsContextExpanded,
|
setIsContextExpanded,
|
||||||
|
socket,
|
||||||
}: MessageProps) {
|
}: MessageProps) {
|
||||||
|
|
||||||
|
// State for expanded message index
|
||||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
|
// State for copied text
|
||||||
const [copiedText, setCopiedText] = useState<string | null>(null)
|
const [copiedText, setCopiedText] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Render copy button for text content
|
||||||
const renderCopyButton = (text: any) => (
|
const renderCopyButton = (text: any) => (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
|
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
|
||||||
@ -42,12 +39,36 @@ export default function ChatMessage({
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set context for code when asking about code
|
||||||
const askAboutCode = (code: any) => {
|
const askAboutCode = (code: any) => {
|
||||||
const contextString = stringifyContent(code)
|
const contextString = stringifyContent(code)
|
||||||
setContext(`Regarding this code:\n${contextString}`)
|
const newContext = `Regarding this code:\n${contextString}`
|
||||||
|
|
||||||
|
// Format timestamp to match chat message format (HH:MM PM)
|
||||||
|
const timestamp = new Date().toLocaleTimeString('en-US', {
|
||||||
|
hour12: true,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Instead of replacing context, append to it
|
||||||
|
if (message.role === "assistant") {
|
||||||
|
// For assistant messages, create a new context tab with the response content and timestamp
|
||||||
|
setContext(newContext, `AI Response (${timestamp})`, {
|
||||||
|
start: 1,
|
||||||
|
end: contextString.split('\n').length
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// For user messages, create a new context tab with the selected content and timestamp
|
||||||
|
setContext(newContext, `User Chat (${timestamp})`, {
|
||||||
|
start: 1,
|
||||||
|
end: contextString.split('\n').length
|
||||||
|
})
|
||||||
|
}
|
||||||
setIsContextExpanded(false)
|
setIsContextExpanded(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render markdown elements for code and text
|
||||||
const renderMarkdownElement = (props: any) => {
|
const renderMarkdownElement = (props: any) => {
|
||||||
const { node, children } = props
|
const { node, children } = props
|
||||||
const content = stringifyContent(children)
|
const content = stringifyContent(children)
|
||||||
@ -65,6 +86,7 @@ export default function ChatMessage({
|
|||||||
<CornerUpLeft className="w-4 h-4" />
|
<CornerUpLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Render markdown element */}
|
||||||
{React.createElement(
|
{React.createElement(
|
||||||
node.tagName,
|
node.tagName,
|
||||||
{
|
{
|
||||||
@ -79,6 +101,13 @@ export default function ChatMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create markdown components
|
||||||
|
const components = createMarkdownComponents(
|
||||||
|
renderCopyButton,
|
||||||
|
renderMarkdownElement,
|
||||||
|
askAboutCode
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-left relative">
|
<div className="text-left relative">
|
||||||
<div
|
<div
|
||||||
@ -88,34 +117,19 @@ export default function ChatMessage({
|
|||||||
: "bg-transparent text-white"
|
: "bg-transparent text-white"
|
||||||
} max-w-full`}
|
} max-w-full`}
|
||||||
>
|
>
|
||||||
{message.role === "user" && (
|
{/* Render context tabs */}
|
||||||
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
{message.role === "user" && message.context && (
|
||||||
{renderCopyButton(message.content)}
|
|
||||||
<Button
|
|
||||||
onClick={() => askAboutCode(message.content)}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="p-1 h-6"
|
|
||||||
>
|
|
||||||
<CornerUpLeft className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{message.context && (
|
|
||||||
<div className="mb-2 bg-input rounded-lg">
|
<div className="mb-2 bg-input rounded-lg">
|
||||||
<div
|
<ContextTabs
|
||||||
className="flex justify-between items-center cursor-pointer"
|
socket={socket}
|
||||||
onClick={() =>
|
activeFileName=""
|
||||||
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
|
onAddFile={() => {}}
|
||||||
}
|
contextTabs={parseContextToTabs(message.context)}
|
||||||
>
|
onRemoveTab={() => {}}
|
||||||
<span className="text-sm text-gray-300">Context</span>
|
isExpanded={expandedMessageIndex === 0}
|
||||||
{expandedMessageIndex === 0 ? (
|
onToggleExpand={() => setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)}
|
||||||
<ChevronUp size={16} />
|
className="[&_div:first-child>div:first-child>div]:bg-[#0D0D0D] [&_button:first-child]:hidden [&_button:last-child]:hidden"
|
||||||
) : (
|
/>
|
||||||
<ChevronDown size={16} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{expandedMessageIndex === 0 && (
|
{expandedMessageIndex === 0 && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute top-0 right-0 flex p-1">
|
<div className="absolute top-0 right-0 flex p-1">
|
||||||
@ -123,6 +137,7 @@ export default function ChatMessage({
|
|||||||
message.context.replace(/^Regarding this code:\n/, "")
|
message.context.replace(/^Regarding this code:\n/, "")
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Render code textarea */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const code = message.context.replace(
|
const code = message.context.replace(
|
||||||
/^Regarding this code:\n/,
|
/^Regarding this code:\n/,
|
||||||
@ -136,7 +151,10 @@ export default function ChatMessage({
|
|||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const updatedContext = `Regarding this code:\n${e.target.value}`
|
const updatedContext = `Regarding this code:\n${e.target.value}`
|
||||||
setContext(updatedContext)
|
setContext(updatedContext, "Selected Content", {
|
||||||
|
start: 1,
|
||||||
|
end: e.target.value.split('\n').length
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||||
rows={code.split("\n").length}
|
rows={code.split("\n").length}
|
||||||
@ -153,67 +171,25 @@ export default function ChatMessage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Render copy and ask about code buttons */}
|
||||||
|
{message.role === "user" && (
|
||||||
|
<div className="absolute top-0 right-0 p-1 flex opacity-40">
|
||||||
|
{renderCopyButton(message.content)}
|
||||||
|
<Button
|
||||||
|
onClick={() => askAboutCode(message.content)}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="p-1 h-6"
|
||||||
|
>
|
||||||
|
<CornerUpLeft className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Render markdown content */}
|
||||||
{message.role === "assistant" ? (
|
{message.role === "assistant" ? (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={components}
|
||||||
code({ node, className, children, ...props }) {
|
|
||||||
const match = /language-(\w+)/.exec(className || "")
|
|
||||||
return match ? (
|
|
||||||
<div className="relative border border-input rounded-md my-4">
|
|
||||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
|
||||||
{match[1]}
|
|
||||||
</div>
|
|
||||||
<div className="absolute top-0 right-0 flex">
|
|
||||||
{renderCopyButton(children)}
|
|
||||||
<Button
|
|
||||||
onClick={() => askAboutCode(children)}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="p-1 h-6"
|
|
||||||
>
|
|
||||||
<CornerUpLeft className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="pt-6">
|
|
||||||
<SyntaxHighlighter
|
|
||||||
style={vscDarkPlus as any}
|
|
||||||
language={match[1]}
|
|
||||||
PreTag="div"
|
|
||||||
customStyle={{
|
|
||||||
margin: 0,
|
|
||||||
padding: "0.5rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{stringifyContent(children)}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<code className={className} {...props}>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
p: renderMarkdownElement,
|
|
||||||
h1: renderMarkdownElement,
|
|
||||||
h2: renderMarkdownElement,
|
|
||||||
h3: renderMarkdownElement,
|
|
||||||
h4: renderMarkdownElement,
|
|
||||||
h5: renderMarkdownElement,
|
|
||||||
h6: renderMarkdownElement,
|
|
||||||
ul: (props) => (
|
|
||||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
|
||||||
{props.children}
|
|
||||||
</ul>
|
|
||||||
),
|
|
||||||
ol: (props) => (
|
|
||||||
<ol className="list-decimal pl-6 mb-4 space-y-2">
|
|
||||||
{props.children}
|
|
||||||
</ol>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{message.content}
|
{message.content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
@ -224,3 +200,27 @@ export default function ChatMessage({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse context to tabs for context tabs component
|
||||||
|
function parseContextToTabs(context: string) {
|
||||||
|
const sections = context.split(/(?=File |Code from )/)
|
||||||
|
return sections.map((section, index) => {
|
||||||
|
const lines = section.trim().split('\n')
|
||||||
|
const titleLine = lines[0]
|
||||||
|
let content = lines.slice(1).join('\n').trim()
|
||||||
|
|
||||||
|
// Remove code block markers for display
|
||||||
|
content = content.replace(/^```[\w-]*\n/, '').replace(/\n```$/, '')
|
||||||
|
|
||||||
|
// Determine if the context is a file or code
|
||||||
|
const isFile = titleLine.startsWith('File ')
|
||||||
|
const name = titleLine.replace(/^(File |Code from )/, '').replace(':', '')
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `context-${index}`,
|
||||||
|
type: isFile ? "file" as const : "code" as const,
|
||||||
|
name: name,
|
||||||
|
content: content
|
||||||
|
}
|
||||||
|
}).filter(tab => tab.content.length > 0)
|
||||||
|
}
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
import { ChevronDown, ChevronUp, X } from "lucide-react"
|
|
||||||
|
|
||||||
interface ContextDisplayProps {
|
|
||||||
context: string | null
|
|
||||||
isContextExpanded: boolean
|
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void
|
|
||||||
setContext: (context: string | null) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ContextDisplay({
|
|
||||||
context,
|
|
||||||
isContextExpanded,
|
|
||||||
setIsContextExpanded,
|
|
||||||
setContext,
|
|
||||||
}: ContextDisplayProps) {
|
|
||||||
if (!context) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-2 bg-input p-2 rounded-lg">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div
|
|
||||||
className="flex-grow cursor-pointer"
|
|
||||||
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
|
||||||
>
|
|
||||||
<span className="text-sm text-gray-300">Context</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{isContextExpanded ? (
|
|
||||||
<ChevronUp
|
|
||||||
size={16}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => setIsContextExpanded(false)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => setIsContextExpanded(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<X
|
|
||||||
size={16}
|
|
||||||
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
|
|
||||||
onClick={() => setContext(null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isContextExpanded && (
|
|
||||||
<textarea
|
|
||||||
value={context.replace(/^Regarding this code:\n/, "")}
|
|
||||||
onChange={(e) =>
|
|
||||||
setContext(`Regarding this code:\n${e.target.value}`)
|
|
||||||
}
|
|
||||||
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
|
||||||
rows={5}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
172
frontend/components/editor/AIChat/ContextTabs.tsx
Normal file
172
frontend/components/editor/AIChat/ContextTabs.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { Plus, X, Image as ImageIcon, FileText } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { TFile, TFolder } from "@/lib/types"
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { ContextTab } from "./types"
|
||||||
|
import { ContextTabsProps } from "./types"
|
||||||
|
// Ignore certain folders and files from the file tree
|
||||||
|
import { ignoredFiles, ignoredFolders } from "./lib/ignored-paths"
|
||||||
|
|
||||||
|
export default function ContextTabs({
|
||||||
|
contextTabs,
|
||||||
|
onRemoveTab,
|
||||||
|
className,
|
||||||
|
files = [],
|
||||||
|
onFileSelect,
|
||||||
|
}: ContextTabsProps & { className?: string }) {
|
||||||
|
|
||||||
|
// State for preview tab
|
||||||
|
const [previewTab, setPreviewTab] = useState<ContextTab | null>(null)
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
|
||||||
|
// Allow preview for images and code selections from editor
|
||||||
|
const togglePreview = (tab: ContextTab) => {
|
||||||
|
if (!tab.lineRange && tab.type !== "image") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle preview for images and code selections from editor
|
||||||
|
if (previewTab?.id === tab.id) {
|
||||||
|
setPreviewTab(null)
|
||||||
|
} else {
|
||||||
|
setPreviewTab(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tab from context when clicking on X
|
||||||
|
const handleRemoveTab = (id: string) => {
|
||||||
|
if (previewTab?.id === id) {
|
||||||
|
setPreviewTab(null)
|
||||||
|
}
|
||||||
|
onRemoveTab(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all files from the file tree to search for context
|
||||||
|
const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => {
|
||||||
|
return items.reduce((acc: TFile[], item) => {
|
||||||
|
// Add file if it's not ignored
|
||||||
|
if (item.type === "file" && !ignoredFiles.some((pattern: string) =>
|
||||||
|
item.name.endsWith(pattern.replace('*', '')) || item.name === pattern
|
||||||
|
)) {
|
||||||
|
acc.push(item)
|
||||||
|
// Add all files from folder if it's not ignored
|
||||||
|
} else if (item.type === "folder" && !ignoredFolders.some((folder: string) => folder === item.name)) {
|
||||||
|
acc.push(...getAllFiles(item.children))
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all files from the file tree to search for context when adding context
|
||||||
|
const allFiles = getAllFiles(files)
|
||||||
|
const filteredFiles = allFiles.filter(file =>
|
||||||
|
file.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`border-none ${className || ''}`}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex items-center gap-1 overflow-hidden mb-2 flex-wrap">
|
||||||
|
{/* Add context tab button */}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{/* Add context tab popover */}
|
||||||
|
<PopoverContent className="w-64 p-2">
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Search files..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[200px] overflow-y-auto">
|
||||||
|
{filteredFiles.map((file) => (
|
||||||
|
<Button
|
||||||
|
key={file.id}
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start text-sm mb-1"
|
||||||
|
onClick={() => onFileSelect?.(file)}
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4 mr-2" />
|
||||||
|
{file.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{/* Add context tab button */}
|
||||||
|
{contextTabs.length === 0 && (
|
||||||
|
<div className="flex items-center gap-1 px-2 rounded">
|
||||||
|
<span className="text-sm text-muted-foreground">Add Context</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Render context tabs */}
|
||||||
|
{contextTabs.map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
className="flex items-center gap-1 px-2 bg-input rounded text-sm cursor-pointer hover:bg-muted"
|
||||||
|
onClick={() => togglePreview(tab)}
|
||||||
|
>
|
||||||
|
{tab.type === "image" && <ImageIcon className="h-3 w-3" />}
|
||||||
|
<span>{tab.name}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-4 w-4"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleRemoveTab(tab.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview Section */}
|
||||||
|
{previewTab && (
|
||||||
|
<div className="p-2 bg-input rounded-md max-h-[200px] overflow-auto mb-2">
|
||||||
|
{previewTab.type === "image" ? (
|
||||||
|
<img
|
||||||
|
src={previewTab.content}
|
||||||
|
alt={previewTab.name}
|
||||||
|
className="max-w-full h-auto"
|
||||||
|
/>
|
||||||
|
) : previewTab.lineRange && (
|
||||||
|
<>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
Lines {previewTab.lineRange.start}-{previewTab.lineRange.end}
|
||||||
|
</div>
|
||||||
|
<pre className="text-xs font-mono whitespace-pre-wrap">
|
||||||
|
{previewTab.content}
|
||||||
|
</pre>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Render file context tab */}
|
||||||
|
{previewTab.type === "file" && (
|
||||||
|
<pre className="text-xs font-mono whitespace-pre-wrap">
|
||||||
|
{previewTab.content}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -3,37 +3,47 @@ import { useEffect, useRef, useState } from "react"
|
|||||||
import LoadingDots from "../../ui/LoadingDots"
|
import LoadingDots from "../../ui/LoadingDots"
|
||||||
import ChatInput from "./ChatInput"
|
import ChatInput from "./ChatInput"
|
||||||
import ChatMessage from "./ChatMessage"
|
import ChatMessage from "./ChatMessage"
|
||||||
import ContextDisplay from "./ContextDisplay"
|
import ContextTabs from "./ContextTabs"
|
||||||
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
interface Message {
|
import { TFile } from "@/lib/types"
|
||||||
role: "user" | "assistant"
|
import { useSocket } from "@/context/SocketContext"
|
||||||
content: string
|
import { Message, ContextTab, AIChatProps } from './types'
|
||||||
context?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AIChat({
|
export default function AIChat({
|
||||||
activeFileContent,
|
activeFileContent,
|
||||||
activeFileName,
|
activeFileName,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
editorRef,
|
||||||
activeFileContent: string
|
lastCopiedRangeRef,
|
||||||
activeFileName: string
|
files,
|
||||||
onClose: () => void
|
}: AIChatProps) {
|
||||||
}) {
|
// Initialize socket and messages
|
||||||
|
const { socket } = useSocket()
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
|
|
||||||
|
// Initialize input and state for generating messages
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
const [isGenerating, setIsGenerating] = useState(false)
|
const [isGenerating, setIsGenerating] = useState(false)
|
||||||
|
|
||||||
|
// Initialize chat container ref and abort controller ref
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const abortControllerRef = useRef<AbortController | null>(null)
|
const abortControllerRef = useRef<AbortController | null>(null)
|
||||||
const [context, setContext] = useState<string | null>(null)
|
|
||||||
|
// Initialize context tabs and state for expanding context
|
||||||
|
const [contextTabs, setContextTabs] = useState<ContextTab[]>([])
|
||||||
const [isContextExpanded, setIsContextExpanded] = useState(false)
|
const [isContextExpanded, setIsContextExpanded] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
// Initialize textarea ref
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
|
// Scroll to bottom of chat when messages change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
// Scroll to bottom of chat when messages change
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (chatContainerRef.current) {
|
if (chatContainerRef.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -45,6 +55,84 @@ export default function AIChat({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add context tab to context tabs
|
||||||
|
const addContextTab = (type: string, name: string, content: string, lineRange?: { start: number; end: number }) => {
|
||||||
|
const newTab = {
|
||||||
|
id: nanoid(),
|
||||||
|
type: type as "file" | "code" | "image",
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
lineRange
|
||||||
|
}
|
||||||
|
setContextTabs(prev => [...prev, newTab])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove context tab from context tabs
|
||||||
|
const removeContextTab = (id: string) => {
|
||||||
|
setContextTabs(prev => prev.filter(tab => tab.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file to context tabs
|
||||||
|
const handleAddFile = (tab: ContextTab) => {
|
||||||
|
setContextTabs(prev => [...prev, tab])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format code content to remove starting and ending code block markers if they exist
|
||||||
|
const formatCodeContent = (content: string) => {
|
||||||
|
return content.replace(/^```[\w-]*\n/, '').replace(/\n```$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get combined context from context tabs
|
||||||
|
const getCombinedContext = () => {
|
||||||
|
if (contextTabs.length === 0) return ''
|
||||||
|
|
||||||
|
return contextTabs.map(tab => {
|
||||||
|
if (tab.type === 'file') {
|
||||||
|
const fileExt = tab.name.split('.').pop() || 'txt'
|
||||||
|
const cleanContent = formatCodeContent(tab.content)
|
||||||
|
return `File ${tab.name}:\n\`\`\`${fileExt}\n${cleanContent}\n\`\`\``
|
||||||
|
} else if (tab.type === 'code') {
|
||||||
|
const cleanContent = formatCodeContent(tab.content)
|
||||||
|
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
|
||||||
|
}
|
||||||
|
return `${tab.name}:\n${tab.content}`
|
||||||
|
}).join('\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sending message with context
|
||||||
|
const handleSendWithContext = () => {
|
||||||
|
const combinedContext = getCombinedContext()
|
||||||
|
handleSend(
|
||||||
|
input,
|
||||||
|
combinedContext,
|
||||||
|
messages,
|
||||||
|
setMessages,
|
||||||
|
setInput,
|
||||||
|
setIsContextExpanded,
|
||||||
|
setIsGenerating,
|
||||||
|
setIsLoading,
|
||||||
|
abortControllerRef,
|
||||||
|
activeFileContent
|
||||||
|
)
|
||||||
|
// Clear context tabs after sending
|
||||||
|
setContextTabs([])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set context for the chat
|
||||||
|
const setContext = (
|
||||||
|
context: string | null,
|
||||||
|
name: string,
|
||||||
|
range?: { start: number, end: number }
|
||||||
|
) => {
|
||||||
|
if (!context) {
|
||||||
|
setContextTabs([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always add a new tab instead of updating existing ones
|
||||||
|
addContextTab('code', name, context, range)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen w-full">
|
<div className="flex flex-col h-screen w-full">
|
||||||
<div className="flex justify-between items-center p-2 border-b">
|
<div className="flex justify-between items-center p-2 border-b">
|
||||||
@ -68,41 +156,65 @@ export default function AIChat({
|
|||||||
className="flex-grow overflow-y-auto p-4 space-y-4"
|
className="flex-grow overflow-y-auto p-4 space-y-4"
|
||||||
>
|
>
|
||||||
{messages.map((message, messageIndex) => (
|
{messages.map((message, messageIndex) => (
|
||||||
|
// Render chat message component for each message
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={messageIndex}
|
key={messageIndex}
|
||||||
message={message}
|
message={message}
|
||||||
setContext={setContext}
|
setContext={setContext}
|
||||||
setIsContextExpanded={setIsContextExpanded}
|
setIsContextExpanded={setIsContextExpanded}
|
||||||
|
socket={socket}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isLoading && <LoadingDots />}
|
{isLoading && <LoadingDots />}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-t mb-14">
|
<div className="p-4 border-t mb-14">
|
||||||
<ContextDisplay
|
{/* Render context tabs component */}
|
||||||
context={context}
|
<ContextTabs
|
||||||
isContextExpanded={isContextExpanded}
|
activeFileName={activeFileName}
|
||||||
setIsContextExpanded={setIsContextExpanded}
|
onAddFile={handleAddFile}
|
||||||
setContext={setContext}
|
contextTabs={contextTabs}
|
||||||
|
onRemoveTab={removeContextTab}
|
||||||
|
isExpanded={isContextExpanded}
|
||||||
|
onToggleExpand={() => setIsContextExpanded(!isContextExpanded)}
|
||||||
|
files={files}
|
||||||
|
socket={socket}
|
||||||
|
onFileSelect={(file: TFile) => {
|
||||||
|
socket?.emit("getFile", { fileId: file.id }, (response: string) => {
|
||||||
|
const fileExt = file.name.split('.').pop() || 'txt'
|
||||||
|
const formattedContent = `\`\`\`${fileExt}\n${response}\n\`\`\``
|
||||||
|
addContextTab('file', file.name, formattedContent)
|
||||||
|
if (textareaRef.current) {
|
||||||
|
textareaRef.current.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* Render chat input component */}
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
textareaRef={textareaRef}
|
||||||
|
addContextTab={addContextTab}
|
||||||
|
editorRef={editorRef}
|
||||||
input={input}
|
input={input}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
isGenerating={isGenerating}
|
isGenerating={isGenerating}
|
||||||
handleSend={() =>
|
handleSend={handleSendWithContext}
|
||||||
handleSend(
|
|
||||||
input,
|
|
||||||
context,
|
|
||||||
messages,
|
|
||||||
setMessages,
|
|
||||||
setInput,
|
|
||||||
setIsContextExpanded,
|
|
||||||
setIsGenerating,
|
|
||||||
setIsLoading,
|
|
||||||
abortControllerRef,
|
|
||||||
activeFileContent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
|
||||||
|
onImageUpload={(file) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
if (e.target?.result) {
|
||||||
|
addContextTab("image", file.name, e.target.result as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}}
|
||||||
|
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||||
|
activeFileName={activeFileName}
|
||||||
|
contextTabs={contextTabs.map(tab => ({
|
||||||
|
...tab,
|
||||||
|
title: tab.id
|
||||||
|
}))}
|
||||||
|
onRemoveTab={removeContextTab}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,30 +1,39 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
|
// Stringify content for chat message component
|
||||||
export const stringifyContent = (
|
export const stringifyContent = (
|
||||||
content: any,
|
content: any,
|
||||||
seen = new WeakSet()
|
seen = new WeakSet()
|
||||||
): string => {
|
): string => {
|
||||||
|
// Stringify content if it's a string
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's null
|
||||||
if (content === null) {
|
if (content === null) {
|
||||||
return "null"
|
return "null"
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's undefined
|
||||||
if (content === undefined) {
|
if (content === undefined) {
|
||||||
return "undefined"
|
return "undefined"
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's a number or boolean
|
||||||
if (typeof content === "number" || typeof content === "boolean") {
|
if (typeof content === "number" || typeof content === "boolean") {
|
||||||
return content.toString()
|
return content.toString()
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's a function
|
||||||
if (typeof content === "function") {
|
if (typeof content === "function") {
|
||||||
return content.toString()
|
return content.toString()
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's a symbol
|
||||||
if (typeof content === "symbol") {
|
if (typeof content === "symbol") {
|
||||||
return content.toString()
|
return content.toString()
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's a bigint
|
||||||
if (typeof content === "bigint") {
|
if (typeof content === "bigint") {
|
||||||
return content.toString() + "n"
|
return content.toString() + "n"
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's a valid React element
|
||||||
if (React.isValidElement(content)) {
|
if (React.isValidElement(content)) {
|
||||||
return React.Children.toArray(
|
return React.Children.toArray(
|
||||||
(content as React.ReactElement).props.children
|
(content as React.ReactElement).props.children
|
||||||
@ -32,11 +41,13 @@ export const stringifyContent = (
|
|||||||
.map((child) => stringifyContent(child, seen))
|
.map((child) => stringifyContent(child, seen))
|
||||||
.join("")
|
.join("")
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's an array
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return (
|
return (
|
||||||
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
|
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's an object
|
||||||
if (typeof content === "object") {
|
if (typeof content === "object") {
|
||||||
if (seen.has(content)) {
|
if (seen.has(content)) {
|
||||||
return "[Circular]"
|
return "[Circular]"
|
||||||
@ -51,19 +62,23 @@ export const stringifyContent = (
|
|||||||
return Object.prototype.toString.call(content)
|
return Object.prototype.toString.call(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Stringify content if it's a primitive value
|
||||||
return String(content)
|
return String(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy to clipboard for chat message component
|
||||||
export const copyToClipboard = (
|
export const copyToClipboard = (
|
||||||
text: string,
|
text: string,
|
||||||
setCopiedText: (text: string | null) => void
|
setCopiedText: (text: string | null) => void
|
||||||
) => {
|
) => {
|
||||||
|
// Copy text to clipboard for chat message component
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setCopiedText(text)
|
setCopiedText(text)
|
||||||
setTimeout(() => setCopiedText(null), 2000)
|
setTimeout(() => setCopiedText(null), 2000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle send for chat message component
|
||||||
export const handleSend = async (
|
export const handleSend = async (
|
||||||
input: string,
|
input: string,
|
||||||
context: string | null,
|
context: string | null,
|
||||||
@ -76,14 +91,26 @@ export const handleSend = async (
|
|||||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||||
activeFileContent: string
|
activeFileContent: string
|
||||||
) => {
|
) => {
|
||||||
|
// Return if input is empty and context is null
|
||||||
if (input.trim() === "" && !context) return
|
if (input.trim() === "" && !context) return
|
||||||
|
|
||||||
const newMessage = {
|
// Get timestamp for chat message component
|
||||||
|
const timestamp = new Date().toLocaleTimeString('en-US', {
|
||||||
|
hour12: true,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).replace(/(\d{2}):(\d{2})/, '$1:$2')
|
||||||
|
|
||||||
|
// Create user message for chat message component
|
||||||
|
const userMessage = {
|
||||||
role: "user" as const,
|
role: "user" as const,
|
||||||
content: input,
|
content: input,
|
||||||
context: context || undefined,
|
context: context || undefined,
|
||||||
|
timestamp: timestamp
|
||||||
}
|
}
|
||||||
const updatedMessages = [...messages, newMessage]
|
|
||||||
|
// Update messages for chat message component
|
||||||
|
const updatedMessages = [...messages, userMessage]
|
||||||
setMessages(updatedMessages)
|
setMessages(updatedMessages)
|
||||||
setInput("")
|
setInput("")
|
||||||
setIsContextExpanded(false)
|
setIsContextExpanded(false)
|
||||||
@ -93,11 +120,13 @@ export const handleSend = async (
|
|||||||
abortControllerRef.current = new AbortController()
|
abortControllerRef.current = new AbortController()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Create anthropic messages for chat message component
|
||||||
const anthropicMessages = updatedMessages.map((msg) => ({
|
const anthropicMessages = updatedMessages.map((msg) => ({
|
||||||
role: msg.role === "user" ? "human" : "assistant",
|
role: msg.role === "user" ? "human" : "assistant",
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Fetch AI response for chat message component
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
|
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
|
||||||
{
|
{
|
||||||
@ -114,20 +143,24 @@ export const handleSend = async (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Throw error if response is not ok
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to get AI response")
|
throw new Error("Failed to get AI response")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get reader for chat message component
|
||||||
const reader = response.body?.getReader()
|
const reader = response.body?.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
const assistantMessage = { role: "assistant" as const, content: "" }
|
const assistantMessage = { role: "assistant" as const, content: "" }
|
||||||
setMessages([...updatedMessages, assistantMessage])
|
setMessages([...updatedMessages, assistantMessage])
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
||||||
|
// Initialize buffer for chat message component
|
||||||
let buffer = ""
|
let buffer = ""
|
||||||
const updateInterval = 100
|
const updateInterval = 100
|
||||||
let lastUpdateTime = Date.now()
|
let lastUpdateTime = Date.now()
|
||||||
|
|
||||||
|
// Read response from reader for chat message component
|
||||||
if (reader) {
|
if (reader) {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
@ -146,6 +179,7 @@ export const handleSend = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update messages for chat message component
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const updatedMessages = [...prev]
|
const updatedMessages = [...prev]
|
||||||
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||||
@ -154,6 +188,7 @@ export const handleSend = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
// Handle abort error for chat message component
|
||||||
if (error.name === "AbortError") {
|
if (error.name === "AbortError") {
|
||||||
console.log("Generation aborted")
|
console.log("Generation aborted")
|
||||||
} else {
|
} else {
|
||||||
@ -171,6 +206,7 @@ export const handleSend = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle stop generation for chat message component
|
||||||
export const handleStopGeneration = (
|
export const handleStopGeneration = (
|
||||||
abortControllerRef: React.MutableRefObject<AbortController | null>
|
abortControllerRef: React.MutableRefObject<AbortController | null>
|
||||||
) => {
|
) => {
|
||||||
@ -178,3 +214,22 @@ export const handleStopGeneration = (
|
|||||||
abortControllerRef.current.abort()
|
abortControllerRef.current.abort()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if text looks like code for chat message component
|
||||||
|
export const looksLikeCode = (text: string): boolean => {
|
||||||
|
const codeIndicators = [
|
||||||
|
/^import\s+/m, // import statements
|
||||||
|
/^function\s+/m, // function declarations
|
||||||
|
/^class\s+/m, // class declarations
|
||||||
|
/^const\s+/m, // const declarations
|
||||||
|
/^let\s+/m, // let declarations
|
||||||
|
/^var\s+/m, // var declarations
|
||||||
|
/[{}\[\]();]/, // common code syntax
|
||||||
|
/^\s*\/\//m, // comments
|
||||||
|
/^\s*\/\*/m, // multi-line comments
|
||||||
|
/=>/, // arrow functions
|
||||||
|
/^export\s+/m, // export statements
|
||||||
|
];
|
||||||
|
|
||||||
|
return codeIndicators.some(pattern => pattern.test(text));
|
||||||
|
};
|
||||||
|
102
frontend/components/editor/AIChat/lib/ignored-paths.ts
Normal file
102
frontend/components/editor/AIChat/lib/ignored-paths.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Ignore certain folders and files from the file tree
|
||||||
|
|
||||||
|
export const ignoredFolders = [
|
||||||
|
// Package managers
|
||||||
|
'node_modules',
|
||||||
|
'venv',
|
||||||
|
'.env',
|
||||||
|
'env',
|
||||||
|
'.venv',
|
||||||
|
'virtualenv',
|
||||||
|
'pip-wheel-metadata',
|
||||||
|
|
||||||
|
// Build outputs
|
||||||
|
'.next',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'out',
|
||||||
|
'__pycache__',
|
||||||
|
'.webpack',
|
||||||
|
'.serverless',
|
||||||
|
'storybook-static',
|
||||||
|
|
||||||
|
// Version control
|
||||||
|
'.git',
|
||||||
|
'.svn',
|
||||||
|
'.hg', // Mercurial
|
||||||
|
|
||||||
|
// Cache and temp files
|
||||||
|
'.cache',
|
||||||
|
'coverage',
|
||||||
|
'tmp',
|
||||||
|
'.temp',
|
||||||
|
'.npm',
|
||||||
|
'.pnpm',
|
||||||
|
'.yarn',
|
||||||
|
'.eslintcache',
|
||||||
|
'.stylelintcache',
|
||||||
|
|
||||||
|
// IDE specific
|
||||||
|
'.idea',
|
||||||
|
'.vscode',
|
||||||
|
'.vs',
|
||||||
|
'.sublime',
|
||||||
|
|
||||||
|
// Framework specific
|
||||||
|
'.streamlit',
|
||||||
|
'.next',
|
||||||
|
'static',
|
||||||
|
'.pytest_cache',
|
||||||
|
'.nuxt',
|
||||||
|
'.docusaurus',
|
||||||
|
'.remix',
|
||||||
|
'.parcel-cache',
|
||||||
|
'public/build', // Remix/Rails
|
||||||
|
'.turbo', // Turborepo
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
'logs',
|
||||||
|
'*.log',
|
||||||
|
'npm-debug.log*',
|
||||||
|
'yarn-debug.log*',
|
||||||
|
'yarn-error.log*',
|
||||||
|
'pnpm-debug.log*',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ignoredFiles = [
|
||||||
|
'.DS_Store',
|
||||||
|
'.env.local',
|
||||||
|
'.env.development',
|
||||||
|
'.env.production',
|
||||||
|
'.env.test',
|
||||||
|
'.env*.local',
|
||||||
|
'.gitignore',
|
||||||
|
'.npmrc',
|
||||||
|
'.yarnrc',
|
||||||
|
'.editorconfig',
|
||||||
|
'.prettierrc',
|
||||||
|
'.eslintrc',
|
||||||
|
'.browserslistrc',
|
||||||
|
'tsconfig.tsbuildinfo',
|
||||||
|
'*.pyc',
|
||||||
|
'*.pyo',
|
||||||
|
'*.pyd',
|
||||||
|
'*.so',
|
||||||
|
'*.dll',
|
||||||
|
'*.dylib',
|
||||||
|
'*.class',
|
||||||
|
'*.exe',
|
||||||
|
'package-lock.json',
|
||||||
|
'yarn.lock',
|
||||||
|
'pnpm-lock.yaml',
|
||||||
|
'composer.lock',
|
||||||
|
'poetry.lock',
|
||||||
|
'Gemfile.lock',
|
||||||
|
'*.min.js',
|
||||||
|
'*.min.css',
|
||||||
|
'*.map',
|
||||||
|
'*.chunk.*',
|
||||||
|
'*.hot-update.*',
|
||||||
|
'.vercel',
|
||||||
|
'.netlify'
|
||||||
|
] as const;
|
79
frontend/components/editor/AIChat/lib/markdownComponents.tsx
Normal file
79
frontend/components/editor/AIChat/lib/markdownComponents.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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 { CornerUpLeft } from "lucide-react"
|
||||||
|
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
|
||||||
|
): Components => ({
|
||||||
|
code: ({ node, className, children, ...props }: {
|
||||||
|
node?: import('hast').Element,
|
||||||
|
className?: string,
|
||||||
|
children?: React.ReactNode,
|
||||||
|
[key: string]: any,
|
||||||
|
}) => {
|
||||||
|
const match = /language-(\w+)/.exec(className || "")
|
||||||
|
|
||||||
|
return match ? (
|
||||||
|
<div className="relative border border-input rounded-md my-4">
|
||||||
|
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||||
|
{match[1]}
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-0 right-0 flex">
|
||||||
|
{renderCopyButton(children)}
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
askAboutCode(children)
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="p-1 h-6"
|
||||||
|
>
|
||||||
|
<CornerUpLeft className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="pt-6">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
style={vscDarkPlus as any}
|
||||||
|
language={match[1]}
|
||||||
|
PreTag="div"
|
||||||
|
customStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: "0.5rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stringifyContent(children)}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<code className={className} {...props}>{children}</code>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// Render markdown elements
|
||||||
|
p: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||||
|
h1: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||||
|
h2: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||||
|
h3: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||||
|
h4: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||||
|
h5: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||||
|
h6: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||||
|
ul: (props) => (
|
||||||
|
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||||
|
{props.children}
|
||||||
|
</ul>
|
||||||
|
),
|
||||||
|
ol: (props) => (
|
||||||
|
<ol className="list-decimal pl-6 mb-4 space-y-2">
|
||||||
|
{props.children}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
|
})
|
93
frontend/components/editor/AIChat/types/index.ts
Normal file
93
frontend/components/editor/AIChat/types/index.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import * as monaco from 'monaco-editor'
|
||||||
|
import { TFile, TFolder } from "@/lib/types"
|
||||||
|
import { Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
// Allowed file types for context tabs
|
||||||
|
export const ALLOWED_FILE_TYPES = {
|
||||||
|
// Text files
|
||||||
|
'text/plain': true,
|
||||||
|
'text/markdown': true,
|
||||||
|
'text/csv': true,
|
||||||
|
// Code files
|
||||||
|
'application/json': true,
|
||||||
|
'text/javascript': true,
|
||||||
|
'text/typescript': true,
|
||||||
|
'text/html': true,
|
||||||
|
'text/css': true,
|
||||||
|
// Documents
|
||||||
|
'application/pdf': true,
|
||||||
|
// Images
|
||||||
|
'image/jpeg': true,
|
||||||
|
'image/png': true,
|
||||||
|
'image/gif': true,
|
||||||
|
'image/webp': true,
|
||||||
|
'image/svg+xml': true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Message interface
|
||||||
|
export interface Message {
|
||||||
|
role: "user" | "assistant"
|
||||||
|
content: string
|
||||||
|
context?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context tab interface
|
||||||
|
export interface ContextTab {
|
||||||
|
id: string
|
||||||
|
type: "file" | "code" | "image"
|
||||||
|
name: string
|
||||||
|
content: string
|
||||||
|
lineRange?: { start: number; end: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
// AIChat props interface
|
||||||
|
export interface AIChatProps {
|
||||||
|
activeFileContent: string
|
||||||
|
activeFileName: string
|
||||||
|
onClose: () => void
|
||||||
|
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||||
|
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||||
|
files: (TFile | TFolder)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat input props interface
|
||||||
|
export interface ChatInputProps {
|
||||||
|
input: string
|
||||||
|
setInput: (input: string) => void
|
||||||
|
isGenerating: boolean
|
||||||
|
handleSend: (useFullContext?: boolean) => void
|
||||||
|
handleStopGeneration: () => void
|
||||||
|
onImageUpload: (file: File) => void
|
||||||
|
addContextTab: (type: string, title: string, content: string, lineRange?: { start: number, end: number }) => void
|
||||||
|
activeFileName?: string
|
||||||
|
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||||
|
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||||
|
contextTabs: { id: string; type: string; title: string; content: string; lineRange?: { start: number; end: number } }[]
|
||||||
|
onRemoveTab: (id: string) => void
|
||||||
|
textareaRef: React.RefObject<HTMLTextAreaElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat message props interface
|
||||||
|
export interface MessageProps {
|
||||||
|
message: {
|
||||||
|
role: "user" | "assistant"
|
||||||
|
content: string
|
||||||
|
context?: string
|
||||||
|
}
|
||||||
|
setContext: (context: string | null, name: string, range?: { start: number, end: number }) => void
|
||||||
|
setIsContextExpanded: (isExpanded: boolean) => void
|
||||||
|
socket: Socket | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context tabs props interface
|
||||||
|
export interface ContextTabsProps {
|
||||||
|
activeFileName: string
|
||||||
|
onAddFile: (tab: ContextTab) => void
|
||||||
|
contextTabs: ContextTab[]
|
||||||
|
onRemoveTab: (id: string) => void
|
||||||
|
isExpanded: boolean
|
||||||
|
onToggleExpand: () => void
|
||||||
|
files?: (TFile | TFolder)[]
|
||||||
|
onFileSelect?: (file: TFile) => void
|
||||||
|
socket: Socket | null
|
||||||
|
}
|
@ -107,7 +107,6 @@ export default function CodeEditor({
|
|||||||
|
|
||||||
// Editor state
|
// Editor state
|
||||||
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
||||||
console.log("editor language: ", editorLanguage)
|
|
||||||
const [cursorLine, setCursorLine] = useState(0)
|
const [cursorLine, setCursorLine] = useState(0)
|
||||||
const [editorRef, setEditorRef] =
|
const [editorRef, setEditorRef] =
|
||||||
useState<monaco.editor.IStandaloneCodeEditor>()
|
useState<monaco.editor.IStandaloneCodeEditor>()
|
||||||
@ -173,6 +172,9 @@ export default function CodeEditor({
|
|||||||
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
const editorPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
|
||||||
|
|
||||||
|
// Ref to store the last copied range in the editor to be used in the AIChat component
|
||||||
|
const lastCopiedRangeRef = useRef<{ startLine: number; endLine: number } | null>(null);
|
||||||
|
|
||||||
const debouncedSetIsSelected = useRef(
|
const debouncedSetIsSelected = useRef(
|
||||||
debounce((value: boolean) => {
|
debounce((value: boolean) => {
|
||||||
setIsSelected(value)
|
setIsSelected(value)
|
||||||
@ -257,6 +259,17 @@ export default function CodeEditor({
|
|||||||
updatedOptions
|
updatedOptions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the last copied range in the editor to be used in the AIChat component
|
||||||
|
editor.onDidChangeCursorSelection((e) => {
|
||||||
|
const selection = editor.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
lastCopiedRangeRef.current = {
|
||||||
|
startLine: selection.startLineNumber,
|
||||||
|
endLine: selection.endLineNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the function with your file structure
|
// Call the function with your file structure
|
||||||
@ -1029,6 +1042,8 @@ export default function CodeEditor({
|
|||||||
setFiles={setFiles}
|
setFiles={setFiles}
|
||||||
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
|
||||||
deletingFolderId={deletingFolderId}
|
deletingFolderId={deletingFolderId}
|
||||||
|
toggleAIChat={toggleAIChat}
|
||||||
|
isAIChatOpen={isAIChatOpen}
|
||||||
/>
|
/>
|
||||||
{/* Outer ResizablePanelGroup for main layout */}
|
{/* Outer ResizablePanelGroup for main layout */}
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
@ -1218,6 +1233,9 @@ export default function CodeEditor({
|
|||||||
"No file selected"
|
"No file selected"
|
||||||
}
|
}
|
||||||
onClose={toggleAIChat}
|
onClose={toggleAIChat}
|
||||||
|
editorRef={{ current: editorRef }}
|
||||||
|
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||||
|
files={files}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</>
|
</>
|
||||||
|
@ -10,7 +10,7 @@ import New from "./new"
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { sortFileExplorer } from "@/lib/utils"
|
import { cn, sortFileExplorer } from "@/lib/utils"
|
||||||
import {
|
import {
|
||||||
dropTargetForElements,
|
dropTargetForElements,
|
||||||
monitorForElements,
|
monitorForElements,
|
||||||
@ -27,6 +27,8 @@ export default function Sidebar({
|
|||||||
setFiles,
|
setFiles,
|
||||||
addNew,
|
addNew,
|
||||||
deletingFolderId,
|
deletingFolderId,
|
||||||
|
toggleAIChat,
|
||||||
|
isAIChatOpen,
|
||||||
}: {
|
}: {
|
||||||
sandboxData: Sandbox
|
sandboxData: Sandbox
|
||||||
files: (TFile | TFolder)[]
|
files: (TFile | TFolder)[]
|
||||||
@ -43,6 +45,8 @@ export default function Sidebar({
|
|||||||
setFiles: (files: (TFile | TFolder)[]) => void
|
setFiles: (files: (TFile | TFolder)[]) => void
|
||||||
addNew: (name: string, type: "file" | "folder") => void
|
addNew: (name: string, type: "file" | "folder") => void
|
||||||
deletingFolderId: string
|
deletingFolderId: string
|
||||||
|
toggleAIChat: () => void
|
||||||
|
isAIChatOpen: boolean
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null) // drop target
|
const ref = useRef(null) // drop target
|
||||||
|
|
||||||
@ -188,7 +192,7 @@ export default function Sidebar({
|
|||||||
style={{ opacity: 1 }}
|
style={{ opacity: 1 }}
|
||||||
>
|
>
|
||||||
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
|
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
|
||||||
Copilot
|
AI Editor
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
||||||
<span className="text-xs">⌘</span>G
|
<span className="text-xs">⌘</span>G
|
||||||
@ -197,12 +201,24 @@ export default function Sidebar({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
|
className={cn(
|
||||||
disabled
|
"w-full justify-start text-sm font-normal h-8 px-2 mb-2 border-t",
|
||||||
aria-disabled="true"
|
isAIChatOpen
|
||||||
|
? "bg-muted-foreground/25 text-foreground"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
onClick={toggleAIChat}
|
||||||
|
aria-disabled={false}
|
||||||
style={{ opacity: 1 }}
|
style={{ opacity: 1 }}
|
||||||
>
|
>
|
||||||
<MessageSquareMore className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
|
<MessageSquareMore
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 mr-2",
|
||||||
|
isAIChatOpen
|
||||||
|
? "text-indigo-500"
|
||||||
|
: "text-indigo-500 opacity-70"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
AI Chat
|
AI Chat
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
|
||||||
|
@ -22,7 +22,7 @@ export default function Landing() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Button variant="outline" size="icon" asChild>
|
<Button variant="outline" size="icon" asChild>
|
||||||
<a href="https://www.x.com/ishaandey_" target="_blank">
|
<a href="https://x.com/gitwitdev" target="_blank">
|
||||||
<svg
|
<svg
|
||||||
width="1200"
|
width="1200"
|
||||||
height="1227"
|
height="1227"
|
||||||
@ -54,7 +54,7 @@ export default function Landing() {
|
|||||||
<CustomButton>Go To App</CustomButton>
|
<CustomButton>Go To App</CustomButton>
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/ishaan1013/sandbox"
|
href="https://github.com/jamesmurdza/sandbox"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="group h-9 px-4 py-2 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
className="group h-9 px-4 py-2 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
@ -73,7 +73,11 @@ function mapModule(module: string): monaco.languages.typescript.ModuleKind {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapJSX(jsx: string): monaco.languages.typescript.JsxEmit {
|
function mapJSX(jsx: string | undefined): monaco.languages.typescript.JsxEmit {
|
||||||
|
if (!jsx || typeof jsx !== 'string') {
|
||||||
|
return monaco.languages.typescript.JsxEmit.React // Default value
|
||||||
|
}
|
||||||
|
|
||||||
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
|
const jsxMap: { [key: string]: monaco.languages.typescript.JsxEmit } = {
|
||||||
preserve: monaco.languages.typescript.JsxEmit.Preserve,
|
preserve: monaco.languages.typescript.JsxEmit.Preserve,
|
||||||
react: monaco.languages.typescript.JsxEmit.React,
|
react: monaco.languages.typescript.JsxEmit.React,
|
||||||
|
6
frontend/package-lock.json
generated
6
frontend/package-lock.json
generated
@ -37,6 +37,7 @@
|
|||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"embla-carousel": "^8.3.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||||
"framer-motion": "^11.2.3",
|
"framer-motion": "^11.2.3",
|
||||||
@ -3128,12 +3129,14 @@
|
|||||||
"node_modules/embla-carousel": {
|
"node_modules/embla-carousel": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz",
|
||||||
"integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA=="
|
"integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz",
|
||||||
"integrity": "sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==",
|
"integrity": "sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"embla-carousel": "8.3.0",
|
"embla-carousel": "8.3.0",
|
||||||
"embla-carousel-reactive-utils": "8.3.0"
|
"embla-carousel-reactive-utils": "8.3.0"
|
||||||
@ -3154,6 +3157,7 @@
|
|||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-8.0.1.tgz",
|
||||||
"integrity": "sha512-LMAnruDqDmsjL6UoQD65aLotpmfO49Fsr3H0bMi7I+BH6jbv9OJiE61kN56daKsVtCQEt0SU1MrJslbhtgF3yQ==",
|
"integrity": "sha512-LMAnruDqDmsjL6UoQD65aLotpmfO49Fsr3H0bMi7I+BH6jbv9OJiE61kN56daKsVtCQEt0SU1MrJslbhtgF3yQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wheel-gestures": "^2.2.5"
|
"wheel-gestures": "^2.2.5"
|
||||||
},
|
},
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"embla-carousel": "^8.3.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||||
"framer-motion": "^11.2.3",
|
"framer-motion": "^11.2.3",
|
||||||
|
1669
package-lock.json
generated
1669
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-popover": "^1.1.1"
|
"@radix-ui/react-popover": "^1.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user