feat: add save diagram to local file button (#60)

- Add save button in chat input area with download icon
- Create SaveDialog component for filename input
- Export current diagram as .drawio file format
- Support custom filename with default timestamp-based name

Closes #53
This commit is contained in:
Dayuan Jiang
2025-12-03 21:02:26 +09:00
committed by GitHub
parent a61d37c818
commit 45f74df349
3 changed files with 143 additions and 1 deletions

View File

@@ -4,12 +4,14 @@ import React, { useCallback, useRef, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { ResetWarningModal } from "@/components/reset-warning-modal";
import { SaveDialog } from "@/components/save-dialog";
import {
Loader2,
Send,
RotateCcw,
Image as ImageIcon,
History,
Download,
} from "lucide-react";
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
import { FilePreviewList } from "./file-preview-list";
@@ -39,11 +41,12 @@ export function ChatInput({
showHistory = false,
onToggleHistory = () => {},
}: ChatInputProps) {
const { diagramHistory } = useDiagram();
const { diagramHistory, saveDiagramToFile } = useDiagram();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [showClearDialog, setShowClearDialog] = useState(false);
const [showSaveDialog, setShowSaveDialog] = useState(false);
// Debug: Log status changes
const isDisabled = status === "streaming" || status === "submitted";
@@ -166,6 +169,7 @@ export function ChatInput({
setShowClearDialog(false);
};
return (
<form
onSubmit={onSubmit}
@@ -235,6 +239,25 @@ export function ChatInput({
<History className="h-4 w-4" />
</ButtonWithTooltip>
{/* Save Diagram Button */}
<ButtonWithTooltip
type="button"
variant="outline"
size="icon"
onClick={() => setShowSaveDialog(true)}
disabled={isDisabled}
tooltipContent="Save diagram to local file"
>
<Download className="h-4 w-4" />
</ButtonWithTooltip>
<SaveDialog
open={showSaveDialog}
onOpenChange={setShowSaveDialog}
onSave={saveDiagramToFile}
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
/>
<Button
type="button"
variant="outline"

View File

@@ -0,0 +1,80 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
interface SaveDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave: (filename: string) => void;
defaultFilename: string;
}
export function SaveDialog({
open,
onOpenChange,
onSave,
defaultFilename,
}: SaveDialogProps) {
const [filename, setFilename] = useState(defaultFilename);
useEffect(() => {
if (open) {
setFilename(defaultFilename);
}
}, [open, defaultFilename]);
const handleSave = () => {
const finalFilename = filename.trim() || defaultFilename;
onSave(finalFilename);
onOpenChange(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
handleSave();
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Save Diagram</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<label className="text-sm font-medium">Filename</label>
<div className="flex items-stretch">
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter filename"
autoFocus
onFocus={(e) => e.target.select()}
className="rounded-r-none border-r-0 focus-visible:z-10"
/>
<span className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-sm text-muted-foreground font-mono">
.drawio
</span>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}