feat: added AI chat
backend implementation remaining
This commit is contained in:
parent
f192d9f3ab
commit
62e282da63
64
frontend/components/editor/AIChat.tsx
Normal file
64
frontend/components/editor/AIChat.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Send } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AIChat() {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatContainerRef.current) {
|
||||||
|
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (input.trim() === '') return;
|
||||||
|
|
||||||
|
const newMessage: Message = { role: 'user', content: input };
|
||||||
|
setMessages(prev => [...prev, newMessage]);
|
||||||
|
setInput('');
|
||||||
|
|
||||||
|
// 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.' };
|
||||||
|
setMessages(prev => [...prev, assistantMessage]);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div ref={chatContainerRef} className="flex-grow overflow-y-auto p-4 space-y-4">
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<div key={index} className={`${message.role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||||
|
<div className={`inline-block p-2 rounded-lg ${message.role === 'user' ? 'bg-blue-500 text-white' : 'bg-gray-200 text-black'}`}>
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="p-4 border-t">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
||||||
|
className="flex-grow p-2 border rounded-lg"
|
||||||
|
placeholder="Type your message..."
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSend}>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -37,6 +37,7 @@ import { Button } from "../ui/button"
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
|
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
|
||||||
import { deepMerge } from "@/lib/utils"
|
import { deepMerge } from "@/lib/utils"
|
||||||
|
import AIChat from "./AIChat"
|
||||||
|
|
||||||
export default function CodeEditor({
|
export default function CodeEditor({
|
||||||
userData,
|
userData,
|
||||||
@ -73,6 +74,12 @@ export default function CodeEditor({
|
|||||||
message: "",
|
message: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Layout state
|
||||||
|
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
|
||||||
|
|
||||||
|
// AI Chat state
|
||||||
|
const [isAIChatOpen, setIsAIChatOpen] = useState(false);
|
||||||
|
|
||||||
// File state
|
// File state
|
||||||
const [files, setFiles] = useState<(TFolder | TFile)[]>([])
|
const [files, setFiles] = useState<(TFolder | TFile)[]>([])
|
||||||
const [tabs, setTabs] = useState<TTab[]>([])
|
const [tabs, setTabs] = useState<TTab[]>([])
|
||||||
@ -513,20 +520,23 @@ export default function CodeEditor({
|
|||||||
[socket, fileContents]
|
[socket, fileContents]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S
|
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S, and toggle AI chat on Ctrl+L or Cmd+L
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
debouncedSaveData(activeFileId);
|
debouncedSaveData(activeFileId);
|
||||||
|
} else if (e.key === "l" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsAIChatOpen(prev => !prev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener("keydown", down)
|
document.addEventListener("keydown", down)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down)
|
||||||
}
|
}
|
||||||
}, [activeFileId, tabs, debouncedSaveData])
|
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen])
|
||||||
|
|
||||||
// Liveblocks live collaboration setup effect
|
// Liveblocks live collaboration setup effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -837,8 +847,6 @@ export default function CodeEditor({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
|
|
||||||
|
|
||||||
const toggleLayout = () => {
|
const toggleLayout = () => {
|
||||||
setIsHorizontalLayout(prev => !prev);
|
setIsHorizontalLayout(prev => !prev);
|
||||||
};
|
};
|
||||||
@ -1075,54 +1083,58 @@ export default function CodeEditor({
|
|||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePanel defaultSize={isHorizontalLayout ? 30 : 30}>
|
<ResizablePanel defaultSize={isHorizontalLayout ? 30 : 30}>
|
||||||
<ResizablePanelGroup direction={isHorizontalLayout ? "horizontal" : "vertical"}>
|
{isAIChatOpen ? (
|
||||||
<ResizablePanel
|
<AIChat />
|
||||||
ref={previewPanelRef}
|
) : (
|
||||||
defaultSize={4}
|
<ResizablePanelGroup direction={isHorizontalLayout ? "horizontal" : "vertical"}>
|
||||||
collapsedSize={isHorizontalLayout ? 20 : 4}
|
<ResizablePanel
|
||||||
minSize={25}
|
ref={previewPanelRef}
|
||||||
collapsible
|
defaultSize={4}
|
||||||
className="p-2 flex flex-col"
|
collapsedSize={isHorizontalLayout ? 20 : 4}
|
||||||
onCollapse={() => setIsPreviewCollapsed(true)}
|
minSize={25}
|
||||||
onExpand={() => setIsPreviewCollapsed(false)}
|
collapsible
|
||||||
>
|
className="p-2 flex flex-col"
|
||||||
<div className="flex items-center justify-between">
|
onCollapse={() => setIsPreviewCollapsed(true)}
|
||||||
<Button onClick={toggleLayout} size="sm" variant="ghost" className="mr-2 border">
|
onExpand={() => setIsPreviewCollapsed(false)}
|
||||||
{isHorizontalLayout ? <ArrowRightToLine className="w-4 h-4" /> : <ArrowDownToLine className="w-4 h-4" />}
|
>
|
||||||
</Button>
|
<div className="flex items-center justify-between">
|
||||||
<PreviewWindow
|
<Button onClick={toggleLayout} size="sm" variant="ghost" className="mr-2 border">
|
||||||
open={togglePreviewPanel}
|
{isHorizontalLayout ? <ArrowRightToLine className="w-4 h-4" /> : <ArrowDownToLine className="w-4 h-4" />}
|
||||||
collapsed={isPreviewCollapsed}
|
</Button>
|
||||||
src={previewURL}
|
<PreviewWindow
|
||||||
ref={previewWindowRef}
|
open={togglePreviewPanel}
|
||||||
/>
|
collapsed={isPreviewCollapsed}
|
||||||
</div>
|
|
||||||
{!isPreviewCollapsed && (
|
|
||||||
<div className="w-full grow rounded-md overflow-hidden bg-foreground mt-2">
|
|
||||||
<iframe
|
|
||||||
width={"100%"}
|
|
||||||
height={"100%"}
|
|
||||||
src={previewURL}
|
src={previewURL}
|
||||||
/>
|
ref={previewWindowRef}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
</ResizablePanel>
|
|
||||||
<ResizableHandle />
|
|
||||||
<ResizablePanel
|
|
||||||
defaultSize={50}
|
|
||||||
minSize={20}
|
|
||||||
className="p-2 flex flex-col"
|
|
||||||
>
|
|
||||||
{isOwner ? (
|
|
||||||
<Terminals />
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
|
||||||
<TerminalSquare className="w-4 h-4 mr-2" />
|
|
||||||
No terminal access.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{!isPreviewCollapsed && (
|
||||||
</ResizablePanel>
|
<div className="w-full grow rounded-md overflow-hidden bg-foreground mt-2">
|
||||||
</ResizablePanelGroup>
|
<iframe
|
||||||
|
width={"100%"}
|
||||||
|
height={"100%"}
|
||||||
|
src={previewURL}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ResizablePanel>
|
||||||
|
<ResizableHandle />
|
||||||
|
<ResizablePanel
|
||||||
|
defaultSize={50}
|
||||||
|
minSize={20}
|
||||||
|
className="p-2 flex flex-col"
|
||||||
|
>
|
||||||
|
{isOwner ? (
|
||||||
|
<Terminals />
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
||||||
|
<TerminalSquare className="w-4 h-4 mr-2" />
|
||||||
|
No terminal access.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
)}
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</PreviewProvider>
|
</PreviewProvider>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user