From 3d1631f37570a59aef570c2c5f927b5f1c5c8bf1 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:02:11 +0100 Subject: [PATCH] feat: d2c tweaks (#7336) --- src/actions/actionMenu.tsx | 9 +- src/analytics.ts | 22 ++-- src/components/Actions.tsx | 12 +- src/components/App.tsx | 116 ++++++++++++------ src/components/JSONExportDialog.tsx | 2 +- src/components/LayerUI.tsx | 21 +++- src/components/MagicSettings.tsx | 2 +- .../OverwriteConfirmActions.tsx | 2 +- src/components/TextField.tsx | 21 ++-- src/components/main-menu/DefaultItems.tsx | 4 +- src/css/styles.scss | 6 + src/packages/excalidraw/CHANGELOG.md | 6 + src/tests/MermaidToExcalidraw.test.tsx | 2 +- src/types.ts | 17 +-- 14 files changed, 168 insertions(+), 74 deletions(-) diff --git a/src/actions/actionMenu.tsx b/src/actions/actionMenu.tsx index b259d726..fa8dcbea 100644 --- a/src/actions/actionMenu.tsx +++ b/src/actions/actionMenu.tsx @@ -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, }; diff --git a/src/analytics.ts b/src/analytics.ts index c7b4f844..671f5920 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -1,3 +1,7 @@ +// 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, @@ -5,13 +9,13 @@ export const trackEvent = ( 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; } @@ -19,6 +23,10 @@ export const trackEvent = ( return; } + if (!import.meta.env.PROD) { + console.info("trackEvent", { category, action, label, value }); + } + if (window.sa_event) { window.sa_event(action, { category, diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 556dc4af..7c9a2ca7 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -339,7 +339,7 @@ export const ShapesSwitcher = ({ Generate app.setOpenDialog("mermaid")} + onSelect={() => app.setOpenDialog({ name: "mermaid" })} icon={mermaidLogoIcon} data-testid="toolbar-embeddable" > @@ -349,14 +349,20 @@ export const ShapesSwitcher = ({ {app.props.aiEnabled !== false && ( <> app.onMagicButtonSelect()} + onSelect={() => app.onMagicframeToolSelect()} icon={MagicIcon} data-testid="toolbar-magicframe" > {t("toolBar.magicframe")} app.setOpenDialog("magicSettings")} + onSelect={() => { + trackEvent("ai", "d2c-settings", "settings"); + app.setOpenDialog({ + name: "magicSettings", + source: "settings", + }); + }} icon={OpenAIIcon} data-testid="toolbar-magicSettings" > diff --git a/src/components/App.tsx b/src/components/App.tsx index c579d8b1..486f32c2 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1435,7 +1435,7 @@ class App extends React.Component { onMagicSettingsConfirm={this.onMagicSettingsConfirm} > {this.props.children} - {this.state.openDialog === "mermaid" && ( + {this.state.openDialog?.name === "mermaid" && ( )} @@ -1467,6 +1467,7 @@ class App extends React.Component { onChange={() => this.onMagicFrameGenerate( firstSelectedElement, + "button", ) } /> @@ -1697,11 +1698,15 @@ class App extends React.Component { 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; } @@ -1712,7 +1717,12 @@ class App extends React.Component { }).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; } @@ -1751,6 +1761,8 @@ class App extends React.Component { const textFromFrameChildren = this.getTextFromElements(magicFrameChildren); + trackEvent("ai", "d2c-generate", "generating"); + const result = await diagramToHTML({ image: dataURL, apiKey: this.OPENAI_KEY, @@ -1759,6 +1771,7 @@ class App extends React.Component { }); if (!result.ok) { + trackEvent("ai", "d2c-generate", "generating-failed"); console.error(result.error); this.updateMagicGeneration({ frameElement, @@ -1770,6 +1783,7 @@ class App extends React.Component { }); return; } + trackEvent("ai", "d2c-generate", "generating-done"); if (result.choices[0].message.content == null) { this.updateMagicGeneration({ @@ -1813,7 +1827,10 @@ class App extends React.Component { 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( @@ -1826,26 +1843,41 @@ class App extends React.Component { } }; - 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; } @@ -1855,19 +1887,33 @@ class App extends React.Component { 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; @@ -1880,19 +1926,19 @@ class App extends React.Component { opacity: 100, locked: false, }); + + this.scene.addNewElement(frame); + + for (const child of selectedElements) { + mutateElement(child, { frameId: frame.id }); + } + + this.setState({ + selectedElementIds: { [frame.id]: true }, + }); } - this.scene.addNewElement(frame); - - for (const child of selectedElements) { - mutateElement(child, { frameId: frame.id }); - } - - this.setState({ - selectedElementIds: { [frame.id]: true }, - }); - - this.onMagicFrameGenerate(frame); + this.onMagicFrameGenerate(frame, "upstream"); } }; @@ -3551,7 +3597,7 @@ class App extends React.Component { if (event.key === KEYS.QUESTION_MARK) { this.setState({ - openDialog: "help", + openDialog: { name: "help" }, }); return; } else if ( @@ -3560,7 +3606,7 @@ class App extends React.Component { event[KEYS.CTRL_OR_CMD] ) { event.preventDefault(); - this.setState({ openDialog: "imageExport" }); + this.setState({ openDialog: { name: "imageExport" } }); return; } diff --git a/src/components/JSONExportDialog.tsx b/src/components/JSONExportDialog.tsx index 59cffbba..b5cea4af 100644 --- a/src/components/JSONExportDialog.tsx +++ b/src/components/JSONExportDialog.tsx @@ -117,7 +117,7 @@ export const JSONExportDialog = ({ return ( <> - {appState.openDialog === "jsonExport" && ( + {appState.openDialog?.name === "jsonExport" && ( void; - onMagicSettingsConfirm: (apiKey: string, shouldPersist: boolean) => void; + onMagicSettingsConfirm: ( + apiKey: string, + shouldPersist: boolean, + source: "tool" | "generation" | "settings", + ) => void; } const DefaultMainMenu: React.FC<{ @@ -177,7 +181,7 @@ const LayerUI = ({ const renderImageExportDialog = () => { if ( !UIOptions.canvasActions.saveAsImage || - appState.openDialog !== "imageExport" + appState.openDialog?.name !== "imageExport" ) { return null; } @@ -448,21 +452,26 @@ const LayerUI = ({ }} /> )} - {appState.openDialog === "help" && ( + {appState.openDialog?.name === "help" && ( { setAppState({ openDialog: null }); }} /> )} - {appState.openDialog === "magicSettings" && ( + {appState.openDialog?.name === "magicSettings" && ( { - 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 }); diff --git a/src/components/MagicSettings.tsx b/src/components/MagicSettings.tsx index b0fa9492..2f2dc9ba 100644 --- a/src/components/MagicSettings.tsx +++ b/src/components/MagicSettings.tsx @@ -106,7 +106,7 @@ export const MagicSettings = (props: { own limit in your OpenAI account dashboard if needed.

{ actionLabel={t("overwriteConfirm.action.exportToImage.button")} onClick={() => { actionManager.executeAction(actionChangeExportEmbedScene, "ui", true); - setAppState({ openDialog: "imageExport" }); + setAppState({ openDialog: { name: "imageExport" } }); }} > {t("overwriteConfirm.action.exportToImage.description")} diff --git a/src/components/TextField.tsx b/src/components/TextField.tsx index bae1b40a..10b3d9b5 100644 --- a/src/components/TextField.tsx +++ b/src/components/TextField.tsx @@ -25,7 +25,7 @@ type TextFieldProps = { label?: string; placeholder?: string; - isPassword?: boolean; + isRedacted?: boolean; }; export const TextField = forwardRef( @@ -39,7 +39,7 @@ export const TextField = forwardRef( readonly, selectOnRender, onKeyDown, - isPassword = false, + isRedacted = false, }, ref, ) => { @@ -53,7 +53,8 @@ export const TextField = forwardRef( } }, [selectOnRender]); - const [isVisible, setIsVisible] = useState(true); + const [isTemporarilyUnredacted, setIsTemporarilyUnredacted] = + useState(false); return (
( })} > ( onChange={(event) => onChange?.(event.target.value)} onKeyDown={onKeyDown} /> - {isPassword && ( + {isRedacted && ( )}
diff --git a/src/components/main-menu/DefaultItems.tsx b/src/components/main-menu/DefaultItems.tsx index 2908d6b4..9191bbe8 100644 --- a/src/components/main-menu/DefaultItems.tsx +++ b/src/components/main-menu/DefaultItems.tsx @@ -107,7 +107,7 @@ export const SaveAsImage = () => { setAppState({ openDialog: "imageExport" })} + onSelect={() => setAppState({ openDialog: { name: "imageExport" } })} shortcut={getShortcutFromShortcutName("imageExport")} aria-label={t("buttons.exportImage")} > @@ -230,7 +230,7 @@ export const Export = () => { { - setAppState({ openDialog: "jsonExport" }); + setAppState({ openDialog: { name: "jsonExport" } }); }} data-testid="json-export-button" aria-label={t("buttons.export")} diff --git a/src/css/styles.scss b/src/css/styles.scss index 2f690a82..032c9abb 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -533,6 +533,12 @@ } } + input.is-redacted { + // we don't use type=password because browsers (chrome?) prompt + // you to save it which is annoying + -webkit-text-security: disc; + } + input[type="text"], textarea:not(.excalidraw-wysiwyg) { color: var(--text-primary-color); diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 8110aad8..368ff414 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -11,6 +11,12 @@ The change should be grouped under one of the below section and must contain PR Please add the latest change on the top under the correct section. --> +## Unreleased + +### Breaking Changes + +- `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336) + ## 0.17.0 (2023-11-14) ### Features diff --git a/src/tests/MermaidToExcalidraw.test.tsx b/src/tests/MermaidToExcalidraw.test.tsx index fc3cb8f3..758831a6 100644 --- a/src/tests/MermaidToExcalidraw.test.tsx +++ b/src/tests/MermaidToExcalidraw.test.tsx @@ -102,7 +102,7 @@ describe("Test ", () => { , diff --git a/src/types.ts b/src/types.ts index a0690f95..507123ba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -245,12 +245,15 @@ export interface AppState { openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null; openSidebar: { name: SidebarName; tab?: SidebarTabName } | null; openDialog: - | "imageExport" - | "help" - | "jsonExport" - | "mermaid" - | "magicSettings" - | null; + | null + | { name: "imageExport" | "help" | "jsonExport" | "mermaid" } + | { + name: "magicSettings"; + source: + | "tool" // when magicframe tool is selected + | "generation" // when magicframe generate button is clicked + | "settings"; // when AI settings dialog is explicitly invoked + }; /** * Reflects user preference for whether the default sidebar should be docked. * @@ -549,7 +552,7 @@ export type AppClassProperties = { setActiveTool: App["setActiveTool"]; setOpenDialog: App["setOpenDialog"]; insertEmbeddableElement: App["insertEmbeddableElement"]; - onMagicButtonSelect: App["onMagicButtonSelect"]; + onMagicframeToolSelect: App["onMagicframeToolSelect"]; }; export type PointerDownState = Readonly<{