mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: rewrite d2c to not require token (#8269)
This commit is contained in:
parent
fb4bb29aa5
commit
b5d7f5b4ba
19 changed files with 282 additions and 564 deletions
|
@ -83,7 +83,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,
|
||||
ExcalidrawNonSelectionElement,
|
||||
ExcalidrawArrowElement,
|
||||
} from "../element/types";
|
||||
|
@ -257,6 +257,7 @@ import type {
|
|||
UnsubscribeCallback,
|
||||
EmbedsValidationStatus,
|
||||
ElementsPendingErasure,
|
||||
GenerateDiagramToCode,
|
||||
NullableGridSize,
|
||||
} from "../types";
|
||||
import {
|
||||
|
@ -405,13 +406,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";
|
||||
|
@ -1018,7 +1015,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",
|
||||
|
@ -1559,10 +1556,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>
|
||||
|
@ -1807,7 +1800,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
private magicGenerations = new Map<
|
||||
ExcalidrawIframeElement["id"],
|
||||
MagicCacheData
|
||||
MagicGenerationData
|
||||
>();
|
||||
|
||||
private updateMagicGeneration = ({
|
||||
|
@ -1815,7 +1808,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
|
||||
|
@ -1838,31 +1831,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;
|
||||
}
|
||||
|
||||
|
@ -1901,68 +1889,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);
|
||||
|
||||
const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);
|
||||
|
||||
trackEvent("ai", "generate (start)", "d2c");
|
||||
try {
|
||||
const { html } = await generateDiagramToCode({
|
||||
frame: magicFrame,
|
||||
children: magicFrameChildren,
|
||||
});
|
||||
|
||||
const result = await diagramToHTML({
|
||||
image: dataURL,
|
||||
apiKey: this.OPENAI_KEY,
|
||||
text: textFromFrameChildren,
|
||||
theme: this.state.theme,
|
||||
});
|
||||
trackEvent("ai", "generate (success)", "d2c");
|
||||
|
||||
if (!result.ok) {
|
||||
if (!html.trim()) {
|
||||
this.updateMagicGeneration({
|
||||
frameElement,
|
||||
data: {
|
||||
status: "error",
|
||||
code: "ERR_OAI",
|
||||
message: "Nothing genereated :(",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedHtml =
|
||||
html.includes("<!DOCTYPE html>") && html.includes("</html>")
|
||||
? html.slice(
|
||||
html.indexOf("<!DOCTYPE html>"),
|
||||
html.indexOf("</html>") + "</html>".length,
|
||||
)
|
||||
: html;
|
||||
|
||||
this.updateMagicGeneration({
|
||||
frameElement,
|
||||
data: { status: "done", html: parsedHtml },
|
||||
});
|
||||
} catch (error: any) {
|
||||
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",
|
||||
message: error.message || "Unknown error during generation",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
trackEvent("ai", "generate (success)", "d2c");
|
||||
|
||||
if (result.choices[0].message.content == null) {
|
||||
this.updateMagicGeneration({
|
||||
frameElement,
|
||||
data: {
|
||||
status: "error",
|
||||
code: "ERR_OAI",
|
||||
message: "Nothing genereated :(",
|
||||
},
|
||||
});
|
||||
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) {
|
||||
|
@ -1976,70 +1946,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,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue