feat: remove Assistant component and implement drag-and-drop file upload in ChatInput

This commit is contained in:
dayuan.jiang
2025-03-23 12:56:47 +00:00
parent 385a103982
commit 51d4536dbb
2 changed files with 51 additions and 53 deletions

View File

@@ -1,21 +0,0 @@
"use client";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
import { Thread } from "@/components/assistant-ui/thread";
import { ThreadList } from "@/components/assistant-ui/thread-list";
export const Assistant = () => {
const runtime = useChatRuntime({
api: "/api/chat",
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<div className="grid h-dvh grid-cols-[200px_1fr] gap-x-2 px-4 py-4">
<ThreadList />
<Thread />
</div>
</AssistantRuntimeProvider>
);
};

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useCallback, useRef, useEffect } from "react"; import React, { useCallback, useRef, useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Loader2, Send, RotateCcw, Image as ImageIcon, X } from "lucide-react"; import { Loader2, Send, RotateCcw, Image as ImageIcon, X } from "lucide-react";
@@ -35,6 +35,7 @@ export function ChatInput({
}: ChatInputProps) { }: ChatInputProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
// Auto-resize textarea based on content // Auto-resize textarea based on content
const adjustTextareaHeight = useCallback(() => { const adjustTextareaHeight = useCallback(() => {
@@ -49,34 +50,6 @@ export function ChatInput({
adjustTextareaHeight(); adjustTextareaHeight();
}, [input, adjustTextareaHeight]); }, [input, adjustTextareaHeight]);
// Handle clipboard paste events
const handlePaste = useCallback(
(e: React.ClipboardEvent) => {
if (!onFileChange) return;
const items = e.clipboardData.items;
const imageItems = Array.from(items).filter(
(item) => item.type.indexOf("image") !== -1
);
if (imageItems.length > 0) {
e.preventDefault();
// Convert clipboard image to File
const file = imageItems[0].getAsFile();
if (file) {
// Create a new FileList-like object
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Pass to the existing file handler
onFileChange(dataTransfer.files);
}
}
},
[onFileChange]
);
// Handle keyboard shortcuts // Handle keyboard shortcuts
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
@@ -110,8 +83,55 @@ export function ChatInput({
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
// Handle drag events
const handleDragOver = (e: React.DragEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (status === "streaming") return;
const droppedFiles = e.dataTransfer.files;
// Only process image files
if (droppedFiles.length > 0) {
const imageFiles = Array.from(droppedFiles).filter((file) =>
file.type.startsWith("image/")
);
if (imageFiles.length > 0 && onFileChange) {
// Create a new FileList-like object with only image files
const dt = new DataTransfer();
imageFiles.forEach((file) => dt.items.add(file));
onFileChange(dt.files);
}
}
};
return ( return (
<form onSubmit={onSubmit} className="w-full space-y-2"> <form
onSubmit={onSubmit}
className={`w-full space-y-2 ${
isDragging
? "border-2 border-dashed border-primary p-4 rounded-lg bg-muted/20"
: ""
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* File preview area */} {/* File preview area */}
{files && files.length > 0 && ( {files && files.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md"> <div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
@@ -135,7 +155,7 @@ export function ChatInput({
<button <button
type="button" type="button"
onClick={clearFiles} onClick={clearFiles}
className="absolute -top-2 -right-2 bg-destructive text-destructive-foreground rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity" className="absolute -top-2 -right-2 bg-destructive rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label="Remove file" aria-label="Remove file"
> >
<X className="h-3 w-3" /> <X className="h-3 w-3" />
@@ -150,7 +170,6 @@ export function ChatInput({
value={input} value={input}
onChange={onChange} onChange={onChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder="Describe what changes you want to make to the diagram... (Press Cmd/Ctrl + Enter to send)" placeholder="Describe what changes you want to make to the diagram... (Press Cmd/Ctrl + Enter to send)"
disabled={status === "streaming"} disabled={status === "streaming"}
aria-label="Chat input" aria-label="Chat input"