diff --git a/excalidraw-app/components/AI.tsx b/excalidraw-app/components/AI.tsx
new file mode 100644
index 0000000000..f9d3b15513
--- /dev/null
+++ b/excalidraw-app/components/AI.tsx
@@ -0,0 +1,152 @@
+import type { ExcalidrawImperativeAPI } from "../../packages/excalidraw/types";
+import {
+ DiagramToCodePlugin,
+ exportToBlob,
+ getTextFromElements,
+ MIME_TYPES,
+ TTDDialog,
+} from "../../packages/excalidraw";
+import { getDataURL } from "../../packages/excalidraw/data/blob";
+import { safelyParseJSON } from "../../packages/excalidraw/utils";
+
+export const AIComponents = ({
+ excalidrawAPI,
+}: {
+ excalidrawAPI: ExcalidrawImperativeAPI;
+}) => {
+ return (
+ <>
+
{
+ const appState = excalidrawAPI.getAppState();
+
+ const blob = await exportToBlob({
+ elements: children,
+ appState: {
+ ...appState,
+ exportBackground: true,
+ viewBackgroundColor: appState.viewBackgroundColor,
+ },
+ exportingFrame: frame,
+ files: excalidrawAPI.getFiles(),
+ mimeType: MIME_TYPES.jpg,
+ });
+
+ const dataURL = await getDataURL(blob);
+
+ const textFromFrameChildren = getTextFromElements(children);
+
+ const response = await fetch(
+ `${
+ import.meta.env.VITE_APP_AI_BACKEND
+ }/v1/ai/diagram-to-code/generate`,
+ {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ texts: textFromFrameChildren,
+ image: dataURL,
+ theme: appState.theme,
+ }),
+ },
+ );
+
+ if (!response.ok) {
+ const text = await response.text();
+ const error = safelyParseJSON(text);
+
+ if (!error) {
+ throw new Error(text);
+ }
+
+ if (error.statusCode === 429) {
+ return {
+ html: `
+
+
+
Too many requests today,please try again tomorrow!
+
+
+
+
+
+ `,
+ };
+ }
+
+ throw new Error(error.message || text);
+ }
+
+ const html = await response.text();
+
+ return {
+ html,
+ };
+ }}
+ />
+
+ {
+ try {
+ const response = await fetch(
+ `${
+ import.meta.env.VITE_APP_AI_BACKEND
+ }/v1/ai/text-to-diagram/generate`,
+ {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ prompt: input }),
+ },
+ );
+
+ const rateLimit = response.headers.has("X-Ratelimit-Limit")
+ ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
+ : undefined;
+
+ const rateLimitRemaining = response.headers.has(
+ "X-Ratelimit-Remaining",
+ )
+ ? parseInt(
+ response.headers.get("X-Ratelimit-Remaining") || "0",
+ 10,
+ )
+ : undefined;
+
+ const json = await response.json();
+
+ if (!response.ok) {
+ if (response.status === 429) {
+ return {
+ rateLimit,
+ rateLimitRemaining,
+ error: new Error(
+ "Too many requests today, please try again tomorrow!",
+ ),
+ };
+ }
+
+ throw new Error(json.message || "Generation failed...");
+ }
+
+ const generatedResponse = json.generatedResponse;
+ if (!generatedResponse) {
+ throw new Error("Generation failed...");
+ }
+
+ return { generatedResponse, rateLimit, rateLimitRemaining };
+ } catch (err: any) {
+ throw new Error("Request failed");
+ }
+ }}
+ />
+ >
+ );
+};
diff --git a/packages/excalidraw/actions/actionClipboard.tsx b/packages/excalidraw/actions/actionClipboard.tsx
index e4f998d016..4d34bf968c 100644
--- a/packages/excalidraw/actions/actionClipboard.tsx
+++ b/packages/excalidraw/actions/actionClipboard.tsx
@@ -10,7 +10,7 @@ import {
} from "../clipboard";
import { actionDeleteSelected } from "./actionDeleteSelected";
import { exportCanvas, prepareElementsForExport } from "../data/index";
-import { isTextElement } from "../element";
+import { getTextFromElements, isTextElement } from "../element";
import { t } from "../i18n";
import { isFirefox } from "../constants";
import { DuplicateIcon, cutIcon, pngIcon, svgIcon } from "../components/icons";
@@ -239,16 +239,8 @@ export const copyText = register({
includeBoundTextElement: true,
});
- const text = selectedElements
- .reduce((acc: string[], element) => {
- if (isTextElement(element)) {
- acc.push(element.text);
- }
- return acc;
- }, [])
- .join("\n\n");
try {
- copyTextToSystemClipboard(text);
+ copyTextToSystemClipboard(getTextFromElements(selectedElements));
} catch (e) {
throw new Error(t("errors.copyToSystemClipboardFailed"));
}
diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx
index c49b4a5f0f..774f19038e 100644
--- a/packages/excalidraw/components/Actions.tsx
+++ b/packages/excalidraw/components/Actions.tsx
@@ -44,7 +44,6 @@ import {
frameToolIcon,
mermaidLogoIcon,
laserPointerToolIcon,
- OpenAIIcon,
MagicIcon,
} from "./icons";
import { KEYS } from "../keys";
@@ -395,7 +394,7 @@ export const ShapesSwitcher = ({
>
{t("toolBar.mermaidToExcalidraw")}
- {app.props.aiEnabled !== false && (
+ {app.props.aiEnabled !== false && app.plugins.diagramToCode && (
<>
app.onMagicframeToolSelect()}
@@ -405,20 +404,6 @@ export const ShapesSwitcher = ({
{t("toolBar.magicframe")}
AI
- {
- trackEvent("ai", "open-settings", "d2c");
- app.setOpenDialog({
- name: "settings",
- source: "settings",
- tab: "diagram-to-code",
- });
- }}
- icon={OpenAIIcon}
- data-testid="toolbar-magicSettings"
- >
- {t("toolBar.magicSettings")}
-
>
)}
diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx
index 093f6bfdfe..2aa299bc24 100644
--- a/packages/excalidraw/components/App.tsx
+++ b/packages/excalidraw/components/App.tsx
@@ -85,7 +85,6 @@ import {
ZOOM_STEP,
POINTER_EVENTS,
TOOL_TYPE,
- EDITOR_LS_KEYS,
isIOS,
supportsResizeObserver,
DEFAULT_COLLISION_THRESHOLD,
@@ -183,6 +182,7 @@ import type {
ExcalidrawIframeElement,
ExcalidrawEmbeddableElement,
Ordered,
+ MagicGenerationData,
} from "../element/types";
import { getCenter, getDistance } from "../gesture";
import {
@@ -253,6 +253,7 @@ import type {
UnsubscribeCallback,
EmbedsValidationStatus,
ElementsPendingErasure,
+ GenerateDiagramToCode,
} from "../types";
import {
debounce,
@@ -399,13 +400,9 @@ import {
} from "../cursor";
import { Emitter } from "../emitter";
import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
-import type { MagicCacheData } from "../data/magic";
-import { diagramToHTML } from "../data/magic";
-import { exportToBlob } from "../../utils/export";
import { COLOR_PALETTE } from "../colors";
import { ElementCanvasButton } from "./MagicButton";
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
-import { EditorLocalStorage } from "../data/EditorLocalStorage";
import FollowMode from "./FollowMode/FollowMode";
import { Store, StoreAction } from "../store";
import { AnimationFrameHandler } from "../animation-frame-handler";
@@ -993,7 +990,7 @@ class App extends React.Component {
if (isIframeElement(el)) {
src = null;
- const data: MagicCacheData = (el.customData?.generationData ??
+ const data: MagicGenerationData = (el.customData?.generationData ??
this.magicGenerations.get(el.id)) || {
status: "error",
message: "No generation data",
@@ -1543,10 +1540,6 @@ class App extends React.Component {
}
app={this}
isCollaborating={this.props.isCollaborating}
- openAIKey={this.OPENAI_KEY}
- isOpenAIKeyPersisted={this.OPENAI_KEY_IS_PERSISTED}
- onOpenAIAPIKeyChange={this.onOpenAIKeyChange}
- onMagicSettingsConfirm={this.onMagicSettingsConfirm}
>
{this.props.children}
@@ -1789,7 +1782,7 @@ class App extends React.Component {
private magicGenerations = new Map<
ExcalidrawIframeElement["id"],
- MagicCacheData
+ MagicGenerationData
>();
private updateMagicGeneration = ({
@@ -1797,7 +1790,7 @@ class App extends React.Component {
data,
}: {
frameElement: ExcalidrawIframeElement;
- data: MagicCacheData;
+ data: MagicGenerationData;
}) => {
if (data.status === "pending") {
// We don't wanna persist pending state to storage. It should be in-app
@@ -1820,31 +1813,26 @@ class App extends React.Component {
this.triggerRender();
};
- private getTextFromElements(elements: readonly ExcalidrawElement[]) {
- const text = elements
- .reduce((acc: string[], element) => {
- if (isTextElement(element)) {
- acc.push(element.text);
- }
- return acc;
- }, [])
- .join("\n\n");
- return text;
+ public plugins: {
+ diagramToCode?: {
+ generate: GenerateDiagramToCode;
+ };
+ } = {};
+
+ public setPlugins(plugins: Partial) {
+ Object.assign(this.plugins, plugins);
}
private async onMagicFrameGenerate(
magicFrame: ExcalidrawMagicFrameElement,
source: "button" | "upstream",
) {
- if (!this.OPENAI_KEY) {
+ const generateDiagramToCode = this.plugins.diagramToCode?.generate;
+
+ if (!generateDiagramToCode) {
this.setState({
- openDialog: {
- name: "settings",
- tab: "diagram-to-code",
- source: "generation",
- },
+ errorMessage: "No diagram to code plugin found",
});
- trackEvent("ai", "generate (missing key)", "d2c");
return;
}
@@ -1883,68 +1871,50 @@ class App extends React.Component {
selectedElementIds: { [frameElement.id]: true },
});
- const blob = await exportToBlob({
- elements: this.scene.getNonDeletedElements(),
- appState: {
- ...this.state,
- exportBackground: true,
- viewBackgroundColor: this.state.viewBackgroundColor,
- },
- exportingFrame: magicFrame,
- files: this.files,
- });
-
- const dataURL = await getDataURL(blob);
+ trackEvent("ai", "generate (start)", "d2c");
+ try {
+ const { html } = await generateDiagramToCode({
+ frame: magicFrame,
+ children: magicFrameChildren,
+ });
- const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);
+ trackEvent("ai", "generate (success)", "d2c");
- trackEvent("ai", "generate (start)", "d2c");
+ if (!html.trim()) {
+ this.updateMagicGeneration({
+ frameElement,
+ data: {
+ status: "error",
+ code: "ERR_OAI",
+ message: "Nothing genereated :(",
+ },
+ });
+ return;
+ }
- const result = await diagramToHTML({
- image: dataURL,
- apiKey: this.OPENAI_KEY,
- text: textFromFrameChildren,
- theme: this.state.theme,
- });
+ const parsedHtml =
+ html.includes("") && html.includes("