2025-12-06 12:46:40 +09:00
"use client"
2025-03-19 07:20:22 +00:00
2025-12-06 12:46:40 +09:00
import { useChat } from "@ai-sdk/react"
2025-12-07 00:40:11 +09:00
import {
DefaultChatTransport ,
lastAssistantMessageIsCompleteWithToolCalls ,
} from "ai"
2025-12-05 22:42:39 +09:00
import {
2025-12-06 12:46:40 +09:00
CheckCircle ,
2025-12-05 22:42:39 +09:00
PanelRightClose ,
PanelRightOpen ,
Settings ,
2025-12-06 12:46:40 +09:00
} from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import type React from "react"
2025-12-07 01:39:09 +09:00
import { useCallback , useEffect , useRef , useState } from "react"
2025-12-06 12:46:40 +09:00
import { flushSync } from "react-dom"
import { FaGithub } from "react-icons/fa"
import { Toaster } from "sonner"
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
import { ChatInput } from "@/components/chat-input"
2025-12-05 22:42:39 +09:00
import {
SettingsDialog ,
STORAGE_ACCESS_CODE_KEY ,
2025-12-06 12:46:40 +09:00
} from "@/components/settings-dialog"
2025-12-07 01:39:09 +09:00
// localStorage keys for persistence
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
2025-12-06 12:46:40 +09:00
import { useDiagram } from "@/contexts/diagram-context"
2025-12-07 19:36:09 +09:00
import { findCachedResponse } from "@/lib/cached-responses"
2025-12-07 14:38:15 +09:00
import { formatXML } from "@/lib/utils"
2025-12-06 12:46:40 +09:00
import { ChatMessageDisplay } from "./chat-message-display"
2025-03-26 00:30:00 +00:00
2025-11-15 12:09:32 +09:00
interface ChatPanelProps {
2025-12-06 12:46:40 +09:00
isVisible : boolean
onToggleVisibility : ( ) = > void
drawioUi : "min" | "sketch"
onToggleDrawioUi : ( ) = > void
isMobile? : boolean
2025-12-06 21:42:28 +09:00
onCloseProtectionChange ? : ( enabled : boolean ) = > 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-05 23:25:59 +09:00
isMobile = false ,
2025-12-06 21:42:28 +09:00
onCloseProtectionChange ,
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-12-07 01:39:09 +09:00
isDrawioReady ,
2025-12-06 12:46:40 +09: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 ) {
2025-12-06 12:46:40 +09:00
resolverRef . current = resolve
2025-11-10 10:28:37 +09:00
}
2025-12-03 21:58:48 +09:00
if ( saveToHistory ) {
2025-12-06 12:46:40 +09:00
onExport ( )
2025-12-03 21:58:48 +09:00
} else {
2025-12-06 12:46:40 +09:00
handleExportWithoutHistory ( )
2025-12-03 21:58:48 +09:00
}
2025-11-10 10:28:37 +09:00
} ) ,
new Promise < string > ( ( _ , reject ) = >
2025-12-03 16:47:45 +09:00
setTimeout (
( ) = >
reject (
2025-12-06 12:46:40 +09:00
new Error (
"Chart export timed out after 10 seconds" ,
) ,
2025-12-03 16:47:45 +09:00
) ,
2025-12-06 12:46:40 +09:00
10000 ,
) ,
2025-12-03 16:47:45 +09:00
) ,
2025-12-06 12:46:40 +09:00
] )
}
2025-03-25 04:23:38 +00:00
2025-12-06 12:46:40 +09:00
const [ files , setFiles ] = useState < File [ ] > ( [ ] )
const [ showHistory , setShowHistory ] = useState ( false )
const [ showSettingsDialog , setShowSettingsDialog ] = useState ( false )
2025-12-07 01:39:09 +09:00
const [ , setAccessCodeRequired ] = useState ( false )
2025-12-06 12:46:40 +09:00
const [ input , setInput ] = useState ( "" )
2025-08-31 12:54:14 +09:00
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 ) )
2025-12-06 12:46:40 +09:00
. catch ( ( ) = > setAccessCodeRequired ( false ) )
} , [ ] )
2025-12-05 21:09:34 +08:00
2025-12-07 01:39:09 +09:00
// Generate a unique session ID for Langfuse tracing (restore from localStorage if available)
const [ sessionId , setSessionId ] = useState ( ( ) = > {
if ( typeof window !== "undefined" ) {
const saved = localStorage . getItem ( STORAGE_SESSION_ID_KEY )
if ( saved ) return saved
}
return ` 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)
2025-12-06 12:46:40 +09:00
const xmlSnapshotsRef = useRef < Map < number , string > > ( new Map ( ) )
2025-12-04 22:56:59 +09:00
2025-12-07 01:39:09 +09:00
// Flag to track if we've restored from localStorage
const hasRestoredRef = useRef ( false )
2025-12-05 00:47:27 +09:00
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
2025-12-06 12:46:40 +09:00
const chartXMLRef = useRef ( chartXML )
2025-12-05 00:47:27 +09:00
useEffect ( ( ) = > {
2025-12-06 12:46:40 +09:00
chartXMLRef . current = chartXML
} , [ chartXML ] )
2025-12-05 00:47:27 +09:00
2025-12-07 00:40:11 +09:00
// Ref to hold stop function for use in onToolCall (avoids stale closure)
const stopRef = useRef < ( ( ) = > void ) | null > ( null )
const {
messages ,
sendMessage ,
addToolOutput ,
stop ,
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-07 14:38:15 +09:00
// loadDiagram validates and returns error if invalid
const validationError = onDisplayChart ( xml )
2025-12-07 00:40:11 +09:00
if ( validationError ) {
console . warn (
"[display_diagram] Validation error:" ,
validationError ,
)
// Return error to model - sendAutomaticallyWhen will trigger retry
const errorMessage = ` ${ validationError }
Please fix the XML issues and call display_diagram again with corrected XML .
Your failed XML :
\ ` \` \` xml
$ { xml }
\ ` \` \` `
addToolOutput ( {
tool : "display_diagram" ,
toolCallId : toolCall.toolCallId ,
state : "output-error" ,
errorText : errorMessage ,
} )
} else {
// Success - diagram will be rendered by chat-message-display
addToolOutput ( {
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 ,
)
2025-12-05 22:42:39 +09:00
} else {
2025-12-07 00:40:11 +09:00
// 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 ,
)
2025-12-06 12:46:40 +09:00
}
2025-12-05 22:42:39 +09:00
2025-12-07 00:40:11 +09:00
const { replaceXMLParts } = await import ( "@/lib/utils" )
const editedXml = replaceXMLParts ( currentXml , edits )
2025-12-07 14:38:15 +09:00
// loadDiagram validates and returns error if invalid
const validationError = onDisplayChart ( editedXml )
if ( validationError ) {
console . warn (
"[edit_diagram] Validation error:" ,
validationError ,
)
addToolOutput ( {
tool : "edit_diagram" ,
toolCallId : toolCall.toolCallId ,
state : "output-error" ,
errorText : ` Edit produced invalid XML: ${ validationError }
Current diagram XML :
\ ` \` \` xml
$ { currentXml }
\ ` \` \`
Please fix the edit to avoid structural issues ( e . g . , duplicate IDs , invalid references ) . ` ,
} )
return
}
2025-12-07 00:40:11 +09:00
addToolOutput ( {
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 )
const errorMessage =
error instanceof Error ? error.message : String ( error )
// Use addToolOutput with state: 'output-error' for proper error signaling
addToolOutput ( {
tool : "edit_diagram" ,
toolCallId : toolCall.toolCallId ,
state : "output-error" ,
errorText : ` 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-07 00:40:11 +09:00
} )
2025-12-05 22:42:39 +09:00
}
2025-12-07 00:40:11 +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 )
}
// 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 } ] ,
2025-08-31 12:54:14 +09:00
}
2025-12-07 00:40:11 +09:00
return [ . . . currentMessages , errorMessage ]
} )
2025-12-05 21:09:34 +08:00
2025-12-07 00:40:11 +09:00
if ( error . message . includes ( "Invalid or missing access code" ) ) {
// Show settings button and open dialog to help user fix it
setAccessCodeRequired ( true )
setShowSettingsDialog ( true )
}
} ,
// Auto-resubmit when all tool results are available (including errors)
// This enables the model to retry when a tool returns an error
sendAutomaticallyWhen : lastAssistantMessageIsCompleteWithToolCalls ,
} )
2025-12-05 22:42:39 +09:00
2025-12-07 00:40:11 +09:00
// Update stopRef so onToolCall can access it
stopRef . current = stop
2025-12-03 21:49:34 +09:00
2025-12-07 01:39:09 +09:00
// Ref to track latest messages for unload persistence
const messagesRef = useRef ( messages )
useEffect ( ( ) = > {
messagesRef . current = messages
} , [ messages ] )
2025-12-06 12:46:40 +09:00
const messagesEndRef = useRef < HTMLDivElement > ( null )
2025-12-03 21:49:34 +09:00
2025-12-07 01:39:09 +09:00
// Restore messages and XML snapshots from localStorage on mount
useEffect ( ( ) = > {
if ( hasRestoredRef . current ) return
hasRestoredRef . current = true
try {
// Restore messages
const savedMessages = localStorage . getItem ( STORAGE_MESSAGES_KEY )
if ( savedMessages ) {
const parsed = JSON . parse ( savedMessages )
if ( Array . isArray ( parsed ) && parsed . length > 0 ) {
setMessages ( parsed )
}
}
// Restore XML snapshots
const savedSnapshots = localStorage . getItem (
STORAGE_XML_SNAPSHOTS_KEY ,
)
if ( savedSnapshots ) {
const parsed = JSON . parse ( savedSnapshots )
xmlSnapshotsRef . current = new Map ( parsed )
}
} catch ( error ) {
console . error ( "Failed to restore from localStorage:" , error )
}
} , [ setMessages ] )
// Restore diagram XML when DrawIO becomes ready
const hasDiagramRestoredRef = useRef ( false )
const [ canSaveDiagram , setCanSaveDiagram ] = useState ( false )
useEffect ( ( ) = > {
console . log (
"[ChatPanel] isDrawioReady:" ,
isDrawioReady ,
"hasDiagramRestored:" ,
hasDiagramRestoredRef . current ,
)
if ( ! isDrawioReady || hasDiagramRestoredRef . current ) return
hasDiagramRestoredRef . current = true
try {
const savedDiagramXml = localStorage . getItem (
STORAGE_DIAGRAM_XML_KEY ,
)
console . log (
"[ChatPanel] Restoring diagram, has saved XML:" ,
! ! savedDiagramXml ,
)
if ( savedDiagramXml ) {
console . log (
"[ChatPanel] Loading saved diagram XML, length:" ,
savedDiagramXml . length ,
)
2025-12-07 14:38:15 +09:00
// Skip validation for trusted saved diagrams
onDisplayChart ( savedDiagramXml , true )
2025-12-07 01:39:09 +09:00
chartXMLRef . current = savedDiagramXml
}
} catch ( error ) {
console . error ( "Failed to restore diagram from localStorage:" , error )
}
// Allow saving after restore is complete
setTimeout ( ( ) = > {
console . log ( "[ChatPanel] Enabling diagram save" )
setCanSaveDiagram ( true )
} , 500 )
} , [ isDrawioReady , onDisplayChart ] )
// Save messages to localStorage whenever they change
useEffect ( ( ) = > {
if ( ! hasRestoredRef . current ) return
try {
localStorage . setItem ( STORAGE_MESSAGES_KEY , JSON . stringify ( messages ) )
} catch ( error ) {
console . error ( "Failed to save messages to localStorage:" , error )
}
} , [ messages ] )
// Save XML snapshots to localStorage whenever they change
const saveXmlSnapshots = useCallback ( ( ) = > {
try {
const snapshotsArray = Array . from ( xmlSnapshotsRef . current . entries ( ) )
localStorage . setItem (
STORAGE_XML_SNAPSHOTS_KEY ,
JSON . stringify ( snapshotsArray ) ,
)
} catch ( error ) {
console . error (
"Failed to save XML snapshots to localStorage:" ,
error ,
)
}
} , [ ] )
// Save session ID to localStorage
useEffect ( ( ) = > {
localStorage . setItem ( STORAGE_SESSION_ID_KEY , sessionId )
} , [ sessionId ] )
// Save current diagram XML to localStorage whenever it changes
// Only save after initial restore is complete and if it's not an empty diagram
useEffect ( ( ) = > {
if ( ! canSaveDiagram ) return
// Don't save empty diagrams (check for minimal content)
if ( chartXML && chartXML . length > 300 ) {
console . log (
"[ChatPanel] Saving diagram to localStorage, length:" ,
chartXML . length ,
)
localStorage . setItem ( STORAGE_DIAGRAM_XML_KEY , chartXML )
}
} , [ chartXML , canSaveDiagram ] )
2025-03-19 07:20:22 +00:00
useEffect ( ( ) = > {
if ( messagesEndRef . current ) {
2025-12-06 12:46:40 +09:00
messagesEndRef . current . scrollIntoView ( { behavior : "smooth" } )
2025-03-19 07:20:22 +00:00
}
2025-12-06 12:46:40 +09:00
} , [ messages ] )
2025-03-19 07:20:22 +00:00
2025-12-07 01:39:09 +09:00
// Save state right before page unload (refresh/close)
useEffect ( ( ) = > {
const handleBeforeUnload = ( ) = > {
try {
localStorage . setItem (
STORAGE_MESSAGES_KEY ,
JSON . stringify ( messagesRef . current ) ,
)
localStorage . setItem (
STORAGE_XML_SNAPSHOTS_KEY ,
JSON . stringify (
Array . from ( xmlSnapshotsRef . current . entries ( ) ) ,
) ,
)
const xml = chartXMLRef . current
if ( xml && xml . length > 300 ) {
localStorage . setItem ( STORAGE_DIAGRAM_XML_KEY , xml )
}
localStorage . setItem ( STORAGE_SESSION_ID_KEY , sessionId )
} catch ( error ) {
console . error ( "Failed to persist state before unload:" , error )
}
}
window . addEventListener ( "beforeunload" , handleBeforeUnload )
return ( ) = >
window . removeEventListener ( "beforeunload" , handleBeforeUnload )
} , [ sessionId ] )
2025-03-22 16:03:03 +00:00
const onFormSubmit = async ( e : React.FormEvent < HTMLFormElement > ) = > {
2025-12-06 12:46:40 +09:00
e . preventDefault ( )
const isProcessing = status === "streaming" || status === "submitted"
2025-11-15 14:29:18 +09:00
if ( input . trim ( ) && ! isProcessing ) {
2025-12-07 19:36:09 +09:00
// Check if input matches a cached example (only when no messages yet)
if ( messages . length === 0 ) {
const cached = findCachedResponse (
input . trim ( ) ,
files . length > 0 ,
)
if ( cached ) {
// Add user message and fake assistant response to messages
// The chat-message-display useEffect will handle displaying the diagram
const toolCallId = ` cached- ${ Date . now ( ) } `
setMessages ( [
{
id : ` user- ${ Date . now ( ) } ` ,
role : "user" as const ,
parts : [ { type : "text" as const , text : input } ] ,
} ,
{
id : ` assistant- ${ Date . now ( ) } ` ,
role : "assistant" as const ,
parts : [
{
type : "tool-display_diagram" as const ,
toolCallId ,
state : "output-available" as const ,
input : { xml : cached.xml } ,
output : "Successfully displayed the diagram." ,
} ,
] ,
} ,
] as any )
setInput ( "" )
setFiles ( [ ] )
return
}
}
2025-03-22 16:03:03 +00:00
try {
2025-12-06 12:46:40 +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
2025-12-06 12:46:40 +09:00
chartXMLRef . current = chartXml
2025-12-05 00:54:35 +09:00
2025-12-06 12:46:40 +09:00
const parts : any [ ] = [ { type : "text" , text : input } ]
2025-08-31 12:54:14 +09:00
if ( files . length > 0 ) {
for ( const file of files ) {
2025-12-06 12:46:40 +09:00
const reader = new FileReader ( )
2025-08-31 12:54:14 +09:00
const dataUrl = await new Promise < string > ( ( resolve ) = > {
reader . onload = ( ) = >
2025-12-06 12:46:40 +09:00
resolve ( reader . result as string )
reader . readAsDataURL ( file )
} )
2025-08-31 12:54:14 +09:00
parts . push ( {
type : "file" ,
url : dataUrl ,
mediaType : file.type ,
2025-12-06 12:46:40 +09:00
} )
2025-08-31 12:54:14 +09:00
}
}
2025-12-04 22:56:59 +09:00
// Save XML snapshot for this message (will be at index = current messages.length)
2025-12-06 12:46:40 +09:00
const messageIndex = messages . length
xmlSnapshotsRef . current . set ( messageIndex , chartXml )
2025-12-07 01:39:09 +09:00
saveXmlSnapshots ( )
2025-12-04 22:56:59 +09:00
2025-12-05 22:42:39 +09:00
const accessCode =
2025-12-06 12:46:40 +09:00
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-12-06 12:46:40 +09:00
} ,
)
2025-08-31 12:54:14 +09:00
2025-12-06 12:46:40 +09:00
setInput ( "" )
setFiles ( [ ] )
2025-03-22 16:03:03 +00:00
} catch ( error ) {
2025-12-06 12:46:40 +09:00
console . error ( "Error fetching chart data:" , error )
2025-03-22 16:03:03 +00:00
}
2025-03-19 07:20:22 +00:00
}
2025-12-06 12:46:40 +09:00
}
2025-03-19 07:20:22 +00:00
2025-08-31 12:54:14 +09:00
const handleInputChange = (
2025-12-06 12:46:40 +09:00
e : React.ChangeEvent < HTMLInputElement | HTMLTextAreaElement > ,
2025-08-31 12:54:14 +09:00
) = > {
2025-12-06 12:46:40 +09:00
setInput ( e . target . value )
}
2025-08-31 12:54:14 +09:00
2025-03-27 08:02:03 +00:00
const handleFileChange = ( newFiles : File [ ] ) = > {
2025-12-06 12:46:40 +09:00
setFiles ( newFiles )
}
2025-03-23 13:54:21 +00:00
2025-12-04 22:56:59 +09:00
const handleRegenerate = async ( messageIndex : number ) = > {
2025-12-06 12:46:40 +09:00
const isProcessing = status === "streaming" || status === "submitted"
if ( isProcessing ) return
2025-12-04 22:56:59 +09:00
// Find the user message before this assistant message
2025-12-06 12:46:40 +09:00
let userMessageIndex = messageIndex - 1
2025-12-05 16:46:17 +09:00
while (
userMessageIndex >= 0 &&
messages [ userMessageIndex ] . role !== "user"
) {
2025-12-06 12:46:40 +09:00
userMessageIndex --
2025-12-04 22:56:59 +09:00
}
2025-12-06 12:46:40 +09:00
if ( userMessageIndex < 0 ) return
2025-12-04 22:56:59 +09:00
2025-12-06 12:46:40 +09:00
const userMessage = messages [ userMessageIndex ]
const userParts = userMessage . parts
2025-12-04 22:56:59 +09:00
// Get the text from the user message
2025-12-06 12:46:40 +09:00
const textPart = userParts ? . find ( ( p : any ) = > p . type === "text" )
if ( ! textPart ) return
2025-12-04 22:56:59 +09:00
// Get the saved XML snapshot for this user message
2025-12-06 12:46:40 +09:00
const savedXml = xmlSnapshotsRef . current . get ( userMessageIndex )
2025-12-04 22:56:59 +09:00
if ( ! savedXml ) {
2025-12-05 16:46:17 +09:00
console . error (
"No saved XML snapshot for message index:" ,
2025-12-06 12:46:40 +09:00
userMessageIndex ,
)
return
2025-12-04 22:56:59 +09:00
}
2025-12-07 14:38:15 +09:00
// Restore the diagram to the saved state (skip validation for trusted snapshots)
onDisplayChart ( savedXml , true )
2025-12-04 22:56:59 +09:00
2025-12-05 00:54:35 +09:00
// Update ref directly to ensure edit_diagram has the correct XML
2025-12-06 12:46:40 +09:00
chartXMLRef . current = savedXml
2025-12-05 00:54:35 +09:00
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 ) {
2025-12-06 12:46:40 +09:00
xmlSnapshotsRef . current . delete ( key )
2025-12-04 22:56:59 +09:00
}
}
2025-12-07 01:39:09 +09:00
saveXmlSnapshots ( )
2025-12-04 22:56:59 +09:00
// 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
2025-12-06 12:46:40 +09:00
const newMessages = messages . slice ( 0 , userMessageIndex )
2025-12-04 22:56:59 +09:00
flushSync ( ( ) = > {
2025-12-06 12:46:40 +09:00
setMessages ( newMessages )
} )
2025-12-04 22:56:59 +09:00
// Now send the message after state is guaranteed to be updated
2025-12-07 01:39:09 +09:00
const accessCode = localStorage . getItem ( STORAGE_ACCESS_CODE_KEY ) || ""
2025-12-04 22:56:59 +09:00
sendMessage (
{ parts : userParts } ,
{
body : {
xml : savedXml ,
2025-12-05 21:15:02 +09:00
sessionId ,
2025-12-04 22:56:59 +09:00
} ,
2025-12-07 01:39:09 +09:00
headers : {
"x-access-code" : accessCode ,
} ,
2025-12-06 12:46:40 +09:00
} ,
)
}
2025-12-04 22:56:59 +09:00
const handleEditMessage = async ( messageIndex : number , newText : string ) = > {
2025-12-06 12:46:40 +09:00
const isProcessing = status === "streaming" || status === "submitted"
if ( isProcessing ) return
2025-12-04 22:56:59 +09:00
2025-12-06 12:46:40 +09:00
const message = messages [ messageIndex ]
if ( ! message || message . role !== "user" ) return
2025-12-04 22:56:59 +09:00
// Get the saved XML snapshot for this user message
2025-12-06 12:46:40 +09:00
const savedXml = xmlSnapshotsRef . current . get ( messageIndex )
2025-12-04 22:56:59 +09:00
if ( ! savedXml ) {
2025-12-05 16:46:17 +09:00
console . error (
"No saved XML snapshot for message index:" ,
2025-12-06 12:46:40 +09:00
messageIndex ,
)
return
2025-12-04 22:56:59 +09:00
}
2025-12-07 14:38:15 +09:00
// Restore the diagram to the saved state (skip validation for trusted snapshots)
onDisplayChart ( savedXml , true )
2025-12-04 22:56:59 +09:00
2025-12-05 00:54:35 +09:00
// Update ref directly to ensure edit_diagram has the correct XML
2025-12-06 12:46:40 +09:00
chartXMLRef . current = savedXml
2025-12-05 00:54:35 +09:00
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 ) {
2025-12-06 12:46:40 +09:00
xmlSnapshotsRef . current . delete ( key )
2025-12-04 22:56:59 +09:00
}
}
2025-12-07 01:39:09 +09:00
saveXmlSnapshots ( )
2025-12-04 22:56:59 +09:00
// Create new parts with updated text
const newParts = message . parts ? . map ( ( part : any ) = > {
if ( part . type === "text" ) {
2025-12-06 12:46:40 +09:00
return { . . . part , text : newText }
2025-12-04 22:56:59 +09:00
}
2025-12-06 12:46:40 +09:00
return part
} ) || [ { type : "text" , text : newText } ]
2025-12-04 22:56:59 +09:00
// 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
2025-12-06 12:46:40 +09:00
const newMessages = messages . slice ( 0 , messageIndex )
2025-12-04 22:56:59 +09:00
flushSync ( ( ) = > {
2025-12-06 12:46:40 +09:00
setMessages ( newMessages )
} )
2025-12-04 22:56:59 +09:00
// Now send the edited message after state is guaranteed to be updated
2025-12-07 01:39:09 +09:00
const accessCode = localStorage . getItem ( STORAGE_ACCESS_CODE_KEY ) || ""
2025-12-04 22:56:59 +09:00
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-07 01:39:09 +09:00
headers : {
"x-access-code" : accessCode ,
} ,
2025-12-06 12:46:40 +09:00
} ,
)
}
2025-12-04 22:56:59 +09:00
2025-12-05 23:25:59 +09:00
// Collapsed view (desktop only)
if ( ! isVisible && ! isMobile ) {
2025-11-15 12:09:32 +09:00
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-12-06 12:46:40 +09:00
)
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 */ }
2025-12-06 12:46:40 +09:00
< header
className = { ` ${ isMobile ? "px-3 py-2" : "px-5 py-4" } border-b border-border/50 ` }
>
2025-12-03 21:49:34 +09:00
< div className = "flex items-center justify-between" >
2025-12-05 23:25:59 +09:00
< div className = "flex items-center gap-2" >
2025-12-03 21:49:34 +09:00
< div className = "flex items-center gap-2" >
< Image
src = "/favicon.ico"
alt = "Next AI Drawio"
2025-12-05 23:25:59 +09:00
width = { isMobile ? 24 : 28 }
height = { isMobile ? 24 : 28 }
2025-12-03 21:49:34 +09:00
className = "rounded"
/ >
2025-12-06 12:46:40 +09:00
< h1
className = { ` ${ isMobile ? "text-sm" : "text-base" } font-semibold tracking-tight whitespace-nowrap ` }
>
2025-12-03 21:49:34 +09:00
Next AI Drawio
< / h1 >
< / div >
2025-12-05 23:25:59 +09:00
{ ! isMobile && (
< Link
href = "/about"
className = "text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
>
About
< / Link >
) }
{ ! isMobile && (
< 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"
>
2025-12-06 12:46:40 +09:00
< FaGithub
className = { ` ${ isMobile ? "w-4 h-4" : "w-5 h-5" } ` }
/ >
2025-12-03 21:49:34 +09:00
< / a >
2025-12-06 21:42:28 +09:00
< ButtonWithTooltip
tooltipContent = "Settings"
variant = "ghost"
size = "icon"
onClick = { ( ) = > setShowSettingsDialog ( true ) }
className = "hover:bg-accent"
>
< Settings
className = { ` ${ isMobile ? "h-4 w-4" : "h-5 w-5" } text-muted-foreground ` }
/ >
< / ButtonWithTooltip >
2025-12-05 23:25:59 +09:00
{ ! isMobile && (
< 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" / >
2025-12-05 21:09:34 +08:00
< / ButtonWithTooltip >
) }
2025-12-03 21:49:34 +09:00
< / 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 */ }
2025-12-06 12:46:40 +09:00
< footer
className = { ` ${ isMobile ? "p-2" : "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 = { ( ) = > {
2025-12-06 12:46:40 +09:00
setMessages ( [ ] )
clearDiagram ( )
2025-12-07 01:39:09 +09:00
const newSessionId = ` session- ${ Date . now ( ) } - ${ Math . random ( )
. toString ( 36 )
. slice ( 2 , 9 ) } `
setSessionId ( newSessionId )
2025-12-06 12:46:40 +09:00
xmlSnapshotsRef . current . clear ( )
2025-12-07 01:39:09 +09:00
// Clear localStorage
localStorage . removeItem ( STORAGE_MESSAGES_KEY )
localStorage . removeItem ( STORAGE_XML_SNAPSHOTS_KEY )
localStorage . removeItem ( STORAGE_DIAGRAM_XML_KEY )
localStorage . setItem (
STORAGE_SESSION_ID_KEY ,
newSessionId ,
)
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-06 21:42:28 +09:00
onCloseProtectionChange = { onCloseProtectionChange }
2025-12-05 21:09:34 +08:00
/ >
2025-12-03 21:49:34 +09:00
< / div >
2025-12-06 12:46:40 +09:00
)
2025-03-19 07:20:22 +00:00
}