|
@@ -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<AppProps, AppState> {
|
|
|
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<AppProps, AppState> {
|
|
|
}
|
|
|
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}
|
|
|
</LayerUI>
|
|
@@ -1789,7 +1782,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
private magicGenerations = new Map<
|
|
|
ExcalidrawIframeElement["id"],
|
|
|
- MagicCacheData
|
|
|
+ MagicGenerationData
|
|
|
>();
|
|
|
|
|
|
private updateMagicGeneration = ({
|
|
@@ -1797,7 +1790,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
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<AppProps, AppState> {
|
|
|
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<App["plugins"]>) {
|
|
|
+ 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<AppProps, AppState> {
|
|
|
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("<!DOCTYPE html>") && html.includes("</html>")
|
|
|
+ ? html.slice(
|
|
|
+ html.indexOf("<!DOCTYPE html>"),
|
|
|
+ html.indexOf("</html>") + "</html>".length,
|
|
|
+ )
|
|
|
+ : html;
|
|
|
|
|
|
- if (!result.ok) {
|
|
|
- trackEvent("ai", "generate (failed)", "d2c");
|
|
|
- console.error(result.error);
|
|
|
this.updateMagicGeneration({
|
|
|
frameElement,
|
|
|
- data: {
|
|
|
- status: "error",
|
|
|
- code: "ERR_OAI",
|
|
|
- message: result.error?.message || "Unknown error during generation",
|
|
|
- },
|
|
|
+ data: { status: "done", html: parsedHtml },
|
|
|
});
|
|
|
- return;
|
|
|
- }
|
|
|
- trackEvent("ai", "generate (success)", "d2c");
|
|
|
-
|
|
|
- if (result.choices[0].message.content == null) {
|
|
|
+ } catch (error: any) {
|
|
|
+ trackEvent("ai", "generate (failed)", "d2c");
|
|
|
this.updateMagicGeneration({
|
|
|
frameElement,
|
|
|
data: {
|
|
|
status: "error",
|
|
|
code: "ERR_OAI",
|
|
|
- message: "Nothing genereated :(",
|
|
|
+ message: error.message || "Unknown error during generation",
|
|
|
},
|
|
|
});
|
|
|
- return;
|
|
|
}
|
|
|
-
|
|
|
- const message = result.choices[0].message.content;
|
|
|
-
|
|
|
- const html = message.slice(
|
|
|
- message.indexOf("<!DOCTYPE html>"),
|
|
|
- message.indexOf("</html>") + "</html>".length,
|
|
|
- );
|
|
|
-
|
|
|
- this.updateMagicGeneration({
|
|
|
- frameElement,
|
|
|
- data: { status: "done", html },
|
|
|
- });
|
|
|
}
|
|
|
|
|
|
private onIframeSrcCopy(element: ExcalidrawIframeElement) {
|
|
@@ -1958,70 +1928,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private OPENAI_KEY: string | null = EditorLocalStorage.get(
|
|
|
- EDITOR_LS_KEYS.OAI_API_KEY,
|
|
|
- );
|
|
|
- private OPENAI_KEY_IS_PERSISTED: boolean =
|
|
|
- EditorLocalStorage.has(EDITOR_LS_KEYS.OAI_API_KEY) || false;
|
|
|
-
|
|
|
- private onOpenAIKeyChange = (
|
|
|
- openAIKey: string | null,
|
|
|
- shouldPersist: boolean,
|
|
|
- ) => {
|
|
|
- this.OPENAI_KEY = openAIKey || null;
|
|
|
- if (shouldPersist) {
|
|
|
- const didPersist = EditorLocalStorage.set(
|
|
|
- EDITOR_LS_KEYS.OAI_API_KEY,
|
|
|
- openAIKey,
|
|
|
- );
|
|
|
- this.OPENAI_KEY_IS_PERSISTED = didPersist;
|
|
|
- } else {
|
|
|
- this.OPENAI_KEY_IS_PERSISTED = false;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- private onMagicSettingsConfirm = (
|
|
|
- apiKey: string,
|
|
|
- shouldPersist: boolean,
|
|
|
- source: "tool" | "generation" | "settings",
|
|
|
- ) => {
|
|
|
- this.OPENAI_KEY = apiKey || null;
|
|
|
- this.onOpenAIKeyChange(this.OPENAI_KEY, shouldPersist);
|
|
|
-
|
|
|
- if (source === "settings") {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const selectedElements = this.scene.getSelectedElements({
|
|
|
- selectedElementIds: this.state.selectedElementIds,
|
|
|
- });
|
|
|
-
|
|
|
- if (apiKey) {
|
|
|
- if (selectedElements.length) {
|
|
|
- this.onMagicframeToolSelect();
|
|
|
- } else {
|
|
|
- this.setActiveTool({ type: "magicframe" });
|
|
|
- }
|
|
|
- } else if (!isMagicFrameElement(selectedElements[0])) {
|
|
|
- // even if user didn't end up setting api key, let's pick the tool
|
|
|
- // so they can draw up a frame and move forward
|
|
|
- this.setActiveTool({ type: "magicframe" });
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
public onMagicframeToolSelect = () => {
|
|
|
- if (!this.OPENAI_KEY) {
|
|
|
- this.setState({
|
|
|
- openDialog: {
|
|
|
- name: "settings",
|
|
|
- tab: "diagram-to-code",
|
|
|
- source: "tool",
|
|
|
- },
|
|
|
- });
|
|
|
- trackEvent("ai", "tool-select (missing key)", "d2c");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
const selectedElements = this.scene.getSelectedElements({
|
|
|
selectedElementIds: this.state.selectedElementIds,
|
|
|
});
|