mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
feat: enhance ChatPanel and ChatInput to support image file uploads and display
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
import React, { useCallback, useRef, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Loader2, Send, Trash } from "lucide-react"
|
||||
import { Loader2, Send, Trash, Image as ImageIcon, X } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
|
||||
interface ChatInputProps {
|
||||
input: string
|
||||
@@ -11,10 +12,21 @@ interface ChatInputProps {
|
||||
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
setMessages: (messages: any[]) => void
|
||||
files?: FileList
|
||||
onFileChange?: (files: FileList | undefined) => void
|
||||
}
|
||||
|
||||
export function ChatInput({ input, status, onSubmit, onChange, setMessages }: ChatInputProps) {
|
||||
export function ChatInput({
|
||||
input,
|
||||
status,
|
||||
onSubmit,
|
||||
onChange,
|
||||
setMessages,
|
||||
files,
|
||||
onFileChange
|
||||
}: ChatInputProps) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Auto-resize textarea based on content
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
@@ -40,8 +52,63 @@ export function ChatInput({ input, status, onSubmit, onChange, setMessages }: Ch
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file changes
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (onFileChange) {
|
||||
onFileChange(e.target.files || undefined)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear file selection
|
||||
const clearFiles = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
if (onFileChange) {
|
||||
onFileChange(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger file input click
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="w-full space-y-2">
|
||||
{/* File preview area */}
|
||||
{files && files.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
|
||||
{Array.from(files).map((file, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div className="w-20 h-20 border rounded-md overflow-hidden bg-muted">
|
||||
{file.type.startsWith('image/') ? (
|
||||
<Image
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-xs text-center p-1">
|
||||
{file.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
aria-label="Remove file"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
@@ -52,17 +119,42 @@ export function ChatInput({ input, status, onSubmit, onChange, setMessages }: Ch
|
||||
aria-label="Chat input"
|
||||
className="min-h-[80px] resize-none transition-all duration-200"
|
||||
/>
|
||||
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={() => setMessages([])}
|
||||
title="Clear messages"
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Start a new conversation
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={() => setMessages([])}
|
||||
title="Clear messages"
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Start a new conversation
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={triggerFileInput}
|
||||
disabled={status === "streaming"}
|
||||
title="Upload image"
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
multiple
|
||||
disabled={status === "streaming"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={status === "streaming" || !input.trim()}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type React from "react"
|
||||
import { useRef, useEffect, useState } from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
@@ -17,6 +18,9 @@ interface ChatPanelProps {
|
||||
export default function ChatPanel({ onDisplayChart, onFetchChart }: ChatPanelProps) {
|
||||
// Add a step counter to track updates
|
||||
const stepCounterRef = useRef<number>(0);
|
||||
// Add state for file attachments
|
||||
const [files, setFiles] = useState<FileList | undefined>(undefined);
|
||||
|
||||
// Remove the currentXmlRef and related useEffect
|
||||
const { messages, input, handleInputChange, handleSubmit, status, error, setInput, setMessages, data } = useChat({
|
||||
maxSteps: 5,
|
||||
@@ -63,13 +67,23 @@ export default function ChatPanel({ onDisplayChart, onFetchChart }: ChatPanelPro
|
||||
"""
|
||||
`
|
||||
)
|
||||
handleSubmit(e)
|
||||
handleSubmit(e, {
|
||||
experimental_attachments: files,
|
||||
})
|
||||
|
||||
// Clear files after submission
|
||||
setFiles(undefined);
|
||||
} catch (error) {
|
||||
console.error("Error fetching chart data:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to handle file changes
|
||||
const handleFileChange = (newFiles: FileList | undefined) => {
|
||||
setFiles(newFiles);
|
||||
}
|
||||
|
||||
// Helper function to render tool invocations
|
||||
const renderToolInvocation = (toolInvocation: any) => {
|
||||
const callId = toolInvocation.toolCallId;
|
||||
@@ -166,6 +180,25 @@ export default function ChatPanel({ onDisplayChart, onFetchChart }: ChatPanelPro
|
||||
message.content
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Display image attachments */}
|
||||
{message?.experimental_attachments?.filter(attachment =>
|
||||
attachment?.contentType?.startsWith('image/')
|
||||
).map((attachment, index) => (
|
||||
<div key={`${message.id}-${index}`} className={`mt-2 ${message.role === "user" ? "text-right" : "text-left"}`}>
|
||||
<div className="inline-block">
|
||||
<Image
|
||||
src={attachment.url}
|
||||
width={200}
|
||||
height={200}
|
||||
alt={attachment.name ?? `attachment-${index}`}
|
||||
className="rounded-md border"
|
||||
style={{ objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Legacy support for function_call format */}
|
||||
{(message as any).function_call && (
|
||||
<div className="mt-2 text-left">
|
||||
@@ -192,6 +225,8 @@ export default function ChatPanel({ onDisplayChart, onFetchChart }: ChatPanelPro
|
||||
onSubmit={onFormSubmit}
|
||||
onChange={handleInputChange}
|
||||
setMessages={setMessages}
|
||||
files={files}
|
||||
onFileChange={handleFileChange}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user