mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
refactor: extract all states to diagram-context.
This commit is contained in:
87
app/page.tsx
87
app/page.tsx
@@ -1,59 +1,22 @@
|
|||||||
"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">
|
||||||
|
<div className="h-full relative">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<div className="w-full h-full">
|
||||||
<DrawIoEmbed
|
<DrawIoEmbed
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
onExport={(data) => {
|
onExport={handleDiagramExport}
|
||||||
const extractedXML = extractDiagramXML(data.data);
|
|
||||||
setChartXML(extractedXML);
|
|
||||||
// Store the latest SVG data
|
|
||||||
setLatestSvg(data.data);
|
|
||||||
// Directly update diagramHistory with the new data
|
|
||||||
setDiagramHistory((prev) => [
|
|
||||||
...prev,
|
|
||||||
{ svg: data.data, xml: extractedXML },
|
|
||||||
]);
|
|
||||||
// If there's a pending resolver, resolve it with the fresh XML
|
|
||||||
if (resolverRef.current) {
|
|
||||||
resolverRef.current(extractedXML);
|
|
||||||
resolverRef.current = null;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
urlParameters={{
|
urlParameters={{
|
||||||
spin: true,
|
spin: true,
|
||||||
libraries: false,
|
libraries: false,
|
||||||
@@ -62,22 +25,20 @@ export default function Home() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/3 h-full p-1">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<DiagramProvider>
|
||||||
|
<DiagramPageLayout>
|
||||||
|
<ChatPanel />
|
||||||
|
</DiagramPageLayout>
|
||||||
|
</DiagramProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
const {
|
||||||
chartXML,
|
chartXML,
|
||||||
onDisplayChart,
|
loadDiagram: onDisplayChart,
|
||||||
onFetchChart,
|
handleExport: onExport,
|
||||||
diagramHistory = [],
|
resolverRef,
|
||||||
onAddToHistory = () => {},
|
diagramHistory,
|
||||||
}: ChatPanelProps) {
|
} = 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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
86
contexts/diagram-context.tsx
Normal file
86
contexts/diagram-context.tsx
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user