mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
chore: add Biome for formatting and linting (#116)
- Add Biome as formatter and linter (replaces Prettier) - Configure Husky + lint-staged for pre-commit hooks - Add VS Code settings for format on save - Ignore components/ui/ (shadcn generated code) - Remove semicolons, use 4-space indent - Reformat all files to new style
This commit is contained in:
@@ -1,85 +1,90 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useRef, useState } from "react";
|
||||
import type { DrawIoEmbedRef } from "react-drawio";
|
||||
import { extractDiagramXML } from "../lib/utils";
|
||||
import type { ExportFormat } from "@/components/save-dialog";
|
||||
import type React from "react"
|
||||
import { createContext, useContext, useRef, useState } from "react"
|
||||
import type { DrawIoEmbedRef } from "react-drawio"
|
||||
import type { ExportFormat } from "@/components/save-dialog"
|
||||
import { extractDiagramXML } from "../lib/utils"
|
||||
|
||||
interface DiagramContextType {
|
||||
chartXML: string;
|
||||
latestSvg: string;
|
||||
diagramHistory: { svg: string; xml: string }[];
|
||||
loadDiagram: (chart: string) => void;
|
||||
handleExport: () => void;
|
||||
handleExportWithoutHistory: () => void;
|
||||
resolverRef: React.Ref<((value: string) => void) | null>;
|
||||
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
||||
handleDiagramExport: (data: any) => void;
|
||||
clearDiagram: () => void;
|
||||
saveDiagramToFile: (filename: string, format: ExportFormat, sessionId?: string) => void;
|
||||
chartXML: string
|
||||
latestSvg: string
|
||||
diagramHistory: { svg: string; xml: string }[]
|
||||
loadDiagram: (chart: string) => void
|
||||
handleExport: () => void
|
||||
handleExportWithoutHistory: () => void
|
||||
resolverRef: React.Ref<((value: string) => void) | null>
|
||||
drawioRef: React.Ref<DrawIoEmbedRef | null>
|
||||
handleDiagramExport: (data: any) => void
|
||||
clearDiagram: () => void
|
||||
saveDiagramToFile: (
|
||||
filename: string,
|
||||
format: ExportFormat,
|
||||
sessionId?: string,
|
||||
) => void
|
||||
}
|
||||
|
||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
||||
|
||||
export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
const [chartXML, setChartXML] = useState<string>("");
|
||||
const [latestSvg, setLatestSvg] = useState<string>("");
|
||||
const [chartXML, setChartXML] = useState<string>("")
|
||||
const [latestSvg, setLatestSvg] = useState<string>("")
|
||||
const [diagramHistory, setDiagramHistory] = useState<
|
||||
{ svg: string; xml: string }[]
|
||||
>([]);
|
||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null);
|
||||
const resolverRef = useRef<((value: string) => void) | null>(null);
|
||||
>([])
|
||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
||||
const resolverRef = useRef<((value: string) => void) | null>(null)
|
||||
// Track if we're expecting an export for history (user-initiated)
|
||||
const expectHistoryExportRef = useRef<boolean>(false);
|
||||
const expectHistoryExportRef = useRef<boolean>(false)
|
||||
// Track if we're expecting an export for file save (stores raw export data)
|
||||
const saveResolverRef = useRef<{
|
||||
resolver: ((data: string) => void) | null;
|
||||
format: ExportFormat | null;
|
||||
}>({ resolver: null, format: null });
|
||||
resolver: ((data: string) => void) | null
|
||||
format: ExportFormat | null
|
||||
}>({ resolver: null, format: null })
|
||||
|
||||
const handleExport = () => {
|
||||
if (drawioRef.current) {
|
||||
// Mark that this export should be saved to history
|
||||
expectHistoryExportRef.current = true;
|
||||
expectHistoryExportRef.current = true
|
||||
drawioRef.current.exportDiagram({
|
||||
format: "xmlsvg",
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleExportWithoutHistory = () => {
|
||||
if (drawioRef.current) {
|
||||
// Export without saving to history (for edit_diagram fetching current state)
|
||||
drawioRef.current.exportDiagram({
|
||||
format: "xmlsvg",
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const loadDiagram = (chart: string) => {
|
||||
if (drawioRef.current) {
|
||||
drawioRef.current.load({
|
||||
xml: chart,
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDiagramExport = (data: any) => {
|
||||
// Handle save to file if requested (process raw data before extraction)
|
||||
if (saveResolverRef.current.resolver) {
|
||||
const format = saveResolverRef.current.format;
|
||||
saveResolverRef.current.resolver(data.data);
|
||||
saveResolverRef.current = { resolver: null, format: null };
|
||||
const format = saveResolverRef.current.format
|
||||
saveResolverRef.current.resolver(data.data)
|
||||
saveResolverRef.current = { resolver: null, format: null }
|
||||
// For non-xmlsvg formats, skip XML extraction as it will fail
|
||||
// Only drawio (which uses xmlsvg internally) has the content attribute
|
||||
if (format === "png" || format === "svg") {
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const extractedXML = extractDiagramXML(data.data);
|
||||
setChartXML(extractedXML);
|
||||
setLatestSvg(data.data);
|
||||
const extractedXML = extractDiagramXML(data.data)
|
||||
setChartXML(extractedXML)
|
||||
setLatestSvg(data.data)
|
||||
|
||||
// Only add to history if this was a user-initiated export
|
||||
if (expectHistoryExportRef.current) {
|
||||
@@ -89,106 +94,117 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
svg: data.data,
|
||||
xml: extractedXML,
|
||||
},
|
||||
]);
|
||||
expectHistoryExportRef.current = false;
|
||||
])
|
||||
expectHistoryExportRef.current = false
|
||||
}
|
||||
|
||||
if (resolverRef.current) {
|
||||
resolverRef.current(extractedXML);
|
||||
resolverRef.current = null;
|
||||
resolverRef.current(extractedXML)
|
||||
resolverRef.current = null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const clearDiagram = () => {
|
||||
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`;
|
||||
loadDiagram(emptyDiagram);
|
||||
setChartXML(emptyDiagram);
|
||||
setLatestSvg("");
|
||||
setDiagramHistory([]);
|
||||
};
|
||||
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
|
||||
loadDiagram(emptyDiagram)
|
||||
setChartXML(emptyDiagram)
|
||||
setLatestSvg("")
|
||||
setDiagramHistory([])
|
||||
}
|
||||
|
||||
const saveDiagramToFile = (filename: string, format: ExportFormat, sessionId?: string) => {
|
||||
const saveDiagramToFile = (
|
||||
filename: string,
|
||||
format: ExportFormat,
|
||||
sessionId?: string,
|
||||
) => {
|
||||
if (!drawioRef.current) {
|
||||
console.warn("Draw.io editor not ready");
|
||||
return;
|
||||
console.warn("Draw.io editor not ready")
|
||||
return
|
||||
}
|
||||
|
||||
// Map format to draw.io export format
|
||||
const drawioFormat = format === "drawio" ? "xmlsvg" : format;
|
||||
const drawioFormat = format === "drawio" ? "xmlsvg" : format
|
||||
|
||||
// Set up the resolver before triggering export
|
||||
saveResolverRef.current = {
|
||||
resolver: (exportData: string) => {
|
||||
let fileContent: string | Blob;
|
||||
let mimeType: string;
|
||||
let extension: string;
|
||||
let fileContent: string | Blob
|
||||
let mimeType: string
|
||||
let extension: string
|
||||
|
||||
if (format === "drawio") {
|
||||
// Extract XML from SVG for .drawio format
|
||||
const xml = extractDiagramXML(exportData);
|
||||
let xmlContent = xml;
|
||||
const xml = extractDiagramXML(exportData)
|
||||
let xmlContent = xml
|
||||
if (!xml.includes("<mxfile")) {
|
||||
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
|
||||
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`
|
||||
}
|
||||
fileContent = xmlContent;
|
||||
mimeType = "application/xml";
|
||||
extension = ".drawio";
|
||||
fileContent = xmlContent
|
||||
mimeType = "application/xml"
|
||||
extension = ".drawio"
|
||||
} else if (format === "png") {
|
||||
// PNG data comes as base64 data URL
|
||||
fileContent = exportData;
|
||||
mimeType = "image/png";
|
||||
extension = ".png";
|
||||
fileContent = exportData
|
||||
mimeType = "image/png"
|
||||
extension = ".png"
|
||||
} else {
|
||||
// SVG format
|
||||
fileContent = exportData;
|
||||
mimeType = "image/svg+xml";
|
||||
extension = ".svg";
|
||||
fileContent = exportData
|
||||
mimeType = "image/svg+xml"
|
||||
extension = ".svg"
|
||||
}
|
||||
|
||||
// Log save event to Langfuse (flags the trace)
|
||||
logSaveToLangfuse(filename, format, sessionId);
|
||||
logSaveToLangfuse(filename, format, sessionId)
|
||||
|
||||
// Handle download
|
||||
let url: string;
|
||||
if (typeof fileContent === "string" && fileContent.startsWith("data:")) {
|
||||
let url: string
|
||||
if (
|
||||
typeof fileContent === "string" &&
|
||||
fileContent.startsWith("data:")
|
||||
) {
|
||||
// Already a data URL (PNG)
|
||||
url = fileContent;
|
||||
url = fileContent
|
||||
} else {
|
||||
const blob = new Blob([fileContent], { type: mimeType });
|
||||
url = URL.createObjectURL(blob);
|
||||
const blob = new Blob([fileContent], { type: mimeType })
|
||||
url = URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${filename}${extension}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `${filename}${extension}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
|
||||
// Delay URL revocation to ensure download completes
|
||||
if (!url.startsWith("data:")) {
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100)
|
||||
}
|
||||
},
|
||||
format,
|
||||
};
|
||||
}
|
||||
|
||||
// Export diagram - callback will be handled in handleDiagramExport
|
||||
drawioRef.current.exportDiagram({ format: drawioFormat });
|
||||
};
|
||||
drawioRef.current.exportDiagram({ format: drawioFormat })
|
||||
}
|
||||
|
||||
// Log save event to Langfuse (just flags the trace, doesn't send content)
|
||||
const logSaveToLangfuse = async (filename: string, format: string, sessionId?: string) => {
|
||||
const logSaveToLangfuse = async (
|
||||
filename: string,
|
||||
format: string,
|
||||
sessionId?: string,
|
||||
) => {
|
||||
try {
|
||||
await fetch("/api/log-save", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ filename, format, sessionId }),
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("Failed to log save to Langfuse:", error);
|
||||
console.warn("Failed to log save to Langfuse:", error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<DiagramContext.Provider
|
||||
@@ -208,13 +224,13 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{children}
|
||||
</DiagramContext.Provider>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function useDiagram() {
|
||||
const context = useContext(DiagramContext);
|
||||
const context = useContext(DiagramContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useDiagram must be used within a DiagramProvider");
|
||||
throw new Error("useDiagram must be used within a DiagramProvider")
|
||||
}
|
||||
return context;
|
||||
return context
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user