Skip to content

Commit

Permalink
feat: d2c tweaks (excalidraw#7336)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwelle committed Nov 24, 2023
1 parent c7ee46e commit 3d1631f
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 72 deletions.
9 changes: 7 additions & 2 deletions src/actions/actionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,18 @@ export const actionShortcuts = register({
viewMode: true,
trackEvent: { category: "menu", action: "toggleHelpDialog" },
perform: (_elements, appState, _, { focusContainer }) => {
if (appState.openDialog === "help") {
if (appState.openDialog?.name === "help") {
focusContainer();
}
return {
appState: {
...appState,
openDialog: appState.openDialog === "help" ? null : "help",
openDialog:
appState.openDialog?.name === "help"
? null
: {
name: "help",
},
},
commitToHistory: false,
};
Expand Down
22 changes: 15 additions & 7 deletions src/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
// place here categories that you want to track. We want to track just a
// small subset of categories at a given time.
const ALLOWED_CATEGORIES_TO_TRACK = ["ai"] as string[];

export const trackEvent = (
category: string,
action: string,
label?: string,
value?: number,
) => {
try {
// place here categories that you want to track as events
// KEEP IN MIND THE PRICING
const ALLOWED_CATEGORIES_TO_TRACK = [] as string[];
// Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value });

if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
// prettier-ignore
if (
typeof window === "undefined"
|| import.meta.env.VITE_WORKER_ID
// comment out to debug locally
|| import.meta.env.PROD
) {
return;
}

if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
return;
}

if (!import.meta.env.PROD) {
console.info("trackEvent", { category, action, label, value });
}

if (window.sa_event) {
window.sa_event(action, {
category,
Expand Down
12 changes: 9 additions & 3 deletions src/components/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ export const ShapesSwitcher = ({
Generate
</div>
<DropdownMenu.Item
onSelect={() => app.setOpenDialog("mermaid")}
onSelect={() => app.setOpenDialog({ name: "mermaid" })}
icon={mermaidLogoIcon}
data-testid="toolbar-embeddable"
>
Expand All @@ -349,14 +349,20 @@ export const ShapesSwitcher = ({
{app.props.aiEnabled !== false && (
<>
<DropdownMenu.Item
onSelect={() => app.onMagicButtonSelect()}
onSelect={() => app.onMagicframeToolSelect()}
icon={MagicIcon}
data-testid="toolbar-magicframe"
>
{t("toolBar.magicframe")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => app.setOpenDialog("magicSettings")}
onSelect={() => {
trackEvent("ai", "d2c-settings", "settings");
app.setOpenDialog({
name: "magicSettings",
source: "settings",
});
}}
icon={OpenAIIcon}
data-testid="toolbar-magicSettings"
>
Expand Down
112 changes: 79 additions & 33 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1435,7 +1435,7 @@ class App extends React.Component<AppProps, AppState> {
onMagicSettingsConfirm={this.onMagicSettingsConfirm}
>
{this.props.children}
{this.state.openDialog === "mermaid" && (
{this.state.openDialog?.name === "mermaid" && (
<MermaidToExcalidraw />
)}
</LayerUI>
Expand Down Expand Up @@ -1467,6 +1467,7 @@ class App extends React.Component<AppProps, AppState> {
onChange={() =>
this.onMagicFrameGenerate(
firstSelectedElement,
"button",
)
}
/>
Expand Down Expand Up @@ -1697,11 +1698,15 @@ class App extends React.Component<AppProps, AppState> {
return text;
}

private async onMagicFrameGenerate(magicFrame: ExcalidrawMagicFrameElement) {
private async onMagicFrameGenerate(
magicFrame: ExcalidrawMagicFrameElement,
source: "button" | "upstream",
) {
if (!this.OPENAI_KEY) {
this.setState({
openDialog: "magicSettings",
openDialog: { name: "magicSettings", source: "generation" },
});
trackEvent("ai", "d2c-generate", "missing-key");
return;
}

Expand All @@ -1712,7 +1717,12 @@ class App extends React.Component<AppProps, AppState> {
}).filter((el) => !isMagicFrameElement(el));

if (!magicFrameChildren.length) {
this.setState({ errorMessage: "Cannot generate from an empty frame" });
if (source === "button") {
this.setState({ errorMessage: "Cannot generate from an empty frame" });
trackEvent("ai", "d2c-generate", "no-children");
} else {
this.setActiveTool({ type: "magicframe" });
}
return;
}

Expand Down Expand Up @@ -1751,6 +1761,8 @@ class App extends React.Component<AppProps, AppState> {

const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);

trackEvent("ai", "d2c-generate", "generating");

const result = await diagramToHTML({
image: dataURL,
apiKey: this.OPENAI_KEY,
Expand All @@ -1759,6 +1771,7 @@ class App extends React.Component<AppProps, AppState> {
});

if (!result.ok) {
trackEvent("ai", "d2c-generate", "generating-failed");
console.error(result.error);
this.updateMagicGeneration({
frameElement,
Expand All @@ -1770,6 +1783,7 @@ class App extends React.Component<AppProps, AppState> {
});
return;
}
trackEvent("ai", "d2c-generate", "generating-done");

if (result.choices[0].message.content == null) {
this.updateMagicGeneration({
Expand Down Expand Up @@ -1813,7 +1827,10 @@ class App extends React.Component<AppProps, AppState> {
private OPENAI_KEY_IS_PERSISTED: boolean =
EditorLocalStorage.has(EDITOR_LS_KEYS.OAI_API_KEY) || false;

private onOpenAIKeyChange = (openAIKey: string, shouldPersist: boolean) => {
private onOpenAIKeyChange = (
openAIKey: string | null,
shouldPersist: boolean,
) => {
this.OPENAI_KEY = openAIKey || null;
if (shouldPersist) {
const didPersist = EditorLocalStorage.set(
Expand All @@ -1826,26 +1843,41 @@ class App extends React.Component<AppProps, AppState> {
}
};

private onMagicSettingsConfirm = (apiKey: string, shouldPersist: boolean) => {
this.onOpenAIKeyChange(apiKey, shouldPersist);
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) {
const selectedElements = this.scene.getSelectedElements({
selectedElementIds: this.state.selectedElementIds,
});
if (selectedElements.length) {
this.onMagicButtonSelect();
this.onMagicframeToolSelect();
} else {
this.setActiveTool({ type: "magicframe" });
}
} else {
this.OPENAI_KEY = null;
} 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 onMagicButtonSelect = () => {
public onMagicframeToolSelect = () => {
if (!this.OPENAI_KEY) {
this.setState({
openDialog: "magicSettings",
openDialog: { name: "magicSettings", source: "tool" },
});
trackEvent("ai", "d2c-tool", "missing-key");
return;
}

Expand All @@ -1855,19 +1887,33 @@ class App extends React.Component<AppProps, AppState> {

if (selectedElements.length === 0) {
this.setActiveTool({ type: TOOL_TYPE.magicframe });
trackEvent("ai", "d2c-tool", "empty-selection");
} else {
if (selectedElements.some((el) => isFrameLikeElement(el))) {
const selectedMagicFrame: ExcalidrawMagicFrameElement | false =
selectedElements.length === 1 &&
isMagicFrameElement(selectedElements[0]) &&
selectedElements[0];

// case: user selected elements containing frame-like(s) or are frame
// members, we don't want to wrap into another magicframe
// (unless the only selected element is a magic frame which we reuse)
if (
!selectedMagicFrame &&
selectedElements.some((el) => isFrameLikeElement(el) || el.frameId)
) {
this.setActiveTool({ type: TOOL_TYPE.magicframe });
return;
}

let frame: ExcalidrawMagicFrameElement | null = null;
if (
selectedElements.length === 1 &&
isMagicFrameElement(selectedElements[0])
) {
frame = selectedElements[0];
trackEvent("ai", "d2c-tool", "existing-selection");

let frame: ExcalidrawMagicFrameElement;
if (selectedMagicFrame) {
// a single magicframe already selected -> use it
frame = selectedMagicFrame;
} else {
// selected elements aren't wrapped in magic frame yet -> wrap now

const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
const padding = 50;

Expand All @@ -1880,19 +1926,19 @@ class App extends React.Component<AppProps, AppState> {
opacity: 100,
locked: false,
});
}

this.scene.addNewElement(frame);
this.scene.addNewElement(frame);

for (const child of selectedElements) {
mutateElement(child, { frameId: frame.id });
}
for (const child of selectedElements) {
mutateElement(child, { frameId: frame.id });
}

this.setState({
selectedElementIds: { [frame.id]: true },
});
this.setState({
selectedElementIds: { [frame.id]: true },
});
}

this.onMagicFrameGenerate(frame);
this.onMagicFrameGenerate(frame, "upstream");
}
};

Expand Down Expand Up @@ -3551,7 +3597,7 @@ class App extends React.Component<AppProps, AppState> {

if (event.key === KEYS.QUESTION_MARK) {
this.setState({
openDialog: "help",
openDialog: { name: "help" },
});
return;
} else if (
Expand All @@ -3560,7 +3606,7 @@ class App extends React.Component<AppProps, AppState> {
event[KEYS.CTRL_OR_CMD]
) {
event.preventDefault();
this.setState({ openDialog: "imageExport" });
this.setState({ openDialog: { name: "imageExport" } });
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/JSONExportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const JSONExportDialog = ({

return (
<>
{appState.openDialog === "jsonExport" && (
{appState.openDialog?.name === "jsonExport" && (
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
<JSONExportModal
elements={elements}
Expand Down
21 changes: 15 additions & 6 deletions src/components/LayerUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ interface LayerUIProps {
openAIKey: string | null;
isOpenAIKeyPersisted: boolean;
onOpenAIAPIKeyChange: (apiKey: string, shouldPersist: boolean) => void;
onMagicSettingsConfirm: (apiKey: string, shouldPersist: boolean) => void;
onMagicSettingsConfirm: (
apiKey: string,
shouldPersist: boolean,
source: "tool" | "generation" | "settings",
) => void;
}

const DefaultMainMenu: React.FC<{
Expand Down Expand Up @@ -177,7 +181,7 @@ const LayerUI = ({
const renderImageExportDialog = () => {
if (
!UIOptions.canvasActions.saveAsImage ||
appState.openDialog !== "imageExport"
appState.openDialog?.name !== "imageExport"
) {
return null;
}
Expand Down Expand Up @@ -448,21 +452,26 @@ const LayerUI = ({
}}
/>
)}
{appState.openDialog === "help" && (
{appState.openDialog?.name === "help" && (
<HelpDialog
onClose={() => {
setAppState({ openDialog: null });
}}
/>
)}
{appState.openDialog === "magicSettings" && (
{appState.openDialog?.name === "magicSettings" && (
<MagicSettings
openAIKey={openAIKey}
isPersisted={isOpenAIKeyPersisted}
onChange={onOpenAIAPIKeyChange}
onConfirm={(apiKey, shouldPersist) => {
setAppState({ openDialog: null });
onMagicSettingsConfirm(apiKey, shouldPersist);
const source =
appState.openDialog?.name === "magicSettings"
? appState.openDialog?.source
: "settings";
setAppState({ openDialog: null }, () => {
onMagicSettingsConfirm(apiKey, shouldPersist, source);
});
}}
onClose={() => {
setAppState({ openDialog: null });
Expand Down
2 changes: 1 addition & 1 deletion src/components/MagicSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export const MagicSettings = (props: {
own limit in your OpenAI account dashboard if needed.
</p>
<TextField
isPassword
isRedacted
value={keyInputValue}
placeholder="Paste your API key here"
label="OpenAI API key"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const ExportToImage = () => {
actionLabel={t("overwriteConfirm.action.exportToImage.button")}
onClick={() => {
actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
setAppState({ openDialog: "imageExport" });
setAppState({ openDialog: { name: "imageExport" } });
}}
>
{t("overwriteConfirm.action.exportToImage.description")}
Expand Down
Loading

0 comments on commit 3d1631f

Please sign in to comment.