refactor: extract all states to diagram-context.

This commit is contained in:
dayuan.jiang
2025-03-26 00:30:00 +00:00
parent bd6946a13c
commit 5c00c00584
5 changed files with 146 additions and 105 deletions

View File

@@ -1,83 +1,44 @@
"use client"; "use client";
import { DrawIoEmbed, DrawIoEmbedRef } from "react-drawio"; import React from "react";
import { DrawIoEmbed } from "react-drawio";
import { useRef, useState } from "react";
import { extractDiagramXML } from "./extract_xml";
import ChatPanel from "@/components/chat-panel"; import ChatPanel from "@/components/chat-panel";
import { DiagramProvider, useDiagram } from "@/contexts/diagram-context";
export default function Home() { // Internal layout component
const drawioRef = useRef<DrawIoEmbedRef>(null); function DiagramPageLayout({ children }: { children: React.ReactNode }) {
const [chartXML, setChartXML] = useState<string>(""); const { drawioRef, handleDiagramExport } = useDiagram();
// Add a ref to store the resolver function
const resolverRef = useRef<((value: string) => void) | null>(null);
// Add state for diagram history
const [diagramHistory, setDiagramHistory] = useState<
{ svg: string; xml: string }[]
>([]);
// Add state for latest SVG
const [latestSvg, setLatestSvg] = useState<string>("");
const handleExport = () => {
if (drawioRef.current) {
drawioRef.current.exportDiagram({
format: "xmlsvg",
});
}
};
const loadDiagram = (chart: string) => {
if (drawioRef.current) {
drawioRef.current.load({
xml: chart,
});
}
};
return ( return (
<div className="flex h-screen bg-gray-100"> <div className="flex h-screen bg-gray-100">
<div className="w-2/3 p-1"> <div className="w-2/3 p-1 h-full">
<DrawIoEmbed <div className="h-full relative">
ref={drawioRef} <div className="absolute inset-0">
onExport={(data) => { <div className="w-full h-full">
const extractedXML = extractDiagramXML(data.data); <DrawIoEmbed
setChartXML(extractedXML); ref={drawioRef}
// Store the latest SVG data onExport={handleDiagramExport}
setLatestSvg(data.data); urlParameters={{
// Directly update diagramHistory with the new data spin: true,
setDiagramHistory((prev) => [ libraries: false,
...prev, saveAndExit: false,
{ svg: data.data, xml: extractedXML }, noExitBtn: true,
]); }}
// If there's a pending resolver, resolve it with the fresh XML />
if (resolverRef.current) { </div>
resolverRef.current(extractedXML); </div>
resolverRef.current = null; </div>
}
}}
urlParameters={{
spin: true,
libraries: false,
saveAndExit: false,
noExitBtn: true,
}}
/>
</div>
<div className="w-1/3 p-1 border-gray-300">
<ChatPanel
chartXML={chartXML}
onDisplayChart={(xml) => loadDiagram(xml)}
onFetchChart={() => {
return new Promise<string>((resolve) => {
// Store the resolver so onExport can use it
resolverRef.current = resolve;
// Trigger the export
handleExport();
});
}}
diagramHistory={diagramHistory}
onAddToHistory={() => {}}
/>
</div> </div>
<div className="w-1/3 h-full p-1">{children}</div>
</div> </div>
); );
} }
export default function Home() {
return (
<DiagramProvider>
<DiagramPageLayout>
<ChatPanel />
</DiagramPageLayout>
</DiagramProvider>
);
}

View File

