-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
75e62c9
commit a88b635
Showing
11 changed files
with
1,310 additions
and
301 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { openai } from "@ai-sdk/openai"; | ||
import { streamText } from "ai"; | ||
|
||
// Allow streaming responses up to 60 seconds | ||
export const maxDuration = 60; | ||
|
||
export async function POST(req: Request) { | ||
const { messages } = await req.json(); | ||
|
||
const result = await streamText({ | ||
model: openai("gpt-4o-mini"), | ||
system: "You are a helpful assistant.", | ||
messages, | ||
}); | ||
|
||
return result.toDataStreamResponse(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { NextResponse } from "next/server"; | ||
import { Operation } from "@/app/types"; | ||
|
||
export async function POST(request: Request) { | ||
try { | ||
const { operation, instruction } = await request.json(); | ||
|
||
// TODO: Implement your LLM API call here to modify the operation based on the instruction | ||
// This is just a placeholder that returns the original operation | ||
const updatedOperation: Operation = operation; | ||
|
||
return NextResponse.json(updatedOperation); | ||
} catch (error) { | ||
console.error("Error in edit API:", error); | ||
return NextResponse.json( | ||
{ error: "Failed to process edit request" }, | ||
{ status: 500 } | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
"use client"; | ||
|
||
import React, { useRef, useState, useEffect } from "react"; | ||
import { ResizableBox } from "react-resizable"; | ||
import { X } from "lucide-react"; | ||
import { Button } from "@/components/ui/button"; | ||
import { Input } from "@/components/ui/input"; | ||
import { ScrollArea } from "@/components/ui/scroll-area"; | ||
import { useChat } from "ai/react"; | ||
import { cn } from "@/lib/utils"; | ||
import { Loader2, Scroll } from "lucide-react"; | ||
import "react-resizable/css/styles.css"; | ||
|
||
interface AIChatPanelProps { | ||
onClose: () => void; | ||
} | ||
|
||
interface Message { | ||
role: "assistant" | "user"; | ||
content: string; | ||
} | ||
|
||
const AIChatPanel: React.FC<AIChatPanelProps> = ({ onClose }) => { | ||
const [position, setPosition] = useState({ | ||
x: window.innerWidth - 400, | ||
y: 80, | ||
}); | ||
const isDragging = useRef(false); | ||
const dragOffset = useRef({ x: 0, y: 0 }); | ||
const scrollAreaRef = useRef<HTMLDivElement>(null); | ||
const { messages, input, handleInputChange, handleSubmit, isLoading } = | ||
useChat({ | ||
keepLastMessageOnError: true, | ||
}); | ||
|
||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => { | ||
if ((e.target as HTMLElement).classList.contains("drag-handle")) { | ||
isDragging.current = true; | ||
dragOffset.current = { | ||
x: e.clientX - position.x, | ||
y: e.clientY - position.y, | ||
}; | ||
} | ||
}; | ||
|
||
const handleMouseMove = (e: MouseEvent) => { | ||
if (isDragging.current) { | ||
setPosition({ | ||
x: Math.max( | ||
0, | ||
Math.min(window.innerWidth - 400, e.clientX - dragOffset.current.x) | ||
), | ||
y: Math.max( | ||
0, | ||
Math.min(window.innerHeight - 600, e.clientY - dragOffset.current.y) | ||
), | ||
}); | ||
} | ||
}; | ||
|
||
const handleMouseUp = () => { | ||
isDragging.current = false; | ||
}; | ||
|
||
useEffect(() => { | ||
document.addEventListener("mousemove", handleMouseMove); | ||
document.addEventListener("mouseup", handleMouseUp); | ||
return () => { | ||
document.removeEventListener("mousemove", handleMouseMove); | ||
document.removeEventListener("mouseup", handleMouseUp); | ||
}; | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (scrollAreaRef.current) { | ||
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight; | ||
} | ||
}, [messages]); | ||
|
||
return ( | ||
<div | ||
style={{ | ||
position: "fixed", | ||
top: position.y, | ||
left: position.x, | ||
zIndex: 9999, | ||
}} | ||
> | ||
<ResizableBox | ||
width={400} | ||
height={500} | ||
minConstraints={[300, 400]} | ||
maxConstraints={[800, 800]} | ||
resizeHandles={["sw", "se"]} | ||
className="bg-white rounded-lg shadow-lg border overflow-hidden" | ||
> | ||
<div | ||
className="h-6 bg-gray-100 drag-handle flex justify-between items-center px-2 cursor-move" | ||
onMouseDown={handleMouseDown} | ||
> | ||
<span className="text-sm text-primary font-medium"> | ||
<Scroll size={14} /> | ||
</span> | ||
<Button | ||
variant="ghost" | ||
size="sm" | ||
className="h-5 w-5 p-0" | ||
onClick={onClose} | ||
> | ||
<X size={14} /> | ||
</Button> | ||
</div> | ||
<div className="flex flex-col h-[calc(100%-24px)]"> | ||
<ScrollArea ref={scrollAreaRef} className="flex-1 p-4"> | ||
{messages.map((message, index) => ( | ||
<div | ||
key={index} | ||
className={cn( | ||
"mb-2 flex", | ||
message.role === "assistant" ? "justify-start" : "justify-end" | ||
)} | ||
> | ||
<div | ||
className={cn( | ||
"rounded-lg px-3 py-2 max-w-[80%]", | ||
message.role === "assistant" | ||
? "bg-gray-100 text-gray-900" | ||
: "bg-primary text-white" | ||
)} | ||
> | ||
{message.content} | ||
</div> | ||
</div> | ||
))} | ||
{isLoading && ( | ||
<div className="flex justify-center"> | ||
<Loader2 className="h-4 w-4 animate-spin" /> | ||
</div> | ||
)} | ||
</ScrollArea> | ||
<form | ||
onSubmit={handleSubmit} | ||
className="border-t p-4 flex gap-2 items-center" | ||
> | ||
<Input | ||
value={input} | ||
onChange={handleInputChange} | ||
placeholder="Ask a question..." | ||
className="flex-1" | ||
/> | ||
<Button type="submit" size="sm" disabled={isLoading}> | ||
Send | ||
</Button> | ||
</form> | ||
</div> | ||
</ResizableBox> | ||
</div> | ||
); | ||
}; | ||
|
||
export default AIChatPanel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import React, { useState } from "react"; | ||
import { Button } from "@/components/ui/button"; | ||
import { Textarea } from "@/components/ui/textarea"; | ||
import { PopoverContent } from "@/components/ui/popover"; | ||
|
||
interface AIEditPopoverProps { | ||
onSubmit: (instruction: string) => void; | ||
} | ||
|
||
export const AIEditPopover: React.FC<AIEditPopoverProps> = React.memo( | ||
({ onSubmit }) => { | ||
const [instruction, setInstruction] = useState(""); | ||
const [isLoading, setIsLoading] = useState(false); | ||
|
||
const handleSubmit = async (e: React.FormEvent) => { | ||
e.preventDefault(); | ||
if (!instruction.trim()) return; | ||
|
||
setIsLoading(true); | ||
try { | ||
await onSubmit(instruction); | ||
setInstruction(""); | ||
} finally { | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<PopoverContent> | ||
<form onSubmit={handleSubmit}> | ||
<div className="grid gap-2"> | ||
<p className="text-sm text-muted-foreground"> | ||
Describe how you want to modify this operation. | ||
</p> | ||
<div className="grid gap-2"> | ||
<Textarea | ||
placeholder="e.g. Make the prompt more concise" | ||
value={instruction} | ||
onChange={(e) => setInstruction(e.target.value)} | ||
disabled={isLoading} | ||
/> | ||
<Button type="submit" disabled={!instruction.trim() || isLoading}> | ||
{isLoading ? ( | ||
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-white"></div> | ||
) : ( | ||
"Apply" | ||
)} | ||
</Button> | ||
</div> | ||
</div> | ||
</form> | ||
</PopoverContent> | ||
); | ||
} | ||
); | ||
|
||
AIEditPopover.displayName = "AIEditPopover"; |
Oops, something went wrong.