Skip to content

Commit

Permalink
feat: add assistants
Browse files Browse the repository at this point in the history
  • Loading branch information
shreyashankar committed Oct 31, 2024
1 parent 75e62c9 commit a88b635
Show file tree
Hide file tree
Showing 11 changed files with 1,310 additions and 301 deletions.
619 changes: 612 additions & 7 deletions website/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@agbishop/react-ansi-18": "^4.0.6",
"@ai-sdk/openai": "^0.0.70",
"@hookform/resolvers": "^3.9.0",
"@next/third-parties": "^14.2.11",
"@radix-ui/react-accordion": "^1.2.0",
Expand All @@ -33,6 +34,8 @@
"@tanstack/react-query": "^5.59.15",
"@tanstack/react-table": "^8.20.5",
"@tanstack/react-virtual": "^3.10.8",
"@types/react-resizable": "^3.0.8",
"ai": "^3.4.29",
"anser": "^2.3.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.7.7",
Expand Down
17 changes: 17 additions & 0 deletions website/src/app/api/chat/route.ts
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();
}
20 changes: 20 additions & 0 deletions website/src/app/api/edit/route.ts
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 }
);
}
}
13 changes: 8 additions & 5 deletions website/src/app/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import dynamic from "next/dynamic";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import { Scroll, Info, Save } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -84,6 +84,7 @@ import {
} from "@/utils/fileOperations";
import * as localStorageKeys from "@/app/localStorageKeys";
import { toast } from "@/hooks/use-toast";
import AIChatPanel from "@/components/AIChatPanel";

const LeftPanelIcon: React.FC<{ isActive: boolean }> = ({ isActive }) => (
<svg
Expand Down Expand Up @@ -221,6 +222,7 @@ const CodeEditorPipelineApp: React.FC = () => {
const [showFileExplorer, setShowFileExplorer] = useState(true);
const [showOutput, setShowOutput] = useState(true);
const [showDatasetView, setShowDatasetView] = useState(false);
const [showChat, setShowChat] = useState(false);

const {
operations,
Expand Down Expand Up @@ -355,11 +357,11 @@ const CodeEditorPipelineApp: React.FC = () => {
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger className="opacity-50 cursor-not-allowed">
Assistant (Not Yet Available)
</MenubarTrigger>
<MenubarTrigger>Assistant</MenubarTrigger>
<MenubarContent>
<MenubarItem disabled>Open Assistant</MenubarItem>
<MenubarItem onSelect={() => setShowChat(!showChat)}>
Toggle Chat
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
Expand Down Expand Up @@ -490,6 +492,7 @@ const CodeEditorPipelineApp: React.FC = () => {
</TooltipProvider>
</div>
</div>
{showChat && <AIChatPanel onClose={() => setShowChat(false)} />}
{/* Main content */}
<ResizablePanelGroup direction="horizontal" className="flex-grow">
{/* File Explorer and Bookmarks */}
Expand Down
161 changes: 161 additions & 0 deletions website/src/components/AIChatPanel.tsx
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;
57 changes: 57 additions & 0 deletions website/src/components/AIEditPopover.tsx
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";
Loading

0 comments on commit a88b635

Please sign in to comment.