2025-03-23 12:48:31 +00:00
"use client" ;
2025-03-19 07:20:22 +00:00
2025-03-23 12:48:31 +00:00
import type React from "react" ;
import { useRef , useEffect , useState } from "react" ;
2025-12-04 22:56:59 +09:00
import { flushSync } from "react-dom" ;
2025-04-03 15:29:26 +00:00
import { FaGithub } from "react-icons/fa" ;
2025-12-05 22:42:39 +09:00
import {
PanelRightClose ,
PanelRightOpen ,
Settings ,
CheckCircle ,
} from "lucide-react" ;
feat: Separate SEO content to /about page (best practice)
- Remove header from main page for clean editor-only interface
- Create /app/about/page.tsx with comprehensive SEO content (1000+ words)
- Add About link next to 'Next-AI-Drawio' title in chat panel
- Add GitHub icon link to /about page navigation
- Update sitemap.ts to include /about page (priority: 0.8)
SEO improvements following industry best practices:
- Separate marketing content from app interface (Figma/Canva/Miro approach)
- Server-rendered /about page for optimal crawlability
- Clean URL structure for better internal linking
- Multiple indexable pages for broader keyword coverage
- Proper semantic HTML: H1, H2, H3, article, section tags
- 1000+ words of keyword-rich content
/about page includes:
- AI diagram generator overview with value proposition
- 6 detailed feature sections (AI creation, AWS diagrams, image replication, etc.)
- 3 popular use cases (AWS architecture, flowcharts, system design)
- Step-by-step usage guide (4 steps)
- Benefits section (save time, precision, free, privacy)
- Clear call-to-action with link back to editor
- GitHub link in navigation for social proof
This follows Google-approved architecture and avoids hidden content penalties.
2025-11-16 09:04:01 +09:00
import Link from "next/link" ;
2025-12-03 21:49:34 +09:00
import Image from "next/image" ;
2025-03-19 08:16:44 +00:00
2025-03-23 12:48:31 +00:00
import { useChat } from "@ai-sdk/react" ;
2025-08-31 12:54:14 +09:00
import { DefaultChatTransport } from "ai" ;
2025-03-23 12:48:31 +00:00
import { ChatInput } from "@/components/chat-input" ;
2025-03-25 02:58:11 +00:00
import { ChatMessageDisplay } from "./chat-message-display" ;
2025-03-26 00:30:00 +00:00
import { useDiagram } from "@/contexts/diagram-context" ;
2025-12-03 16:14:53 +09:00
import { replaceNodes , formatXML , validateMxCellStructure } from "@/lib/utils" ;
2025-11-15 12:09:32 +09:00
import { ButtonWithTooltip } from "@/components/button-with-tooltip" ;
2025-12-05 19:30:50 +09:00
import { Toaster } from "sonner" ;
2025-12-05 22:42:39 +09:00
import {
SettingsDialog ,
STORAGE_ACCESS_CODE_KEY ,
} from "@/components/settings-dialog" ;
2025-03-26 00:30:00 +00:00
2025-11-15 12:09:32 +09:00
interface ChatPanelProps {
isVisible : boolean ;
onToggleVisibility : ( ) = > void ;
2025-12-05 23:10:48 +09:00
drawioUi : "min" | "sketch" ;
onToggleDrawioUi : ( ) = > void ;
2025-11-15 12:09:32 +09:00
}
2025-12-03 16:47:45 +09:00
export default function ChatPanel ( {
isVisible ,
onToggleVisibility ,
2025-12-05 23:10:48 +09:00
drawioUi ,
onToggleDrawioUi ,
2025-12-03 16:47:45 +09:00
} : ChatPanelProps ) {
2025-03-26 00:30:00 +00:00
const {
loadDiagram : onDisplayChart ,
handleExport : onExport ,
2025-12-03 21:58:48 +09:00
handleExportWithoutHistory ,
2025-03-26 00:30:00 +00:00
resolverRef ,
2025-04-03 15:10:53 +00:00
chartXML ,
2025-03-27 08:09:22 +00:00
clearDiagram ,
2025-03-26 00:30:00 +00:00
} = useDiagram ( ) ;
2025-03-19 08:16:44 +00:00
2025-12-03 21:58:48 +09:00
const onFetchChart = ( saveToHistory = true ) = > {
2025-11-10 10:28:37 +09:00
return Promise . race ( [
new Promise < string > ( ( resolve ) = > {
if ( resolverRef && "current" in resolverRef ) {
resolverRef . current = resolve ;
}
2025-12-03 21:58:48 +09:00
if ( saveToHistory ) {
onExport ( ) ;
} else {
handleExportWithoutHistory ( ) ;
}
2025-11-10 10:28:37 +09:00
} ) ,
new Promise < string > ( ( _ , reject ) = >
2025-12-03 16:47:45 +09:00
setTimeout (
( ) = >
reject (
new Error ( "Chart export timed out after 10 seconds" )
) ,
10000
)
) ,
2025-11-10 10:28:37 +09:00
] ) ;
2025-03-26 00:30:00 +00:00
} ;
2025-03-25 04:23:38 +00:00
2025-03-27 08:02:03 +00:00
const [ files , setFiles ] = useState < File [ ] > ( [ ] ) ;
2025-03-23 13:54:21 +00:00
const [ showHistory , setShowHistory ] = useState ( false ) ;
2025-12-05 21:09:34 +08:00
const [ showSettingsDialog , setShowSettingsDialog ] = useState ( false ) ;
const [ accessCodeRequired , setAccessCodeRequired ] = useState ( false ) ;
2025-08-31 12:54:14 +09:00
const [ input , setInput ] = useState ( "" ) ;
2025-12-05 21:09:34 +08:00
// Check if access code is required on mount
useEffect ( ( ) = > {
fetch ( "/api/config" )
. then ( ( res ) = > res . json ( ) )
. then ( ( data ) = > setAccessCodeRequired ( data . accessCodeRequired ) )
. catch ( ( ) = > setAccessCodeRequired ( false ) ) ;
} , [ ] ) ;
2025-12-05 21:15:02 +09:00
// Generate a unique session ID for Langfuse tracing
2025-12-05 22:42:39 +09:00
const [ sessionId , setSessionId ] = useState (
( ) = > ` session- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 9 ) } `
) ;
2025-12-05 21:15:02 +09:00
2025-12-04 22:56:59 +09:00
// Store XML snapshots for each user message (keyed by message index)
const xmlSnapshotsRef = useRef < Map < number , string > > ( new Map ( ) ) ;
2025-12-05 00:47:27 +09:00
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
const chartXMLRef = useRef ( chartXML ) ;
useEffect ( ( ) = > {
chartXMLRef . current = chartXML ;
} , [ chartXML ] ) ;
2025-12-05 22:42:39 +09:00
const { messages , sendMessage , addToolResult , status , error , setMessages } =
useChat ( {
transport : new DefaultChatTransport ( {
api : "/api/chat" ,
} ) ,
async onToolCall ( { toolCall } ) {
if ( toolCall . toolName === "display_diagram" ) {
const { xml } = toolCall . input as { xml : string } ;
2025-12-05 16:46:17 +09:00
2025-12-05 22:42:39 +09:00
const validationError = validateMxCellStructure ( xml ) ;
2025-12-05 16:46:17 +09:00
2025-12-05 22:42:39 +09:00
if ( validationError ) {
addToolResult ( {
tool : "display_diagram" ,
toolCallId : toolCall.toolCallId ,
output : validationError ,
} ) ;
} else {
addToolResult ( {
tool : "display_diagram" ,
toolCallId : toolCall.toolCallId ,
output : "Successfully displayed the diagram." ,
} ) ;
}
} else if ( toolCall . toolName === "edit_diagram" ) {
const { edits } = toolCall . input as {
edits : Array < { search : string ; replace : string } > ;
} ;
let currentXml = "" ;
try {
console . log ( "[edit_diagram] Starting..." ) ;
// Use chartXML from ref directly - more reliable than export
// especially on Vercel where DrawIO iframe may have latency issues
// Using ref to avoid stale closure in callback
const cachedXML = chartXMLRef . current ;
if ( cachedXML ) {
currentXml = cachedXML ;
console . log (
"[edit_diagram] Using cached chartXML, length:" ,
currentXml . length
) ;
} else {
// Fallback to export only if no cached XML
console . log (
"[edit_diagram] No cached XML, fetching from DrawIO..."
) ;
currentXml = await onFetchChart ( false ) ;
console . log (
"[edit_diagram] Got XML from export, length:" ,
currentXml . length
) ;
}
const { replaceXMLParts } = await import ( "@/lib/utils" ) ;
const editedXml = replaceXMLParts ( currentXml , edits ) ;
onDisplayChart ( editedXml ) ;
addToolResult ( {
tool : "edit_diagram" ,
toolCallId : toolCall.toolCallId ,
output : ` Successfully applied ${ edits . length } edit(s) to the diagram. ` ,
} ) ;
console . log ( "[edit_diagram] Success" ) ;
} catch ( error ) {
console . error ( "[edit_diagram] Failed:" , error ) ;
2025-11-10 11:27:25 +09:00
2025-12-05 22:42:39 +09:00
const errorMessage =
error instanceof Error
? error . message
: String ( error ) ;
2025-12-05 16:46:17 +09:00
2025-12-05 22:42:39 +09:00
addToolResult ( {
tool : "edit_diagram" ,
toolCallId : toolCall.toolCallId ,
output : ` Edit failed: ${ errorMessage }
2025-11-13 22:27:11 +09:00
Current diagram XML :
\ ` \` \` xml
2025-12-05 00:43:21 +09:00
$ { currentXml || "No XML available" }
2025-11-13 22:27:11 +09:00
\ ` \` \`
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted . ` ,
2025-12-05 22:42:39 +09:00
} ) ;
}
}
} ,
onError : ( error ) = > {
// Silence access code error in console since it's handled by UI
if ( ! error . message . includes ( "Invalid or missing access code" ) ) {
console . error ( "Chat error:" , error ) ;
2025-08-31 12:54:14 +09:00
}
2025-12-05 21:09:34 +08:00
2025-12-05 22:42:39 +09:00
// Add system message for error so it can be cleared
setMessages ( ( currentMessages ) = > {
const errorMessage = {
id : ` error- ${ Date . now ( ) } ` ,
role : "system" as const ,
content : error.message ,
parts : [ { type : "text" as const , text : error.message } ] ,
} ;
return [ . . . currentMessages , errorMessage ] ;
} ) ;
if ( error . message . includes ( "Invalid or missing access code" ) ) {
// Show settings button and open dialog to help user fix it
setAccessCodeRequired ( true ) ;
setShowSettingsDialog ( true ) ;
}
} ,
} ) ;
2025-12-03 21:49:34 +09:00
2025-03-23 12:48:31 +00:00
const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
2025-12-03 21:49:34 +09:00
2025-03-19 07:20:22 +00:00
useEffect ( ( ) = > {
if ( messagesEndRef . current ) {
2025-03-23 12:48:31 +00:00
messagesEndRef . current . scrollIntoView ( { behavior : "smooth" } ) ;
2025-03-19 07:20:22 +00:00
}
2025-03-23 12:48:31 +00:00
} , [ messages ] ) ;
2025-03-19 07:20:22 +00:00
2025-03-22 16:03:03 +00:00
const onFormSubmit = async ( e : React.FormEvent < HTMLFormElement > ) = > {
2025-03-23 12:48:31 +00:00
e . preventDefault ( ) ;
2025-12-05 20:18:19 +09:00
const isProcessing = status === "streaming" || status === "submitted" ;
2025-11-15 14:29:18 +09:00
if ( input . trim ( ) && ! isProcessing ) {
2025-03-22 16:03:03 +00:00
try {
2025-08-31 20:52:04 +09:00
let chartXml = await onFetchChart ( ) ;
chartXml = formatXML ( chartXml ) ;
2025-08-31 12:54:14 +09:00
2025-12-05 00:54:35 +09:00
// Update ref directly to avoid race condition with React's async state update
// This ensures edit_diagram has the correct XML before AI responds
chartXMLRef . current = chartXml ;
2025-08-31 12:54:14 +09:00
const parts : any [ ] = [ { type : "text" , text : input } ] ;
if ( files . length > 0 ) {
for ( const file of files ) {
const reader = new FileReader ( ) ;
const dataUrl = await new Promise < string > ( ( resolve ) = > {
reader . onload = ( ) = >
resolve ( reader . result as string ) ;
reader . readAsDataURL ( file ) ;
} ) ;
parts . push ( {
type : "file" ,
url : dataUrl ,
mediaType : file.type ,
} ) ;
}
}
2025-12-04 22:56:59 +09:00
// Save XML snapshot for this message (will be at index = current messages.length)
const messageIndex = messages . length ;
xmlSnapshotsRef . current . set ( messageIndex , chartXml ) ;
2025-12-05 22:42:39 +09:00
const accessCode =
localStorage . getItem ( STORAGE_ACCESS_CODE_KEY ) || "" ;
2025-08-31 12:54:14 +09:00
sendMessage (
{ parts } ,
{
body : {
xml : chartXml ,
2025-12-05 21:15:02 +09:00
sessionId ,
2025-08-31 12:54:14 +09:00
} ,
2025-12-05 21:09:34 +08:00
headers : {
"x-access-code" : accessCode ,
} ,
2025-08-31 12:54:14 +09:00
}
) ;
setInput ( "" ) ;
2025-03-27 08:02:03 +00:00
setFiles ( [ ] ) ;
2025-03-22 16:03:03 +00:00
} catch ( error ) {
console . error ( "Error fetching chart data:" , error ) ;
}
2025-03-19 07:20:22 +00:00
}
2025-03-23 12:48:31 +00:00
} ;
2025-03-19 07:20:22 +00:00
2025-08-31 12:54:14 +09:00
const handleInputChange = (
e : React.ChangeEvent < HTMLInputElement | HTMLTextAreaElement >
) = > {
setInput ( e . target . value ) ;
} ;
2025-03-27 08:02:03 +00:00
const handleFileChange = ( newFiles : File [ ] ) = > {
2025-03-23 11:03:25 +00:00
setFiles ( newFiles ) ;
2025-03-23 12:48:31 +00:00
} ;
2025-03-23 13:54:21 +00:00
2025-12-04 22:56:59 +09:00
const handleRegenerate = async ( messageIndex : number ) = > {
const isProcessing = status === "streaming" || status === "submitted" ;
if ( isProcessing ) return ;
// Find the user message before this assistant message
let userMessageIndex = messageIndex - 1 ;
2025-12-05 16:46:17 +09:00
while (
userMessageIndex >= 0 &&
messages [ userMessageIndex ] . role !== "user"
) {
2025-12-04 22:56:59 +09:00
userMessageIndex -- ;
}
if ( userMessageIndex < 0 ) return ;
const userMessage = messages [ userMessageIndex ] ;
const userParts = userMessage . parts ;
// Get the text from the user message
const textPart = userParts ? . find ( ( p : any ) = > p . type === "text" ) ;
if ( ! textPart ) return ;
// Get the saved XML snapshot for this user message
const savedXml = xmlSnapshotsRef . current . get ( userMessageIndex ) ;
if ( ! savedXml ) {
2025-12-05 16:46:17 +09:00
console . error (
"No saved XML snapshot for message index:" ,
userMessageIndex
) ;
2025-12-04 22:56:59 +09:00
return ;
}
// Restore the diagram to the saved state
onDisplayChart ( savedXml ) ;
2025-12-05 00:54:35 +09:00
// Update ref directly to ensure edit_diagram has the correct XML
chartXMLRef . current = savedXml ;
2025-12-04 22:56:59 +09:00
// Clean up snapshots for messages after the user message (they will be removed)
for ( const key of xmlSnapshotsRef . current . keys ( ) ) {
if ( key > userMessageIndex ) {
xmlSnapshotsRef . current . delete ( key ) ;
}
}
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
// Use flushSync to ensure state update is processed synchronously before sending
const newMessages = messages . slice ( 0 , userMessageIndex ) ;
flushSync ( ( ) = > {
setMessages ( newMessages ) ;
} ) ;
// Now send the message after state is guaranteed to be updated
sendMessage (
{ parts : userParts } ,
{
body : {
xml : savedXml ,
2025-12-05 21:15:02 +09:00
sessionId ,
2025-12-04 22:56:59 +09:00
} ,
}
) ;
} ;
const handleEditMessage = async ( messageIndex : number , newText : string ) = > {
const isProcessing = status === "streaming" || status === "submitted" ;
if ( isProcessing ) return ;
const message = messages [ messageIndex ] ;
if ( ! message || message . role !== "user" ) return ;
// Get the saved XML snapshot for this user message
const savedXml = xmlSnapshotsRef . current . get ( messageIndex ) ;
if ( ! savedXml ) {
2025-12-05 16:46:17 +09:00
console . error (
"No saved XML snapshot for message index:" ,
messageIndex
) ;
2025-12-04 22:56:59 +09:00
return ;
}
// Restore the diagram to the saved state
onDisplayChart ( savedXml ) ;
2025-12-05 00:54:35 +09:00
// Update ref directly to ensure edit_diagram has the correct XML
chartXMLRef . current = savedXml ;
2025-12-04 22:56:59 +09:00
// Clean up snapshots for messages after the user message (they will be removed)
for ( const key of xmlSnapshotsRef . current . keys ( ) ) {
if ( key > messageIndex ) {
xmlSnapshotsRef . current . delete ( key ) ;
}
}
// Create new parts with updated text
const newParts = message . parts ? . map ( ( part : any ) = > {
if ( part . type === "text" ) {
return { . . . part , text : newText } ;
}
return part ;
} ) || [ { type : "text" , text : newText } ] ;
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
// Use flushSync to ensure state update is processed synchronously before sending
const newMessages = messages . slice ( 0 , messageIndex ) ;
flushSync ( ( ) = > {
setMessages ( newMessages ) ;
} ) ;
// Now send the edited message after state is guaranteed to be updated
sendMessage (
{ parts : newParts } ,
{
body : {
xml : savedXml ,
2025-12-05 21:15:02 +09:00
sessionId ,
2025-12-04 22:56:59 +09:00
} ,
}
) ;
} ;
2025-12-03 21:49:34 +09:00
// Collapsed view
2025-11-15 12:09:32 +09:00
if ( ! isVisible ) {
return (
2025-12-03 21:49:34 +09:00
< div className = "h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl" >
2025-11-15 12:09:32 +09:00
< ButtonWithTooltip
tooltipContent = "Show chat panel (Ctrl+B)"
variant = "ghost"
size = "icon"
onClick = { onToggleVisibility }
2025-12-03 21:49:34 +09:00
className = "hover:bg-accent transition-colors"
2025-11-15 12:09:32 +09:00
>
2025-12-03 21:49:34 +09:00
< PanelRightOpen className = "h-5 w-5 text-muted-foreground" / >
2025-11-15 12:09:32 +09:00
< / ButtonWithTooltip >
< div
2025-12-03 21:49:34 +09:00
className = "text-sm font-medium text-muted-foreground mt-8 tracking-wide"
2025-12-03 16:47:45 +09:00
style = { {
writingMode : "vertical-rl" ,
transform : "rotate(180deg)" ,
} }
2025-11-15 12:09:32 +09:00
>
2025-12-03 21:49:34 +09:00
AI Chat
2025-11-15 12:09:32 +09:00
< / div >
2025-12-03 21:49:34 +09:00
< / div >
2025-11-15 12:09:32 +09:00
) ;
}
2025-12-03 21:49:34 +09:00
// Full view
2025-03-19 07:20:22 +00:00
return (
2025-12-05 19:30:50 +09:00
< div className = "h-full flex flex-col bg-card shadow-soft animate-slide-in-right rounded-xl border border-border/30 relative" >
2025-12-05 22:42:39 +09:00
< Toaster
position = "bottom-center"
richColors
style = { { position : "absolute" } }
/ >
2025-12-03 21:49:34 +09:00
{ /* Header */ }
< header className = "px-5 py-4 border-b border-border/50" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-3" >
< div className = "flex items-center gap-2" >
< Image
src = "/favicon.ico"
alt = "Next AI Drawio"
width = { 28 }
height = { 28 }
className = "rounded"
/ >
< h1 className = "text-base font-semibold tracking-tight whitespace-nowrap" >
Next AI Drawio
< / h1 >
< / div >
< Link
href = "/about"
className = "text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
>
About
< / Link >
2025-12-05 23:22:29 +09:00
< ButtonWithTooltip
tooltipContent = "Recent generation failures were caused by our AI provider's infrastructure issue, not the app code. After extensive debugging, I've switched providers and observed 6 hours of stability. If issues persist, please report on GitHub."
variant = "ghost"
size = "icon"
className = "h-6 w-6 text-green-500 hover:text-green-600"
>
< CheckCircle className = "h-4 w-4" / >
< / ButtonWithTooltip >
2025-12-03 21:49:34 +09:00
< / div >
< div className = "flex items-center gap-1" >
< a
href = "https://github.com/DayuanJiang/next-ai-draw-io"
target = "_blank"
rel = "noopener noreferrer"
className = "p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
>
< FaGithub className = "w-5 h-5" / >
< / a >
2025-12-05 21:09:34 +08:00
{ accessCodeRequired && (
< ButtonWithTooltip
tooltipContent = "Settings"
variant = "ghost"
size = "icon"
onClick = { ( ) = > setShowSettingsDialog ( true ) }
className = "hover:bg-accent"
>
< Settings className = "h-5 w-5 text-muted-foreground" / >
< / ButtonWithTooltip >
) }
2025-12-03 21:49:34 +09:00
< ButtonWithTooltip
tooltipContent = "Hide chat panel (Ctrl+B)"
variant = "ghost"
size = "icon"
onClick = { onToggleVisibility }
className = "hover:bg-accent"
>
< PanelRightClose className = "h-5 w-5 text-muted-foreground" / >
< / ButtonWithTooltip >
< / div >
feat: Separate SEO content to /about page (best practice)
- Remove header from main page for clean editor-only interface
- Create /app/about/page.tsx with comprehensive SEO content (1000+ words)
- Add About link next to 'Next-AI-Drawio' title in chat panel
- Add GitHub icon link to /about page navigation
- Update sitemap.ts to include /about page (priority: 0.8)
SEO improvements following industry best practices:
- Separate marketing content from app interface (Figma/Canva/Miro approach)
- Server-rendered /about page for optimal crawlability
- Clean URL structure for better internal linking
- Multiple indexable pages for broader keyword coverage
- Proper semantic HTML: H1, H2, H3, article, section tags
- 1000+ words of keyword-rich content
/about page includes:
- AI diagram generator overview with value proposition
- 6 detailed feature sections (AI creation, AWS diagrams, image replication, etc.)
- 3 popular use cases (AWS architecture, flowcharts, system design)
- Step-by-step usage guide (4 steps)
- Benefits section (save time, precision, free, privacy)
- Clear call-to-action with link back to editor
- GitHub link in navigation for social proof
This follows Google-approved architecture and avoids hidden content penalties.
2025-11-16 09:04:01 +09:00
< / div >
2025-12-03 21:49:34 +09:00
< / header >
{ /* Messages */ }
2025-12-05 22:42:39 +09:00
< main className = "flex-1 w-full overflow-hidden" >
2025-03-25 02:58:11 +00:00
< ChatMessageDisplay
messages = { messages }
setInput = { setInput }
setFiles = { handleFileChange }
2025-12-05 21:15:02 +09:00
sessionId = { sessionId }
2025-12-04 22:56:59 +09:00
onRegenerate = { handleRegenerate }
onEditMessage = { handleEditMessage }
2025-03-25 02:58:11 +00:00
/ >
2025-12-03 21:49:34 +09:00
< / main >
2025-03-23 13:15:28 +00:00
2025-12-03 21:49:34 +09:00
{ /* Input */ }
< footer className = "p-4 border-t border-border/50 bg-card/50" >
2025-03-22 13:15:51 +00:00
< ChatInput
input = { input }
2025-03-22 13:26:14 +00:00
status = { status }
2025-03-22 13:15:51 +00:00
onSubmit = { onFormSubmit }
onChange = { handleInputChange }
2025-03-27 08:09:22 +00:00
onClearChat = { ( ) = > {
setMessages ( [ ] ) ;
clearDiagram ( ) ;
2025-12-05 22:42:39 +09:00
setSessionId (
` session- ${ Date . now ( ) } - ${ Math . random ( )
. toString ( 36 )
. slice ( 2 , 9 ) } `
) ;
2025-12-04 22:56:59 +09:00
xmlSnapshotsRef . current . clear ( ) ;
2025-03-27 08:09:22 +00:00
} }
2025-03-23 11:03:25 +00:00
files = { files }
onFileChange = { handleFileChange }
2025-03-23 13:54:21 +00:00
showHistory = { showHistory }
2025-03-27 07:48:19 +00:00
onToggleHistory = { setShowHistory }
2025-12-05 21:15:02 +09:00
sessionId = { sessionId }
2025-12-05 20:18:19 +09:00
error = { error }
2025-12-05 23:10:48 +09:00
drawioUi = { drawioUi }
onToggleDrawioUi = { onToggleDrawioUi }
2025-03-22 13:15:51 +00:00
/ >
2025-12-03 21:49:34 +09:00
< / footer >
2025-12-05 21:09:34 +08:00
< SettingsDialog
open = { showSettingsDialog }
onOpenChange = { setShowSettingsDialog }
/ >
2025-12-03 21:49:34 +09:00
< / div >
2025-03-23 12:48:31 +00:00
) ;
2025-03-19 07:20:22 +00:00
}