Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 116 additions & 17 deletions chat/bun.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions chat/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
{
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.11",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jszip": "^3.10.1",
"lucide-react": "^0.511.0",
"next": "15.4.7",
"next-themes": "^0.4.6",
"path-browserify": "^1.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.8",
"react-textarea-autosize": "^8.5.9",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0"
Expand Down
48 changes: 48 additions & 0 deletions chat/src/components/chat-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface ChatContextValue {
loading: boolean;
serverStatus: ServerStatus;
sendMessage: (message: string, type?: MessageType) => void;
uploadFiles: (formData: FormData) => Promise<boolean>;
}

const ChatContext = createContext<ChatContextValue | undefined>(undefined);
Expand Down Expand Up @@ -268,13 +269,60 @@ export function ChatProvider({ children }: PropsWithChildren) {
}
};

// Upload files to workspace
const uploadFiles = async (formData: FormData): Promise<boolean> => {
let success = true;
try{
const response = await fetch(`${agentAPIUrl}/upload`, {
method: 'POST',
body: formData,
});

if (!response.ok) {
success = false;
const errorData = await response.json();
console.error("Failed to send message:", errorData);
const detail = errorData.detail;
const messages =
"errors" in errorData
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
errorData.errors.map((e: any) => e.message).join(", ")
: "";

const fullDetail = `${detail}: ${messages}`;
toast.error(`Failed to upload files`, {
description: fullDetail,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
success = false;
console.error("Error uploading files:", error);
const detail = error.detail;
const messages =
"errors" in error
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
error.errors.map((e: any) => e.message).join("\n")
: "";

const fullDetail = `${detail}: ${messages}`;

toast.error(`Error uploading files`, {
description: fullDetail,
});
}
return success;
}


return (
<ChatContext.Provider
value={{
messages,
loading,
sendMessage,
serverStatus,
uploadFiles,
}}
>
{children}
Expand Down
6 changes: 3 additions & 3 deletions chat/src/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
"use client";

import { useChat } from "./chat-provider";
import {useChat} from "./chat-provider";
import MessageInput from "./message-input";
import MessageList from "./message-list";

export function Chat() {
const { messages, loading, sendMessage, serverStatus } = useChat();
const {messages, loading, sendMessage, serverStatus} = useChat();

return (
<>
<MessageList messages={messages} />
<MessageList messages={messages}/>
<MessageInput
onSendMessage={sendMessage}
disabled={loading}
Expand Down
45 changes: 36 additions & 9 deletions chat/src/components/message-input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, FormEvent, KeyboardEvent, useEffect, useRef } from "react";
import { useState, FormEvent, KeyboardEvent, MouseEvent, useEffect, useRef } from "react";
import { Button } from "./ui/button";
import {
ArrowDownIcon,
Expand All @@ -10,11 +10,13 @@ import {
CornerDownLeftIcon,
DeleteIcon,
SendIcon,
Upload,
Square,
} from "lucide-react";
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
import type { ServerStatus } from "./chat-provider";
import TextareaAutosize from "react-textarea-autosize";
import { UploadDialog } from "./upload-dialog";

interface MessageInputProps {
onSendMessage: (message: string, type: "user" | "raw") => void;
Expand Down Expand Up @@ -56,6 +58,7 @@ export default function MessageInput({
const textareaRef = useRef<HTMLTextAreaElement>(null);
const nextCharId = useRef(0);
const [controlAreaFocused, setControlAreaFocused] = useState(false);
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);

const handleSubmit = (e: FormEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -143,6 +146,11 @@ export default function MessageInput({
}
};

const handleUploadClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setUploadDialogOpen(true);
};

return (
<Tabs value={inputMode} onValueChange={setInputMode}>
<div className="max-w-4xl mx-auto w-full p-4 pt-0">
Expand Down Expand Up @@ -205,17 +213,28 @@ export default function MessageInput({
</TabsTrigger>
</TabsList>

{inputMode === "text" && serverStatus !== "running" && (
<div className={"flex flex-row gap-3"}>
<Button
type="submit"
disabled={disabled || !message.trim()}
size="icon"
className="rounded-full"
onClick={handleUploadClick}
>
<SendIcon />
<span className="sr-only">Send</span>
<Upload/>
<span className="sr-only">Upload</span>
</Button>
)}

{inputMode === "text" && serverStatus !== "running" && (
<Button
type="submit"
disabled={disabled || !message.trim()}
size="icon"
className="rounded-full"
>
<SendIcon/>
<span className="sr-only">Send</span>
</Button>
)}

{inputMode === "text" && serverStatus === "running" && (
<Button
Expand All @@ -240,9 +259,11 @@ export default function MessageInput({
>
<Char char={char.char} />
</span>
))}
</div>
)}
))}
</div>
)}
</div>

</div>
</div>
</form>
Expand All @@ -259,6 +280,12 @@ export default function MessageInput({
)}
</span>
</div>

<UploadDialog
open={uploadDialogOpen}
onOpenChange={setUploadDialogOpen}
setMessage={setMessage}
/>
</Tabs>
);
}
Expand Down
32 changes: 32 additions & 0 deletions chat/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client"

import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}

export { Checkbox }
150 changes: 150 additions & 0 deletions chat/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"use client"

import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"

function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}

function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}

function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}

function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}

function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50",
className
)}
{...props}
/>
)
}

function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}

function DialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
}

function DialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
}

function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
)
}

function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}

export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
Loading