1
- import { useState } from "react" ;
1
+ import { useRef , useState } from "react" ;
2
2
import { useParams } from "react-router-dom" ;
3
3
import { useMutation } from "@tanstack/react-query" ;
4
4
import { Button } from "@/components/ui/button" ;
5
+ import { ImageIcon } from "lucide-react" ;
5
6
import { Input } from "@/components/ui/input" ;
6
7
import "./App.css" ;
8
+ import path from "path" ;
7
9
8
10
type TextResponse = {
9
11
text : string ;
10
12
user : string ;
13
+ attachments ?: { url : string ; contentType : string ; title : string } [ ] ;
11
14
} ;
12
15
13
16
export default function Chat ( ) {
14
17
const { agentId } = useParams ( ) ;
15
18
const [ input , setInput ] = useState ( "" ) ;
16
19
const [ messages , setMessages ] = useState < TextResponse [ ] > ( [ ] ) ;
20
+ const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
21
+ const fileInputRef = useRef < HTMLInputElement > ( null ) ;
17
22
18
23
const mutation = useMutation ( {
19
24
mutationFn : async ( text : string ) => {
25
+ const formData = new FormData ( ) ;
26
+ formData . append ( "text" , text ) ;
27
+ formData . append ( "userId" , "user" ) ;
28
+ formData . append ( "roomId" , `default-room-${ agentId } ` ) ;
29
+
30
+ if ( selectedFile ) {
31
+ formData . append ( "file" , selectedFile ) ;
32
+ }
33
+
20
34
const res = await fetch ( `/api/${ agentId } /message` , {
21
35
method : "POST" ,
22
- headers : {
23
- "Content-Type" : "application/json" ,
24
- } ,
25
- body : JSON . stringify ( {
26
- text,
27
- userId : "user" ,
28
- roomId : `default-room-${ agentId } ` ,
29
- } ) ,
36
+ body : formData ,
30
37
} ) ;
31
38
return res . json ( ) as Promise < TextResponse [ ] > ;
32
39
} ,
33
40
onSuccess : ( data ) => {
34
41
setMessages ( ( prev ) => [ ...prev , ...data ] ) ;
42
+ setSelectedFile ( null ) ;
35
43
} ,
36
44
} ) ;
37
45
38
46
const handleSubmit = async ( e : React . FormEvent ) => {
39
47
e . preventDefault ( ) ;
40
- if ( ! input . trim ( ) ) return ;
48
+ if ( ! input . trim ( ) && ! selectedFile ) return ;
41
49
42
50
// Add user message immediately to state
43
51
const userMessage : TextResponse = {
44
52
text : input ,
45
53
user : "user" ,
54
+ attachments : selectedFile ? [ { url : URL . createObjectURL ( selectedFile ) , contentType : selectedFile . type , title : selectedFile . name } ] : undefined ,
46
55
} ;
47
56
setMessages ( ( prev ) => [ ...prev , userMessage ] ) ;
48
57
49
58
mutation . mutate ( input ) ;
50
59
setInput ( "" ) ;
51
60
} ;
52
61
62
+ const handleFileSelect = ( ) => {
63
+ fileInputRef . current ?. click ( ) ;
64
+ } ;
65
+
66
+ const handleFileChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
67
+ const file = e . target . files ?. [ 0 ] ;
68
+ if ( file && file . type . startsWith ( 'image/' ) ) {
69
+ setSelectedFile ( file ) ;
70
+ }
71
+ } ;
72
+
53
73
return (
54
74
< div className = "flex flex-col h-screen max-h-screen w-full" >
55
75
< div className = "flex-1 min-h-0 overflow-y-auto p-4" >
@@ -71,10 +91,23 @@ export default function Chat() {
71
91
: "bg-muted"
72
92
} `}
73
93
>
74
- < pre className = "whitespace-pre-wrap break-words font-sans m-0" >
75
- { message . text }
76
- </ pre >
77
- </ div >
94
+ { message . text }
95
+ { message . attachments ?. map ( ( attachment , i ) => (
96
+ attachment . contentType . startsWith ( 'image/' ) && (
97
+ < img
98
+ key = { i }
99
+ src = { message . user === "user"
100
+ ? attachment . url
101
+ : attachment . url . startsWith ( 'http' )
102
+ ? attachment . url
103
+ : `http://localhost:3000/media/generated/${ attachment . url . split ( '/' ) . pop ( ) } `
104
+ }
105
+ alt = { attachment . title || "Attached image" }
106
+ className = "mt-2 max-w-full rounded-lg"
107
+ />
108
+ )
109
+ ) ) }
110
+ </ div >
78
111
</ div >
79
112
) )
80
113
) : (
@@ -88,17 +121,38 @@ export default function Chat() {
88
121
< div className = "border-t p-4 bg-background" >
89
122
< div className = "max-w-3xl mx-auto" >
90
123
< form onSubmit = { handleSubmit } className = "flex gap-2" >
124
+ < input
125
+ type = "file"
126
+ ref = { fileInputRef }
127
+ onChange = { handleFileChange }
128
+ accept = "image/*"
129
+ className = "hidden"
130
+ />
91
131
< Input
92
132
value = { input }
93
133
onChange = { ( e ) => setInput ( e . target . value ) }
94
134
placeholder = "Type a message..."
95
135
className = "flex-1"
96
136
disabled = { mutation . isPending }
97
137
/>
138
+ < Button
139
+ type = "button"
140
+ variant = "outline"
141
+ size = "icon"
142
+ onClick = { handleFileSelect }
143
+ disabled = { mutation . isPending }
144
+ >
145
+ < ImageIcon className = "h-4 w-4" />
146
+ </ Button >
98
147
< Button type = "submit" disabled = { mutation . isPending } >
99
148
{ mutation . isPending ? "..." : "Send" }
100
149
</ Button >
101
150
</ form >
151
+ { selectedFile && (
152
+ < div className = "mt-2 text-sm text-muted-foreground" >
153
+ Selected file: { selectedFile . name }
154
+ </ div >
155
+ ) }
102
156
</ div >
103
157
</ div >
104
158
</ div >
0 commit comments