Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
132 changes: 115 additions & 17 deletions chat/bun.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions chat/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
{
"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",
"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
85 changes: 71 additions & 14 deletions chat/src/components/chat-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useContext,
} from "react";
import { toast } from "sonner";
import {getErrorMessage} from "@/lib/error-utils";

interface Message {
id: number;
Expand All @@ -34,6 +35,22 @@ interface StatusChangeEvent {
status: string;
}

interface APIErrorDetail {
location: string;
message: string;
value: null | string | number | boolean | object;
}

interface APIErrorModel {
$schema: string;
detail: string;
errors: APIErrorDetail[];
instance: string;
status: number;
title: string;
type: string;
}

function isDraftMessage(message: Message | DraftMessage): boolean {
return message.id === undefined;
}
Expand All @@ -42,11 +59,17 @@ type MessageType = "user" | "raw";

export type ServerStatus = "stable" | "running" | "offline" | "unknown";

export interface FileUploadResponse {
ok: boolean;
filePath?: string;
}

interface ChatContextValue {
messages: (Message | DraftMessage)[];
loading: boolean;
serverStatus: ServerStatus;
sendMessage: (message: string, type?: MessageType) => void;
uploadFiles: (formData: FormData) => Promise<FileUploadResponse>;
}

const ChatContext = createContext<ChatContextValue | undefined>(undefined);
Expand Down Expand Up @@ -229,34 +252,27 @@ export function ChatProvider({ children }: PropsWithChildren) {
});

if (!response.ok) {
const errorData = await response.json();
const errorData = await response.json() as APIErrorModel;
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(", ")
?
errorData.errors.map((e: APIErrorDetail) => e.message).join(", ")
: "";

const fullDetail = `${detail}: ${messages}`;
toast.error(`Failed to send message`, {
description: fullDetail,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error sending message:", 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}`;
} catch (error) {
console.error("Error sending message:", error);
const message = getErrorMessage(error)

toast.error(`Error sending message`, {
description: fullDetail,
description: message,
});
} finally {
if (type === "user") {
Expand All @@ -268,13 +284,54 @@ export function ChatProvider({ children }: PropsWithChildren) {
}
};

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

if (!response.ok) {
result.ok = false;
const errorData = await response.json() as APIErrorModel;
console.error("Failed to send message:", errorData);
const detail = errorData.detail;
const messages =
"errors" in errorData
?
errorData.errors.map((e: APIErrorDetail) => e.message).join(", ")
: "";

const fullDetail = `${detail}: ${messages}`;
toast.error(`Failed to upload files`, {
description: fullDetail,
});
} else {
result = (await response.json()) as FileUploadResponse;
}

} catch (error) {
result.ok = false;
console.error("Error uploading files:", error);
const message = getErrorMessage(error)

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

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
37 changes: 37 additions & 0 deletions chat/src/components/drag-drop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { useDropzone } from "react-dropzone";

export interface DragDropProps {
onFilesAdded: (files: File[]) => void;
disabled?: boolean;
children: React.ReactNode;
className?: string;
}

export function DragDrop({ onFilesAdded, disabled = false, children, className = "" }: DragDropProps) {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
noClick: true,
disabled,
onDropAccepted: (files: File[]) => {
onFilesAdded(files);
},
multiple: true,
});

return (
<div
{...getRootProps()}
className={`relative ${className} ${
isDragActive && !disabled ? 'border-primary border-2 border-dashed rounded-lg text-center transition-colors' : ''
}`}
>
<input {...getInputProps()} />
{isDragActive && !disabled && (
<div className="absolute inset-0 flex items-center justify-center bg-primary/20z-10">
<p className="text-sm text-primary font-medium">Drop the files here</p>
</div>
)}
{children}
</div>
);
}
Loading