Skip to content

Commit 3fb84d2

Browse files
committed
Support image uploads in chat input
1 parent a57a089 commit 3fb84d2

File tree

3 files changed

+73
-13
lines changed

3 files changed

+73
-13
lines changed

components/chat/input.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
ArrowUpIcon,
66
Loader2Icon,
77
MicIcon,
8+
PaperclipIcon,
89
PauseIcon,
10+
UploadIcon,
911
XIcon,
1012
} from "lucide-react";
1113
import { useEffect, useRef, useState } from "react";
@@ -41,6 +43,7 @@ export type Props = {
4143
onStopRecord: () => void;
4244
attachments: Attachment[];
4345
onRemoveAttachment: (attachment: Attachment) => void;
46+
onAddAttachment: (newAttachments: Attachment[]) => void;
4447
};
4548

4649
export const ChatInput = ({
@@ -53,12 +56,44 @@ export const ChatInput = ({
5356
onStopRecord,
5457
attachments,
5558
onRemoveAttachment,
59+
onAddAttachment,
5660
}: Props) => {
5761
const inputRef = useRef<HTMLTextAreaElement>(null);
5862
const { onKeyDown } = useEnterSubmit({
5963
onSubmit,
6064
});
6165
const [model, setModel] = useState<Models>(getSettings().model);
66+
const fileInputRef = useRef<HTMLInputElement>(null);
67+
68+
const handleFileUpload = () => {
69+
fileInputRef.current?.click();
70+
};
71+
72+
const convertToBase64 = (file: File): Promise<string> => {
73+
return new Promise((resolve, reject) => {
74+
const reader = new FileReader();
75+
reader.readAsDataURL(file);
76+
reader.onload = () => resolve(reader.result as string);
77+
reader.onerror = (error) => reject(error);
78+
});
79+
};
80+
81+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
82+
if (e.target.files) {
83+
const filesArray = Array.from(e.target.files);
84+
const attachmentsPromises = filesArray.map(async (file) => {
85+
const base64 = await convertToBase64(file);
86+
return {
87+
url: base64,
88+
name: file.name,
89+
contentType: file.type,
90+
} as Attachment;
91+
});
92+
93+
const newAttachments = await Promise.all(attachmentsPromises);
94+
onAddAttachment(newAttachments);
95+
}
96+
};
6297

6398
useEffect(() => {
6499
if (inputRef.current) {
@@ -107,6 +142,23 @@ export const ChatInput = ({
107142
onChange={(e) => setInput(e.target.value)}
108143
/>
109144

145+
<input
146+
type="file"
147+
accept="image/*"
148+
multiple
149+
ref={fileInputRef}
150+
style={{ display: "none" }}
151+
onChange={handleFileChange}
152+
/>
153+
<Button
154+
variant="outline"
155+
size="icon"
156+
className="w-8 h-8 bg-transparent"
157+
onClick={handleFileUpload}
158+
>
159+
<PaperclipIcon className="w-4 h-4" />
160+
</Button>
161+
110162
<TooltipProvider>
111163
<Tooltip>
112164
<TooltipTrigger asChild>
@@ -116,7 +168,8 @@ export const ChatInput = ({
116168
recording ? onStopRecord() : onStartRecord();
117169
}}
118170
size="icon"
119-
className="w-8 h-8 disabled:pointer-events-auto"
171+
variant="outline"
172+
className="w-8 h-8 bg-transparent disabled:pointer-events-auto"
120173
>
121174
{recording ? (
122175
<PauseIcon className="w-4 h-4" />

components/chat/panel.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ export const ChatPanel = ({ id }: Props) => {
156156
});
157157
};
158158

159+
const handleAddAttachment: ChatInputProps["onAddAttachment"] = (
160+
newAttachments
161+
) => {
162+
setAttachments((prev) => [...prev, ...newAttachments]);
163+
};
164+
159165
const handleRemoveAttachment: ChatInputProps["onRemoveAttachment"] = (
160166
attachment
161167
) => {
@@ -227,6 +233,7 @@ export const ChatPanel = ({ id }: Props) => {
227233
onStartRecord={startRecording}
228234
onStopRecord={stopRecording}
229235
attachments={attachments}
236+
onAddAttachment={handleAddAttachment}
230237
onRemoveAttachment={handleRemoveAttachment}
231238
/>
232239
</div>

components/ui/button.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import * as React from "react"
2-
import { Slot } from "@radix-ui/react-slot"
3-
import { cva, type VariantProps } from "class-variance-authority"
1+
import * as React from "react";
2+
import { Slot } from "@radix-ui/react-slot";
3+
import { cva, type VariantProps } from "class-variance-authority";
44

5-
import { cn } from "@/lib/utils"
5+
import { cn } from "@/lib/utils";
66

77
const buttonVariants = cva(
8-
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
8+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed",
99
{
1010
variants: {
1111
variant: {
@@ -31,26 +31,26 @@ const buttonVariants = cva(
3131
size: "default",
3232
},
3333
}
34-
)
34+
);
3535

3636
export interface ButtonProps
3737
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
3838
VariantProps<typeof buttonVariants> {
39-
asChild?: boolean
39+
asChild?: boolean;
4040
}
4141

4242
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
4343
({ className, variant, size, asChild = false, ...props }, ref) => {
44-
const Comp = asChild ? Slot : "button"
44+
const Comp = asChild ? Slot : "button";
4545
return (
4646
<Comp
4747
className={cn(buttonVariants({ variant, size, className }))}
4848
ref={ref}
4949
{...props}
5050
/>
51-
)
51+
);
5252
}
53-
)
54-
Button.displayName = "Button"
53+
);
54+
Button.displayName = "Button";
5555

56-
export { Button, buttonVariants }
56+
export { Button, buttonVariants };

0 commit comments

Comments
 (0)