mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 03:59:58 +08:00
The canvas now fills the window and the chat floats on its right. draw.io is served from our own origin (public/drawio, downloaded at build time and checked against the release's SHA-256), so the app can drive the editor directly: - every AI change becomes a version card with a thumbnail; compare, restore or undo it, and Ctrl+Z on the canvas takes an AI change back in one step - shapes the AI just changed are highlighted on the canvas - selected shapes appear as a chip and are sent to the model - the app's own canvas toolbar, shape library and page tabs replace draw.io's chrome - new start screen, session rename and switcher, open .drawio files - settings split into Models & keys, Appearance, Advanced and About - phones switch between canvas and chat - dark mode keeps the diagram on a white sheet and no longer flashes With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the app falls back to draw.io's own toolbar and the features above that need same origin are off. The chat logic moved from the 1500-line chat panel into a chat engine plus small UI components; zustand holds canvas, versions, settings and UI state.
117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
import { getAssetUrl } from "@/lib/base-path"
|
|
import type { Locale } from "@/lib/i18n/config"
|
|
import { BLANK_MXFILE } from "@/packages/mcp-server/src/pages.ts"
|
|
|
|
/** External draw.io configured at build time (cross-origin, limited features) */
|
|
const EXTERNAL_DRAWIO_URL = process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || ""
|
|
|
|
/**
|
|
* URL of the draw.io editor iframe. By default this is the copy bundled in
|
|
* public/drawio, served from our own origin.
|
|
*/
|
|
export function getDrawioSrc({
|
|
lang,
|
|
isElectron,
|
|
}: {
|
|
lang: Locale
|
|
isElectron: boolean
|
|
}): string {
|
|
// index.html: Next.js does not serve directory indexes
|
|
const base =
|
|
EXTERNAL_DRAWIO_URL ||
|
|
`${window.location.origin}${getAssetUrl("/drawio/index.html")}`
|
|
const url = new URL(base)
|
|
const params: Record<string, string> = {
|
|
embed: "1",
|
|
proto: "json",
|
|
configure: "1",
|
|
ui: "simple",
|
|
spin: "0",
|
|
libraries: "0",
|
|
noSaveBtn: "1",
|
|
noExitBtn: "1",
|
|
saveAndExit: "0",
|
|
// The diagram always renders on a light sheet; dark mode only
|
|
// recolors the table around it
|
|
dark: "0",
|
|
// draw.io names Traditional Chinese "zh-tw"
|
|
lang: lang === "zh-Hant" ? "zh-tw" : lang,
|
|
}
|
|
// No calls to external services from the desktop app
|
|
if (isElectron && !EXTERNAL_DRAWIO_URL) params.offline = "1"
|
|
for (const [key, value] of Object.entries(params)) {
|
|
url.searchParams.set(key, value)
|
|
}
|
|
return url.toString()
|
|
}
|
|
|
|
/**
|
|
* Show the page (a white sheet with a grid) unless the diagram says
|
|
* otherwise. AI output usually has a bare <mxGraphModel>, and draw.io's
|
|
* simple UI would then show neither page nor grid.
|
|
*/
|
|
export function withPageDefaults(xml: string): string {
|
|
return xml.replace(
|
|
/<mxGraphModel\b([^>]*?)(\/?)>/g,
|
|
(_match, attrs, slash) => {
|
|
let next = attrs as string
|
|
if (!/\spage\s*=/.test(next)) next += ' page="1"'
|
|
if (!/\sgrid\s*=/.test(next)) next += ' grid="1"'
|
|
if (!/\sgridSize\s*=/.test(next)) next += ' gridSize="10"'
|
|
return `<mxGraphModel${next}${slash}>`
|
|
},
|
|
)
|
|
}
|
|
|
|
/** Empty document loaded when draw.io starts */
|
|
export const EMPTY_DRAWIO_DOCUMENT = withPageDefaults(BLANK_MXFILE)
|
|
|
|
/**
|
|
* Editor defaults sent with draw.io's configure message. The simple UI turns
|
|
* page view off for new diagrams; keep the white page on the canvas, as in
|
|
* the classic UI.
|
|
*/
|
|
export const DRAWIO_CONFIG = {
|
|
defaultPageVisible: true,
|
|
defaultGridEnabled: true,
|
|
}
|
|
|
|
/**
|
|
* CSS injected into draw.io. `customChrome` hides draw.io's own toolbar and
|
|
* page tabs because the app draws its own over the canvas.
|
|
*/
|
|
export function getDrawioCss({
|
|
dark,
|
|
customChrome,
|
|
}: {
|
|
dark: boolean
|
|
customChrome: boolean
|
|
}): string {
|
|
const workspace = dark ? "#0f2440" : "#f4f5f7"
|
|
// draw.io itself stays light, so its focus color does too
|
|
const accent = "#14181d"
|
|
let css = `
|
|
:root {
|
|
--workspace-color: ${workspace};
|
|
--focus-color: ${accent};
|
|
}
|
|
.geDiagramContainer { background-color: ${workspace}; }
|
|
.geBackgroundPage { box-shadow: 0 1px 2px rgba(20,24,29,.08), 0 12px 32px -12px rgba(20,24,29,${dark ? ".5" : ".18"}) !important; }
|
|
`
|
|
if (customChrome) {
|
|
css += `
|
|
.geSimpleMainMenu, .geTabContainer, .geFooterContainer, .geStatus,
|
|
body > .geToolbarContainer, .geHsplit,
|
|
.geSidebarContainer:not(.geFormatContainer) { display: none !important; }
|
|
.geDiagramContainer { top: 0 !important; bottom: 0 !important; left: 0 !important; border: 0 !important; }
|
|
.geFormatContainer { top: 64px !important; bottom: 64px !important; border-radius: 14px 0 0 14px; box-shadow: 0 10px 30px -8px rgba(20,24,29,.2); border: 0 !important; }
|
|
body.nai-shapes-open .geSidebarContainer:not(.geFormatContainer) { display: block !important; top: 64px !important; bottom: 64px !important; left: 12px !important; width: 248px !important; transform: none !important; border-radius: 14px; box-shadow: 0 10px 30px -8px rgba(20,24,29,.2); border: 0 !important; z-index: 3; }
|
|
`
|
|
} else {
|
|
css += `
|
|
.geSimpleMainMenu { border-bottom: 1px solid #e4e7eb !important; box-shadow: none !important; }
|
|
`
|
|
}
|
|
return css
|
|
}
|