@@ -27,17 +27,16 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import Image from "next/image"; import Image from "next/image";
import { useDiagram } from "@/contexts/diagram-context";
interface ChatInputProps { interface ChatInputProps {
input: string; input: string;
status: "submitted" | "streaming" | "ready" | "error"; status: "submitted" | "streaming" | "ready" | "error";
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void; onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
setMessages: (messages: any[]) => void; setMessages: (messages: any[]) => void;
onDisplayChart: (xml: string) => void;
files?: FileList; files?: FileList;
onFileChange?: (files: FileList | undefined) => void; onFileChange?: (files: FileList | undefined) => void;
diagramHistory?: { svg: string; xml: string }[];
onSelectHistoryItem?: (xml: string) => void;
showHistory?: boolean; showHistory?: boolean;
setShowHistory?: (show: boolean) => void; setShowHistory?: (show: boolean) => void;
} }
@@ -48,14 +47,12 @@ export function ChatInput({
onSubmit, onSubmit,
onChange, onChange,
setMessages, setMessages,
onDisplayChart,
files, files,
onFileChange, onFileChange,
diagramHistory = [],
onSelectHistoryItem = () => {},
showHistory = false, showHistory = false,
setShowHistory = () => {}, setShowHistory = () => {},
}: ChatInputProps) { }: ChatInputProps) {
const { loadDiagram: onDisplayChart, diagramHistory } = useDiagram();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@@ -289,9 +286,10 @@ export function ChatInput({
<div <div
key={index} key={index}
className="border rounded-md p-2 cursor-pointer hover:border-primary transition-colors" className="border rounded-md p-2 cursor-pointer hover:border-primary transition-colors"
onClick={() => onClick={() => {
onSelectHistoryItem(item.xml) onDisplayChart(item.xml);
} setShowHistory(false);
}}
> >
<div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center"> <div className="aspect-video bg-white rounded overflow-hidden flex items-center justify-center">
<Image <Image

View File

@@ -8,23 +8,22 @@ import ExamplePanel from "./chat-example-panel";
import { Message } from "ai"; import { Message } from "ai";
import { convertToLegalXml, replaceNodes } from "@/lib/utils"; import { convertToLegalXml, replaceNodes } from "@/lib/utils";
import { useDiagram } from "@/contexts/diagram-context";
interface ChatMessageDisplayProps { interface ChatMessageDisplayProps {
chartXML: string;
messages: Message[]; messages: Message[];
error?: Error | null; error?: Error | null;
setInput: (input: string) => void; setInput: (input: string) => void;
setFiles: (files: FileList | undefined) => void; setFiles: (files: FileList | undefined) => void;
onDisplayChart: (xml: string) => void;
} }
export function ChatMessageDisplay({ export function ChatMessageDisplay({
chartXML,
messages, messages,
error, error,
setInput, setInput,
setFiles, setFiles,
onDisplayChart,
}: ChatMessageDisplayProps) { }: ChatMessageDisplayProps) {
const { chartXML, loadDiagram: onDisplayChart } = useDiagram();
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const previousXML = useRef<string>(""); const previousXML = useRef<string>("");
useEffect(() => { useEffect(() => {

View File

@@ -13,21 +13,23 @@ import {
import { useChat } from "@ai-sdk/react"; import { useChat } from "@ai-sdk/react";
import { ChatInput } from "@/components/chat-input"; import { ChatInput } from "@/components/chat-input";
import { ChatMessageDisplay } from "./chat-message-display"; import { ChatMessageDisplay } from "./chat-message-display";
interface ChatPanelProps { import { useDiagram } from "@/contexts/diagram-context";
chartXML: string;
onDisplayChart: (xml: string) => void;
onFetchChart: () => Promise<string>;
diagramHistory?: { svg: string; xml: string }[];
onAddToHistory?: () => void;
}
export default function ChatPanel({ export default function ChatPanel() {
chartXML, const {
onDisplayChart, chartXML,
onFetchChart, loadDiagram: onDisplayChart,
diagramHistory = [], handleExport: onExport,
onAddToHistory = () => {}, resolverRef,
}: ChatPanelProps) { diagramHistory,
} = useDiagram();
const onFetchChart = () => {
return new Promise<string>((resolve) => {
resolverRef.current = resolve; // Store the resolver
onExport(); // Trigger the export
});
};
// Add a step counter to track updates // Add a step counter to track updates
// Add state for file attachments // Add state for file attachments
@@ -106,12 +108,10 @@ export default function ChatPanel({
</CardHeader> </CardHeader>
<CardContent className="flex-grow overflow-hidden px-2"> <CardContent className="flex-grow overflow-hidden px-2">
<ChatMessageDisplay <ChatMessageDisplay
chartXML={chartXML}
messages={messages} messages={messages}
error={error} error={error}
setInput={setInput} setInput={setInput}
setFiles={handleFileChange} setFiles={handleFileChange}
onDisplayChart={onDisplayChart}
/> />
</CardContent> </CardContent>
@@ -122,11 +122,8 @@ export default function ChatPanel({
onSubmit={onFormSubmit} onSubmit={onFormSubmit}
onChange={handleInputChange} onChange={handleInputChange}
setMessages={setMessages} setMessages={setMessages}
onDisplayChart={onDisplayChart}
files={files} files={files}
onFileChange={handleFileChange} onFileChange={handleFileChange}
diagramHistory={diagramHistory}
onSelectHistoryItem={handleSelectHistoryItem}
showHistory={showHistory} showHistory={showHistory}
setShowHistory={setShowHistory} setShowHistory={setShowHistory}
/> />

View File

@@ -0,0 +1,86 @@
"use client";
import React, { createContext, useContext, useRef, useState } from "react";
import type { DrawIoEmbedRef } from "react-drawio";
import { extractDiagramXML } from "@/app/extract_xml";
interface DiagramContextType {
chartXML: string;
latestSvg: string;
diagramHistory: { svg: string; xml: string }[];
loadDiagram: (chart: string) => void;
handleExport: () => void;
resolverRef: React.MutableRefObject<((value: string) => void) | null>;
drawioRef: React.MutableRefObject<DrawIoEmbedRef | null>;
handleDiagramExport: (data: any) => void;
}
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
export function DiagramProvider({ children }: { children: React.ReactNode }) {
const [chartXML, setChartXML] = useState<string>("");
const [latestSvg, setLatestSvg] = useState<string>("");
const [diagramHistory, setDiagramHistory] = useState<
{ svg: string; xml: string }[]
>([]);
const drawioRef = useRef<DrawIoEmbedRef>(null);
const resolverRef = useRef<((value: string) => void) | null>(null);
const handleExport = () => {
if (drawioRef.current) {
drawioRef.current.exportDiagram({
format: "xmlsvg",
});
}
};
const loadDiagram = (chart: string) => {
if (drawioRef.current) {
drawioRef.current.load({
xml: chart,
});
}
};
const handleDiagramExport = (data: any) => {
const extractedXML = extractDiagramXML(data.data);
setChartXML(extractedXML);
setLatestSvg(data.data);
setDiagramHistory((prev) => [
...prev,
{
svg: data.data,
xml: extractedXML,
},
]);
if (resolverRef.current) {
resolverRef.current(extractedXML);
resolverRef.current = null;
}
};
return (
<DiagramContext.Provider
value={{
chartXML,
latestSvg,
diagramHistory,
loadDiagram,
handleExport,
resolverRef,
drawioRef,
handleDiagramExport,
}}
>
{children}
</DiagramContext.Provider>
);
}
export function useDiagram() {
const context = useContext(DiagramContext);
if (context === undefined) {
throw new Error("useDiagram must be used within a DiagramProvider");
}
return context;
}