mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
feat: add confirmation dialog for clearing conversation and diagram in ChatInput
This commit is contained in:
@@ -10,6 +10,15 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import Image from "next/image";
|
||||
|
||||
interface ChatInputProps {
|
||||
@@ -36,6 +45,7 @@ export function ChatInput({
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [showClearDialog, setShowClearDialog] = useState(false);
|
||||
|
||||
// Auto-resize textarea based on content
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
@@ -120,6 +130,20 @@ export function ChatInput({
|
||||
}
|
||||
};
|
||||
|
||||
// Handle clearing conversation and diagram
|
||||
const handleClear = () => {
|
||||
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>`);
|
||||
setShowClearDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
@@ -185,17 +209,7 @@ export function ChatInput({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
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>`);
|
||||
}}
|
||||
onClick={() => setShowClearDialog(true)}
|
||||
>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
</Button>
|
||||
@@ -205,6 +219,37 @@ export function ChatInput({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
{/* Warning Modal */}
|
||||
<Dialog
|
||||
open={showClearDialog}
|
||||
onOpenChange={setShowClearDialog}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Clear Everything?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will clear the current conversation and
|
||||
reset the diagram. This action cannot be
|
||||
undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowClearDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleClear}
|
||||
>
|
||||
Clear Everything
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
|
||||
@@ -30,7 +30,6 @@ export default function ChatPanel({
|
||||
// Add state for file attachments
|
||||
const [files, setFiles] = useState<FileList | undefined>(undefined);
|
||||
// Add state to control visibility of prompt examples panel
|
||||
const [showExamples, setShowExamples] = useState<boolean>(true);
|
||||
|
||||
// Remove the currentXmlRef and related useEffect
|
||||
const {
|
||||
@@ -73,7 +72,6 @@ export default function ChatPanel({
|
||||
if (input.trim() && status !== "streaming") {
|
||||
try {
|
||||
// Hide examples panel after sending a message
|
||||
setShowExamples(false);
|
||||
|
||||
// Fetch chart data before setting input
|
||||
const chartXml = await onFetchChart();
|
||||
@@ -149,7 +147,7 @@ export default function ChatPanel({
|
||||
// Determine whether to show details based on a simple threshold
|
||||
// Rather than comparing lengths (which can cause re-renders)
|
||||
if (
|
||||
stepCounterRef.current >= 20 &&
|
||||
stepCounterRef.current >= 50 &&
|
||||
stepCounterRef.current % 20 === 0
|
||||
) {
|
||||
onDisplayChart(convertToLegalXml(currentXml));
|
||||
@@ -215,6 +213,34 @@ export default function ChatPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const examplePanel = (
|
||||
<div className="px-4 py-2 border-t border-b border-gray-100">
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
{" "}
|
||||
Start a conversation to generate or modify diagrams.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
{" "}
|
||||
You can also upload images to use as references.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mb-2">Try these examples:</p>
|
||||
<div className="flex flex-wrap gap-5">
|
||||
<button
|
||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
||||
onClick={handleReplicateFlowchart}
|
||||
>
|
||||
Replicate this flowchart
|
||||
</button>
|
||||
<button
|
||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
||||
onClick={() => setInput("Draw a cat for me")}
|
||||
>
|
||||
Draw a cat for me
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col rounded-none py-0">
|
||||
<CardHeader className="p-4 text-center">
|
||||
@@ -222,106 +248,95 @@ export default function ChatPanel({
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow overflow-hidden p-4">
|
||||
<ScrollArea className="h-full pr-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center text-gray-500 mt-8">
|
||||
<p>
|
||||
Start a conversation to generate or modify
|
||||
diagrams.
|
||||
</p>
|
||||
<p className="text-sm mt-2">
|
||||
Try: "Create a flowchart for user
|
||||
authentication"
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`mb-4 ${
|
||||
message.role === "user"
|
||||
? "text-right"
|
||||
: "text-left"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`inline-block px-4 py-2 whitespace-pre-wrap text-sm rounded-lg max-w-[85%] break-words ${
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{/* Render message content based on parts if available */}
|
||||
{message.parts
|
||||
? message.parts.map((part, index) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<div key={index}>
|
||||
{part.text}
|
||||
</div>
|
||||
);
|
||||
case "tool-invocation":
|
||||
return renderToolInvocation(
|
||||
part.toolInvocation
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
{messages.length === 0
|
||||
? examplePanel
|
||||
: messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`mb-4 ${
|
||||
message.role === "user"
|
||||
? "text-right"
|
||||
: "text-left"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`inline-block px-4 py-2 whitespace-pre-wrap text-sm rounded-lg max-w-[85%] break-words ${
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{/* Render message content based on parts if available */}
|
||||
{message.parts
|
||||
? message.parts.map((part, index) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<div key={index}>
|
||||
{part.text}
|
||||
</div>
|
||||
);
|
||||
case "tool-invocation":
|
||||
return renderToolInvocation(
|
||||
part.toolInvocation
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
: // Fallback to simple content for older format
|
||||
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">
|
||||
<div className="text-xs text-gray-500">
|
||||
Using tool:{" "}
|
||||
{
|
||||
(message as any).function_call
|
||||
.name
|
||||
}
|
||||
})
|
||||
: // Fallback to simple content for older format
|
||||
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">
|
||||
<div className="text-xs text-gray-500">
|
||||
Using tool:{" "}
|
||||
{
|
||||
(message as any).function_call
|
||||
.name
|
||||
}
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm mt-2">
|
||||
Error: {error.message}
|
||||
@@ -331,29 +346,6 @@ export default function ChatPanel({
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
|
||||
{/* Conditionally render Prompt Examples Panel */}
|
||||
{showExamples && (
|
||||
<div className="px-4 py-2 border-t border-b border-gray-100">
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Try these examples:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
||||
onClick={() => setInput("Draw a cat for me.")}
|
||||
>
|
||||
Draw a cat for me.
|
||||
</button>
|
||||
<button
|
||||
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-2 rounded"
|
||||
onClick={handleReplicateFlowchart}
|
||||
>
|
||||
Replicate this flowchart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardFooter className="p-2">
|
||||
<ChatInput
|
||||
input={input}
|
||||
|
||||
135
components/ui/dialog.tsx
Normal file
135
components/ui/dialog.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
Reference in New Issue
Block a user