feat: add onDisplayChart prop to ChatInput and integrate with ChatPanel for diagram handling

This commit is contained in:
dayuan.jiang
2025-03-23 12:04:33 +00:00
parent 50dc4eda6d
commit 9faa75ec9b
2 changed files with 79 additions and 48 deletions

View File

@@ -1,19 +1,26 @@
"use client" "use client";
import React, { useCallback, useRef, useEffect } from "react" import React, { useCallback, useRef, useEffect } 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, Trash, Image as ImageIcon, X } from "lucide-react" import { Loader2, Send, RotateCcw, Image as ImageIcon, X } from "lucide-react";
import Image from "next/image" import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import Image from "next/image";
interface ChatInputProps { interface ChatInputProps {
input: string input: string;
status: "submitted" | "streaming" | "ready" | "error" status: "submitted" | "streaming" | "ready" | "error";
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
setMessages: (messages: any[]) => void setMessages: (messages: any[]) => void;
files?: FileList onDisplayChart: (xml: string) => void;
onFileChange?: (files: FileList | undefined) => void files?: FileList;
onFileChange?: (files: FileList | undefined) => void;
} }
export function ChatInput({ export function ChatInput({
@@ -22,57 +29,58 @@ export function ChatInput({
onSubmit, onSubmit,
onChange, onChange,
setMessages, setMessages,
onDisplayChart,
files, files,
onFileChange onFileChange,
}: ChatInputProps) { }: ChatInputProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null);
// Auto-resize textarea based on content // Auto-resize textarea based on content
const adjustTextareaHeight = useCallback(() => { const adjustTextareaHeight = useCallback(() => {
const textarea = textareaRef.current const textarea = textareaRef.current;
if (textarea) { if (textarea) {
textarea.style.height = "auto" textarea.style.height = "auto";
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px` textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
adjustTextareaHeight() adjustTextareaHeight();
}, [input, adjustTextareaHeight]) }, [input, adjustTextareaHeight]);
// 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") {
e.preventDefault() e.preventDefault();
const form = e.currentTarget.closest("form") const form = e.currentTarget.closest("form");
if (form && input.trim() && status !== "streaming") { if (form && input.trim() && status !== "streaming") {
form.requestSubmit() form.requestSubmit();
}
} }
} }
};
// Handle file changes // Handle file changes
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (onFileChange) { if (onFileChange) {
onFileChange(e.target.files || undefined) onFileChange(e.target.files || undefined);
}
} }
};
// Clear file selection // Clear file selection
const clearFiles = () => { const clearFiles = () => {
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = '' fileInputRef.current.value = "";
} }
if (onFileChange) { if (onFileChange) {
onFileChange(undefined) onFileChange(undefined);
}
} }
};
// Trigger file input click // Trigger file input click
const triggerFileInput = () => { const triggerFileInput = () => {
fileInputRef.current?.click() fileInputRef.current?.click();
} };
return ( return (
<form onSubmit={onSubmit} className="w-full space-y-2"> <form onSubmit={onSubmit} className="w-full space-y-2">
@@ -82,7 +90,7 @@ export function ChatInput({
{Array.from(files).map((file, index) => ( {Array.from(files).map((file, index) => (
<div key={index} className="relative group"> <div key={index} className="relative group">
<div className="w-20 h-20 border rounded-md overflow-hidden bg-muted"> <div className="w-20 h-20 border rounded-md overflow-hidden bg-muted">
{file.type.startsWith('image/') ? ( {file.type.startsWith("image/") ? (
<Image <Image
src={URL.createObjectURL(file)} src={URL.createObjectURL(file)}
alt={file.name} alt={file.name}
@@ -120,19 +128,37 @@ export function ChatInput({
className="min-h-[80px] resize-none transition-all duration-200" className="min-h-[80px] resize-none transition-all duration-200"
/> />
<div className="flex justify-between gap-2"> <div className="flex items-center gap-2">
<div className="flex gap-2"> <div className="mr-auto">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button <Button
type="button" type="button"
variant="outline" variant="ghost"
size="default" size="icon"
onClick={() => setMessages([])} onClick={() => {
title="Clear messages" setMessages([]);
onDisplayChart(`<mxfile host="embed.diagrams.net" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" version="26.1.1">
<diagram name="Page-1" id="NsivuNt5aJDXaP8udwGv">
<mxGraphModel dx="394" dy="700" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
</root>
</mxGraphModel>
</diagram>
</mxfile>`);
}}
> >
<Trash className="mr-2 h-4 w-4" /> <RotateCcw className="mr-2 h-4 w-4" />
Start a new conversation
</Button> </Button>
</TooltipTrigger>
<TooltipContent>
Clear current conversation and diagram
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex gap-2">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -159,7 +185,11 @@ export function ChatInput({
type="submit" type="submit"
disabled={status === "streaming" || !input.trim()} disabled={status === "streaming" || !input.trim()}
className="transition-opacity" className="transition-opacity"
aria-label={status === "streaming" ? "Sending message..." : "Send message"} aria-label={
status === "streaming"
? "Sending message..."
: "Send message"
}
> >
{status === "streaming" ? ( {status === "streaming" ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
@@ -170,5 +200,5 @@ export function ChatInput({
</Button> </Button>
</div> </div>
</form> </form>
) );
} }

View File

@@ -225,6 +225,7 @@ export default function ChatPanel({ onDisplayChart, onFetchChart }: ChatPanelPro
onSubmit={onFormSubmit} onSubmit={onFormSubmit}
onChange={handleInputChange} onChange={handleInputChange}
setMessages={setMessages} setMessages={setMessages}
onDisplayChart={onDisplayChart}
files={files} files={files}
onFileChange={handleFileChange} onFileChange={handleFileChange}
/> />