mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
feat: remove Assistant component and implement drag-and-drop file upload in ChatInput
This commit is contained in:
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user