Pārlūkot izejas kodu

feat: d2c tweaks (#7336)

David Luzar 1 gadu atpakaļ
vecāks
revīzija
3d1631f375

+ 7 - 2
src/actions/actionMenu.tsx

@@ -56,13 +56,18 @@ export const actionShortcuts = register({
   viewMode: true,
   viewMode: true,
   trackEvent: { category: "menu", action: "toggleHelpDialog" },
   trackEvent: { category: "menu", action: "toggleHelpDialog" },
   perform: (_elements, appState, _, { focusContainer }) => {
   perform: (_elements, appState, _, { focusContainer }) => {
-    if (appState.openDialog === "help") {
+    if (appState.openDialog?.name === "help") {
       focusContainer();
       focusContainer();
     }
     }
     return {
     return {
       appState: {
       appState: {
         ...appState,
         ...appState,
-        openDialog: appState.openDialog === "help" ? null : "help",
+        openDialog:
+          appState.openDialog?.name === "help"
+            ? null
+            : {
+                name: "help",
+              },
       },
       },
       commitToHistory: false,
       commitToHistory: false,
     };
     };

+ 15 - 7
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 = (
 export const trackEvent = (
   category: string,
   category: string,
   action: string,
   action: string,
@@ -5,13 +9,13 @@ export const trackEvent = (
   value?: number,
   value?: number,
 ) => {
 ) => {
   try {
   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;
       return;
     }
     }
 
 
@@ -19,6 +23,10 @@ export const trackEvent = (
       return;
       return;
     }
     }
 
 
+    if (!import.meta.env.PROD) {
+      console.info("trackEvent", { category, action, label, value });
+    }
+
     if (window.sa_event) {
     if (window.sa_event) {
       window.sa_event(action, {
       window.sa_event(action, {
         category,
         category,

+ 9 - 3
src/components/Actions.tsx

@@ -339,7 +339,7 @@ export const ShapesSwitcher = ({
             Generate
             Generate
           </div>
           </div>
           <DropdownMenu.Item
           <DropdownMenu.Item
-            onSelect={() => app.setOpenDialog("mermaid")}
+            onSelect={() => app.setOpenDialog({ name: "mermaid" })}
             icon={mermaidLogoIcon}
             icon={mermaidLogoIcon}
             data-testid="toolbar-embeddable"
             data-testid="toolbar-embeddable"
           >
           >
@@ -349,14 +349,20 @@ export const ShapesSwitcher = ({
           {app.props.aiEnabled !== false && (
           {app.props.aiEnabled !== false && (
             <>
             <>
               <DropdownMenu.Item
               <DropdownMenu.Item
-                onSelect={() => app.onMagicButtonSelect()}
+                onSelect={() => app.onMagicframeToolSelect()}
                 icon={MagicIcon}
                 icon={MagicIcon}
                 data-testid="toolbar-magicframe"
                 data-testid="toolbar-magicframe"
               >
               >
                 {t("toolBar.magicframe")}
                 {t("toolBar.magicframe")}
               </DropdownMenu.Item>
               </DropdownMenu.Item>
               <DropdownMenu.Item
               <DropdownMenu.Item
-                onSelect={() => app.setOpenDialog("magicSettings")}
+                onSelect={() => {
+                  trackEvent("ai", "d2c-settings", "settings");
+                  app.setOpenDialog({
+                    name: "magicSettings",
+                    source: "settings",
+                  });
+                }}
                 icon={OpenAIIcon}
                 icon={OpenAIIcon}
                 data-testid="toolbar-magicSettings"
                 data-testid="toolbar-magicSettings"
               >
               >

+ 79 - 33
src/components/App.tsx

@@ -1435,7 +1435,7 @@ class App extends React.Component<AppProps, AppState> {
                           onMagicSettingsConfirm={this.onMagicSettingsConfirm}
                           onMagicSettingsConfirm={this.onMagicSettingsConfirm}
                         >
                         >
                           {this.props.children}
                           {this.props.children}
-                          {this.state.openDialog === "mermaid" && (
+                          {this.state.openDialog?.name === "mermaid" && (
                             <MermaidToExcalidraw />
                             <MermaidToExcalidraw />
                           )}
                           )}
                         </LayerUI>
                         </LayerUI>
@@ -1467,6 +1467,7 @@ class App extends React.Component<AppProps, AppState> {
                                 onChange={() =>
                                 onChange={() =>
                                   this.onMagicFrameGenerate(
                                   this.onMagicFrameGenerate(
                                     firstSelectedElement,
                                     firstSelectedElement,
+                                    "button",
                                   )
                                   )
                                 }
                                 }
                               />
                               />
@@ -1697,11 +1698,15 @@ class App extends React.Component<AppProps, AppState> {
     return text;
     return text;
   }
   }
 
 
-  private async onMagicFrameGenerate(magicFrame: ExcalidrawMagicFrameElement) {
+  private async onMagicFrameGenerate(
+    magicFrame: ExcalidrawMagicFrameElement,
+    source: "button" | "upstream",
+  ) {
     if (!this.OPENAI_KEY) {
     if (!this.OPENAI_KEY) {
       this.setState({
       this.setState({
-        openDialog: "magicSettings",
+        openDialog: { name: "magicSettings", source: "generation" },
       });
       });
+      trackEvent("ai", "d2c-generate", "missing-key");
       return;
       return;
     }
     }
 
 
@@ -1712,7 +1717,12 @@ class App extends React.Component<AppProps, AppState> {
     }).filter((el) => !isMagicFrameElement(el));
     }).filter((el) => !isMagicFrameElement(el));
 
 
     if (!magicFrameChildren.length) {
     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;
       return;
     }
     }
 
 
@@ -1751,6 +1761,8 @@ class App extends React.Component<AppProps, AppState> {
 
 
     const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);
     const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);
 
 
+    trackEvent("ai", "d2c-generate", "generating");
+
     const result = await diagramToHTML({
     const result = await diagramToHTML({
       image: dataURL,
       image: dataURL,
       apiKey: this.OPENAI_KEY,
       apiKey: this.OPENAI_KEY,
@@ -1759,6 +1771,7 @@ class App extends React.Component<AppProps, AppState> {
     });
     });
 
 
     if (!result.ok) {
     if (!result.ok) {
+      trackEvent("ai", "d2c-generate", "generating-failed");
       console.error(result.error);
       console.error(result.error);
       this.updateMagicGeneration({
       this.updateMagicGeneration({
         frameElement,
         frameElement,
@@ -1770,6 +1783,7 @@ class App extends React.Component<AppProps, AppState> {
       });
       });
       return;
       return;
     }
     }
+    trackEvent("ai", "d2c-generate", "generating-done");
 
 
     if (result.choices[0].message.content == null) {
     if (result.choices[0].message.content == null) {
       this.updateMagicGeneration({
       this.updateMagicGeneration({
@@ -1813,7 +1827,10 @@ class App extends React.Component<AppProps, AppState> {
   private OPENAI_KEY_IS_PERSISTED: boolean =
   private OPENAI_KEY_IS_PERSISTED: boolean =
     EditorLocalStorage.has(EDITOR_LS_KEYS.OAI_API_KEY) || false;
     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;
     this.OPENAI_KEY = openAIKey || null;
     if (shouldPersist) {
     if (shouldPersist) {
       const didPersist = EditorLocalStorage.set(
       const didPersist = EditorLocalStorage.set(
@@ -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) {
     if (apiKey) {
-      const selectedElements = this.scene.getSelectedElements({
-        selectedElementIds: this.state.selectedElementIds,
-      });
       if (selectedElements.length) {
       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) {
     if (!this.OPENAI_KEY) {
       this.setState({
       this.setState({
-        openDialog: "magicSettings",
+        openDialog: { name: "magicSettings", source: "tool" },
       });
       });
+      trackEvent("ai", "d2c-tool", "missing-key");
       return;
       return;
     }
     }
 
 
@@ -1855,19 +1887,33 @@ class App extends React.Component<AppProps, AppState> {
 
 
     if (selectedElements.length === 0) {
     if (selectedElements.length === 0) {
       this.setActiveTool({ type: TOOL_TYPE.magicframe });
       this.setActiveTool({ type: TOOL_TYPE.magicframe });
+      trackEvent("ai", "d2c-tool", "empty-selection");
     } else {
     } 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 });
         this.setActiveTool({ type: TOOL_TYPE.magicframe });
         return;
         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 {
       } else {
+        // selected elements aren't wrapped in magic frame yet -> wrap now
+
         const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
         const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
         const padding = 50;
         const padding = 50;
 
 
@@ -1880,19 +1926,19 @@ class App extends React.Component<AppProps, AppState> {
           opacity: 100,
           opacity: 100,
           locked: false,
           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");
     }
     }
   };
   };
 
 
@@ -3551,7 +3597,7 @@ class App extends React.Component<AppProps, AppState> {
 
 
       if (event.key === KEYS.QUESTION_MARK) {
       if (event.key === KEYS.QUESTION_MARK) {
         this.setState({
         this.setState({
-          openDialog: "help",
+          openDialog: { name: "help" },
         });
         });
         return;
         return;
       } else if (
       } else if (
@@ -3560,7 +3606,7 @@ class App extends React.Component<AppProps, AppState> {
         event[KEYS.CTRL_OR_CMD]
         event[KEYS.CTRL_OR_CMD]
       ) {
       ) {
         event.preventDefault();
         event.preventDefault();
-        this.setState({ openDialog: "imageExport" });
+        this.setState({ openDialog: { name: "imageExport" } });
         return;
         return;
       }
       }
 
 

+ 1 - 1
src/components/JSONExportDialog.tsx

@@ -117,7 +117,7 @@ export const JSONExportDialog = ({
 
 
   return (
   return (
     <>
     <>
-      {appState.openDialog === "jsonExport" && (
+      {appState.openDialog?.name === "jsonExport" && (
         <Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
         <Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
           <JSONExportModal
           <JSONExportModal
             elements={elements}
             elements={elements}

+ 15 - 6
src/components/LayerUI.tsx

@@ -86,7 +86,11 @@ interface LayerUIProps {
   openAIKey: string | null;
   openAIKey: string | null;
   isOpenAIKeyPersisted: boolean;
   isOpenAIKeyPersisted: boolean;
   onOpenAIAPIKeyChange: (apiKey: string, shouldPersist: boolean) => void;
   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<{
 const DefaultMainMenu: React.FC<{
@@ -177,7 +181,7 @@ const LayerUI = ({
   const renderImageExportDialog = () => {
   const renderImageExportDialog = () => {
     if (
     if (
       !UIOptions.canvasActions.saveAsImage ||
       !UIOptions.canvasActions.saveAsImage ||
-      appState.openDialog !== "imageExport"
+      appState.openDialog?.name !== "imageExport"
     ) {
     ) {
       return null;
       return null;
     }
     }
@@ -448,21 +452,26 @@ const LayerUI = ({
           }}
           }}
         />
         />
       )}
       )}
-      {appState.openDialog === "help" && (
+      {appState.openDialog?.name === "help" && (
         <HelpDialog
         <HelpDialog
           onClose={() => {
           onClose={() => {
             setAppState({ openDialog: null });
             setAppState({ openDialog: null });
           }}
           }}
         />
         />
       )}
       )}
-      {appState.openDialog === "magicSettings" && (
+      {appState.openDialog?.name === "magicSettings" && (
         <MagicSettings
         <MagicSettings
           openAIKey={openAIKey}
           openAIKey={openAIKey}
           isPersisted={isOpenAIKeyPersisted}
           isPersisted={isOpenAIKeyPersisted}
           onChange={onOpenAIAPIKeyChange}
           onChange={onOpenAIAPIKeyChange}
           onConfirm={(apiKey, shouldPersist) => {
           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={() => {
           onClose={() => {
             setAppState({ openDialog: null });
             setAppState({ openDialog: null });

+ 1 - 1
src/components/MagicSettings.tsx

@@ -106,7 +106,7 @@ export const MagicSettings = (props: {
         own limit in your OpenAI account dashboard if needed.
         own limit in your OpenAI account dashboard if needed.
       </p>
       </p>
       <TextField
       <TextField
-        isPassword
+        isRedacted
         value={keyInputValue}
         value={keyInputValue}
         placeholder="Paste your API key here"
         placeholder="Paste your API key here"
         label="OpenAI API key"
         label="OpenAI API key"

+ 1 - 1
src/components/OverwriteConfirm/OverwriteConfirmActions.tsx

@@ -47,7 +47,7 @@ export const ExportToImage = () => {
       actionLabel={t("overwriteConfirm.action.exportToImage.button")}
       actionLabel={t("overwriteConfirm.action.exportToImage.button")}
       onClick={() => {
       onClick={() => {
         actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
         actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
-        setAppState({ openDialog: "imageExport" });
+        setAppState({ openDialog: { name: "imageExport" } });
       }}
       }}
     >
     >
       {t("overwriteConfirm.action.exportToImage.description")}
       {t("overwriteConfirm.action.exportToImage.description")}

+ 13 - 8
src/components/TextField.tsx

@@ -25,7 +25,7 @@ type TextFieldProps = {
 
 
   label?: string;
   label?: string;
   placeholder?: string;
   placeholder?: string;
-  isPassword?: boolean;
+  isRedacted?: boolean;
 };
 };
 
 
 export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
 export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
@@ -39,7 +39,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
       readonly,
       readonly,
       selectOnRender,
       selectOnRender,
       onKeyDown,
       onKeyDown,
-      isPassword = false,
+      isRedacted = false,
     },
     },
     ref,
     ref,
   ) => {
   ) => {
@@ -53,7 +53,8 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
       }
       }
     }, [selectOnRender]);
     }, [selectOnRender]);
 
 
-    const [isVisible, setIsVisible] = useState<boolean>(true);
+    const [isTemporarilyUnredacted, setIsTemporarilyUnredacted] =
+      useState<boolean>(false);
 
 
     return (
     return (
       <div
       <div
@@ -71,7 +72,9 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
           })}
           })}
         >
         >
           <input
           <input
-            type={isPassword && isVisible ? "password" : undefined}
+            className={clsx({
+              "is-redacted": value && isRedacted && !isTemporarilyUnredacted,
+            })}
             readOnly={readonly}
             readOnly={readonly}
             value={value}
             value={value}
             placeholder={placeholder}
             placeholder={placeholder}
@@ -79,12 +82,14 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
             onChange={(event) => onChange?.(event.target.value)}
             onChange={(event) => onChange?.(event.target.value)}
             onKeyDown={onKeyDown}
             onKeyDown={onKeyDown}
           />
           />
-          {isPassword && (
+          {isRedacted && (
             <Button
             <Button
-              onSelect={() => setIsVisible(!isVisible)}
-              style={{ border: 0 }}
+              onSelect={() =>
+                setIsTemporarilyUnredacted(!isTemporarilyUnredacted)
+              }
+              style={{ border: 0, userSelect: "none" }}
             >
             >
-              {isVisible ? eyeIcon : eyeClosedIcon}
+              {isTemporarilyUnredacted ? eyeClosedIcon : eyeIcon}
             </Button>
             </Button>
           )}
           )}
         </div>
         </div>

+ 2 - 2
src/components/main-menu/DefaultItems.tsx

@@ -107,7 +107,7 @@ export const SaveAsImage = () => {
     <DropdownMenuItem
     <DropdownMenuItem
       icon={ExportImageIcon}
       icon={ExportImageIcon}
       data-testid="image-export-button"
       data-testid="image-export-button"
-      onSelect={() => setAppState({ openDialog: "imageExport" })}
+      onSelect={() => setAppState({ openDialog: { name: "imageExport" } })}
       shortcut={getShortcutFromShortcutName("imageExport")}
       shortcut={getShortcutFromShortcutName("imageExport")}
       aria-label={t("buttons.exportImage")}
       aria-label={t("buttons.exportImage")}
     >
     >
@@ -230,7 +230,7 @@ export const Export = () => {
     <DropdownMenuItem
     <DropdownMenuItem
       icon={ExportIcon}
       icon={ExportIcon}
       onSelect={() => {
       onSelect={() => {
-        setAppState({ openDialog: "jsonExport" });
+        setAppState({ openDialog: { name: "jsonExport" } });
       }}
       }}
       data-testid="json-export-button"
       data-testid="json-export-button"
       aria-label={t("buttons.export")}
       aria-label={t("buttons.export")}

+ 6 - 0
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"],
   input[type="text"],
   textarea:not(.excalidraw-wysiwyg) {
   textarea:not(.excalidraw-wysiwyg) {
     color: var(--text-primary-color);
     color: var(--text-primary-color);

+ 6 - 0
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.
 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)
 ## 0.17.0 (2023-11-14)
 
 
 ### Features
 ### Features

+ 1 - 1
src/tests/MermaidToExcalidraw.test.tsx

@@ -102,7 +102,7 @@ describe("Test <MermaidToExcalidraw/>", () => {
       <Excalidraw
       <Excalidraw
         initialData={{
         initialData={{
           appState: {
           appState: {
-            openDialog: "mermaid",
+            openDialog: { name: "mermaid" },
           },
           },
         }}
         }}
       />,
       />,

+ 10 - 7
src/types.ts

@@ -245,12 +245,15 @@ export interface AppState {
   openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
   openPopup: "canvasBackground" | "elementBackground" | "elementStroke" | null;
   openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
   openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
   openDialog:
   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.
    * Reflects user preference for whether the default sidebar should be docked.
    *
    *
@@ -549,7 +552,7 @@ export type AppClassProperties = {
   setActiveTool: App["setActiveTool"];
   setActiveTool: App["setActiveTool"];
   setOpenDialog: App["setOpenDialog"];
   setOpenDialog: App["setOpenDialog"];
   insertEmbeddableElement: App["insertEmbeddableElement"];
   insertEmbeddableElement: App["insertEmbeddableElement"];
-  onMagicButtonSelect: App["onMagicButtonSelect"];
+  onMagicframeToolSelect: App["onMagicframeToolSelect"];
 };
 };
 
 
 export type PointerDownState = Readonly<{
 export type PointerDownState = Readonly<{