Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Here are some example prompts and their generated diagrams:
- **Interactive Chat Interface**: Communicate with AI to refine your diagrams in real-time
- **Cloud Architecture Diagram Support**: Specialized support for generating cloud architecture diagrams (AWS, GCP, Azure)
- **Animated Connectors**: Create dynamic and animated connectors between diagram elements for better visualization
- **Structurizr DSL Export**: Export C4 model diagrams to [Structurizr DSL](https://structurizr.com/) format for use with Structurizr Lite, CLI, or cloud service - perfect for architecture documentation and code-as-diagrams workflows

## MCP Server (Preview)

Expand Down
22 changes: 22 additions & 0 deletions components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
Download,
FileCode,
History,
Image as ImageIcon,
Loader2,
Expand All @@ -13,6 +14,7 @@ import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner"
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
import { ErrorToast } from "@/components/error-toast"
import { ExportStructurizrDialog } from "@/components/export-structurizr-dialog"
import { HistoryDialog } from "@/components/history-dialog"
import { ResetWarningModal } from "@/components/reset-warning-modal"
import { SaveDialog } from "@/components/save-dialog"
Expand Down Expand Up @@ -160,11 +162,13 @@ export function ChatInput({
saveDiagramToFile,
showSaveDialog,
setShowSaveDialog,
chartXML,
} = useDiagram()
const textareaRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const [isDragging, setIsDragging] = useState(false)
const [showClearDialog, setShowClearDialog] = useState(false)
const [showExportDslDialog, setShowExportDslDialog] = useState(false)

// Allow retry when there's an error (even if status is still "streaming" or "submitted")
const isDisabled =
Expand Down Expand Up @@ -399,6 +403,18 @@ export function ChatInput({
<History className="h-4 w-4" />
</ButtonWithTooltip>

<ButtonWithTooltip
type="button"
variant="ghost"
size="sm"
onClick={() => setShowExportDslDialog(true)}
disabled={isDisabled || !chartXML}
tooltipContent="Export to Structurizr DSL"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<FileCode className="h-4 w-4" />
</ButtonWithTooltip>

<ButtonWithTooltip
type="button"
variant="ghost"
Expand All @@ -422,6 +438,12 @@ export function ChatInput({
.slice(0, 10)}`}
/>

<ExportStructurizrDialog
open={showExportDslDialog}
onOpenChange={setShowExportDslDialog}
xml={chartXML}
/>

<ButtonWithTooltip
type="button"
variant="ghost"
Expand Down
127 changes: 127 additions & 0 deletions components/export-structurizr-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"use client"

import { Check, Copy, Download } from "lucide-react"
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { ScrollArea } from "@/components/ui/scroll-area"
import { convertToStructurizrDsl } from "@/lib/structurizr-utils"

interface ExportStructurizrDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
xml: string
}

export function ExportStructurizrDialog({
open,
onOpenChange,
xml,
}: ExportStructurizrDialogProps) {
const [dsl, setDsl] = useState("")
const [copied, setCopied] = useState(false)
const [error, setError] = useState("")

useEffect(() => {
if (open && xml) {
try {
const result = convertToStructurizrDsl(xml)
setDsl(result)
setError("")
} catch (err) {
setError("Failed to convert diagram to DSL format")
setDsl("")
console.error("DSL conversion error:", err)
}
}
}, [open, xml])
Comment on lines +32 to +44
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copied state is not reset when the dialog is closed or when the xml prop changes. If a user copies the DSL, closes the dialog, then reopens it (either with the same or different diagram), the button will still show "Copied" for up to 2 seconds. This creates a confusing UX where the button state doesn't match the actual clipboard content.

Consider resetting the copied state when the dialog opens or when the xml/dsl changes.

Copilot uses AI. Check for mistakes.

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(dsl)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
Comment on lines +46 to +50
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout for resetting the copied state is not cleaned up when the component unmounts or when the dialog is closed. If a user clicks copy and then closes the dialog before the 2-second timeout completes, the timeout will still fire and attempt to update the state of an unmounted component, which can lead to memory leaks and React warnings.

Store the timeout ID in a ref and clear it in a cleanup function or when the dialog closes.

Copilot uses AI. Check for mistakes.
} catch (err) {
console.error("Failed to copy to clipboard:", err)
}
}

const handleDownload = () => {
const blob = new Blob([dsl], { type: "text/plain" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "diagram.dsl"
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL created by URL.createObjectURL should be revoked after the download completes to prevent memory leaks. Looking at the similar implementation in contexts/diagram-context.tsx (lines 306-316), there's a pattern of revoking the URL with a timeout. The current implementation creates the blob URL but never revokes it, which can lead to memory leaks if users export DSL multiple times during a session.

Suggested change
URL.revokeObjectURL(url)
setTimeout(() => {
URL.revokeObjectURL(url)
}, 1000)

Copilot uses AI. Check for mistakes.
}

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Export to Structurizr DSL</DialogTitle>
<DialogDescription>
C4 model diagram as code. Use with Structurizr Lite,
CLI, or cloud service.
</DialogDescription>
</DialogHeader>

{error ? (
<div className="rounded-md bg-destructive/10 p-4 text-sm text-destructive">
{error}
</div>
) : (
<div className="space-y-4">
<ScrollArea className="h-[400px] w-full rounded-md border">
<pre className="p-4 text-xs font-mono">
<code>{dsl}</code>
</pre>
</ScrollArea>
</div>
)}
Comment on lines +79 to +91
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error state blocks display of action buttons, making it impossible for users to close the dialog using the footer buttons when an error occurs. Users would have to use the X button or press Escape. This is inconsistent with the "Close" button being visible when DSL content is successfully generated.

Consider displaying the action buttons even in the error state, or ensure the error message clearly indicates how to dismiss the dialog.

Copilot uses AI. Check for mistakes.

<DialogFooter className="sm:justify-between">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
>
Close
</Button>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleCopy}
disabled={!dsl}
>
{copied ? (
<>
<Check className="w-4 h-4 mr-2" />
Copied
</>
) : (
<>
<Copy className="w-4 h-4 mr-2" />
Copy
</>
)}
</Button>
<Button onClick={handleDownload} disabled={!dsl}>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Loading