Skip to content

Commit 6242e77

Browse files
committed
feat: ui changes for files upload
1 parent d0b5ad0 commit 6242e77

File tree

8 files changed

+12697
-29
lines changed

8 files changed

+12697
-29
lines changed

chat/bun.lock

Lines changed: 194 additions & 18 deletions
Large diffs are not rendered by default.

chat/package-lock.json

Lines changed: 12092 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chat/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
{
22
"dependencies": {
3+
"@radix-ui/react-dialog": "^1.1.15",
34
"@radix-ui/react-dropdown-menu": "^2.1.14",
45
"@radix-ui/react-slot": "^1.2.2",
56
"@radix-ui/react-tabs": "^1.1.11",
7+
"@uppy/core": "^5.0.2",
8+
"@uppy/react": "^5.0.3",
69
"class-variance-authority": "^0.7.1",
710
"clsx": "^2.1.1",
11+
"jszip": "^3.10.1",
812
"lucide-react": "^0.511.0",
913
"next": "15.4.7",
1014
"next-themes": "^0.4.6",

chat/src/components/chat-provider.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ interface ChatContextValue {
4747
loading: boolean;
4848
serverStatus: ServerStatus;
4949
sendMessage: (message: string, type?: MessageType) => void;
50+
uploadFiles: (formData: FormData) => void;
5051
}
5152

5253
const ChatContext = createContext<ChatContextValue | undefined>(undefined);
@@ -268,13 +269,56 @@ export function ChatProvider({ children }: PropsWithChildren) {
268269
}
269270
};
270271

272+
// Upload files to workspace
273+
const uploadFiles = async (formData: FormData) => {
274+
try{
275+
const response = await fetch(`${agentAPIUrl}/upload`, {
276+
method: 'POST',
277+
body: formData,
278+
});
279+
280+
if (!response.ok) {
281+
const errorData = await response.json();
282+
console.error("Failed to send message:", errorData);
283+
const detail = errorData.detail;
284+
const messages =
285+
"errors" in errorData
286+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
287+
errorData.errors.map((e: any) => e.message).join(", ")
288+
: "";
289+
290+
const fullDetail = `${detail}: ${messages}`;
291+
toast.error(`Failed to upload files`, {
292+
description: fullDetail,
293+
});
294+
}
295+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
296+
} catch (error: any) {
297+
console.error("Error uploading files:", error);
298+
const detail = error.detail;
299+
const messages =
300+
"errors" in error
301+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
302+
error.errors.map((e: any) => e.message).join("\n")
303+
: "";
304+
305+
const fullDetail = `${detail}: ${messages}`;
306+
307+
toast.error(`Error uploading files`, {
308+
description: fullDetail,
309+
});
310+
}
311+
}
312+
313+
271314
return (
272315
<ChatContext.Provider
273316
value={{
274317
messages,
275318
loading,
276319
sendMessage,
277320
serverStatus,
321+
uploadFiles,
278322
}}
279323
>
280324
{children}

chat/src/components/chat.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
import { useChat } from "./chat-provider";
44
import MessageInput from "./message-input";
55
import MessageList from "./message-list";
6+
import {UppyContextProvider} from "@uppy/react";
7+
import {useState} from "react";
8+
import {Uppy} from "@uppy/core";
69

710
export function Chat() {
811
const { messages, loading, sendMessage, serverStatus } = useChat();
12+
const [uppy] = useState(() => new Uppy());
913

1014
return (
11-
<>
15+
<UppyContextProvider uppy={uppy}>
1216
<MessageList messages={messages} />
1317
<MessageInput
1418
onSendMessage={sendMessage}
1519
disabled={loading}
1620
serverStatus={serverStatus}
1721
/>
18-
</>
22+
</UppyContextProvider>
1923
);
2024
}

chat/src/components/message-input.tsx

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, FormEvent, KeyboardEvent, useEffect, useRef } from "react";
3+
import { useState, FormEvent, KeyboardEvent, MouseEvent, useEffect, useRef } from "react";
44
import { Button } from "./ui/button";
55
import {
66
ArrowDownIcon,
@@ -10,11 +10,13 @@ import {
1010
CornerDownLeftIcon,
1111
DeleteIcon,
1212
SendIcon,
13+
Upload,
1314
Square,
1415
} from "lucide-react";
1516
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
1617
import type { ServerStatus } from "./chat-provider";
1718
import TextareaAutosize from "react-textarea-autosize";
19+
import { UploadDialog } from "./upload-dialog";
1820

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

6063
const handleSubmit = (e: FormEvent) => {
6164
e.preventDefault();
@@ -143,6 +146,11 @@ export default function MessageInput({
143146
}
144147
};
145148

149+
const handleUploadClick = (e: MouseEvent<HTMLButtonElement>) => {
150+
e.preventDefault();
151+
setUploadDialogOpen(true);
152+
};
153+
146154
return (
147155
<Tabs value={inputMode} onValueChange={setInputMode}>
148156
<div className="max-w-4xl mx-auto w-full p-4 pt-0">
@@ -205,17 +213,28 @@ export default function MessageInput({
205213
</TabsTrigger>
206214
</TabsList>
207215

208-
{inputMode === "text" && serverStatus !== "running" && (
216+
<div className={"flex flex-row gap-3"}>
209217
<Button
210218
type="submit"
211-
disabled={disabled || !message.trim()}
212219
size="icon"
213220
className="rounded-full"
221+
onClick={handleUploadClick}
214222
>
215-
<SendIcon />
216-
<span className="sr-only">Send</span>
223+
<Upload/>
224+
<span className="sr-only">Upload</span>
217225
</Button>
218-
)}
226+
227+
{inputMode === "text" && serverStatus !== "running" && (
228+
<Button
229+
type="submit"
230+
disabled={disabled || !message.trim()}
231+
size="icon"
232+
className="rounded-full"
233+
>
234+
<SendIcon/>
235+
<span className="sr-only">Send</span>
236+
</Button>
237+
)}
219238

220239
{inputMode === "text" && serverStatus === "running" && (
221240
<Button
@@ -240,9 +259,11 @@ export default function MessageInput({
240259
>
241260
<Char char={char.char} />
242261
</span>
243-
))}
244-
</div>
245-
)}
262+
))}
263+
</div>
264+
)}
265+
</div>
266+
246267
</div>
247268
</div>
248269
</form>
@@ -259,6 +280,11 @@ export default function MessageInput({
259280
)}
260281
</span>
261282
</div>
283+
284+
<UploadDialog
285+
open={uploadDialogOpen}
286+
onOpenChange={setUploadDialogOpen}
287+
/>
262288
</Tabs>
263289
);
264290
}

chat/src/components/ui/dialog.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as DialogPrimitive from "@radix-ui/react-dialog"
5+
import { X } from "lucide-react"
6+
7+
import { cn } from "@/lib/utils"
8+
9+
function Dialog({
10+
...props
11+
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
12+
return <DialogPrimitive.Root data-slot="dialog" {...props} />
13+
}
14+
15+
function DialogTrigger({
16+
...props
17+
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
19+
}
20+
21+
function DialogPortal({
22+
...props
23+
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
25+
}
26+
27+
function DialogClose({
28+
...props
29+
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
30+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
31+
}
32+
33+
function DialogOverlay({
34+
className,
35+
...props
36+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37+
return (
38+
<DialogPrimitive.Overlay
39+
data-slot="dialog-overlay"
40+
className={cn(
41+
"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",
42+
className
43+
)}
44+
{...props}
45+
/>
46+
)
47+
}
48+
49+
function DialogContent({
50+
className,
51+
children,
52+
...props
53+
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
54+
return (
55+
<DialogPortal>
56+
<DialogOverlay />
57+
<DialogPrimitive.Content
58+
data-slot="dialog-content"
59+
className={cn(
60+
"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",
61+
className
62+
)}
63+
{...props}
64+
>
65+
{children}
66+
<DialogPrimitive.Close
67+
data-slot="dialog-close"
68+
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"
69+
>
70+
<X className="size-4" />
71+
<span className="sr-only">Close</span>
72+
</DialogPrimitive.Close>
73+
</DialogPrimitive.Content>
74+
</DialogPortal>
75+
)
76+
}
77+
78+
function DialogHeader({
79+
className,
80+
...props
81+
}: React.ComponentProps<"div">) {
82+
return (
83+
<div
84+
data-slot="dialog-header"
85+
className={cn(
86+
"flex flex-col space-y-1.5 text-center sm:text-left",
87+
className
88+
)}
89+
{...props}
90+
/>
91+
)
92+
}
93+
94+
function DialogFooter({
95+
className,
96+
...props
97+
}: React.ComponentProps<"div">) {
98+
return (
99+
<div
100+
data-slot="dialog-footer"
101+
className={cn(
102+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
103+
className
104+
)}
105+
{...props}
106+
/>
107+
)
108+
}
109+
110+
function DialogTitle({
111+
className,
112+
...props
113+
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
114+
return (
115+
<DialogPrimitive.Title
116+
data-slot="dialog-title"
117+
className={cn(
118+
"text-lg font-semibold leading-none tracking-tight",
119+
className
120+
)}
121+
{...props}
122+
/>
123+
)
124+
}
125+
126+
function DialogDescription({
127+
className,
128+
...props
129+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
130+
return (
131+
<DialogPrimitive.Description
132+
data-slot="dialog-description"
133+
className={cn("text-muted-foreground text-sm", className)}
134+
{...props}
135+
/>
136+
)
137+
}
138+
139+
export {
140+
Dialog,
141+
DialogPortal,
142+
DialogOverlay,
143+
DialogTrigger,
144+
DialogClose,
145+
DialogContent,
146+
DialogHeader,
147+
DialogFooter,
148+
DialogTitle,
149+
DialogDescription,
150+
}

0 commit comments

Comments
 (0)