瀏覽代碼

feat: TTD dialog tweaks (#7346)

* tweaks to TTD dialog ~ prepping for settings dialog

* tweaks to ttd parsing & error logging
David Luzar 1 年之前
父節點
當前提交
dd220bcaea

+ 2 - 1
src/components/Actions.tsx

@@ -363,8 +363,9 @@ export const ShapesSwitcher = ({
                 onSelect={() => {
                   trackEvent("ai", "open-settings", "d2c");
                   app.setOpenDialog({
-                    name: "magicSettings",
+                    name: "settings",
                     source: "settings",
+                    tab: "diagram-to-code",
                   });
                 }}
                 icon={OpenAIIcon}

+ 10 - 2
src/components/App.tsx

@@ -1700,7 +1700,11 @@ class App extends React.Component<AppProps, AppState> {
   ) {
     if (!this.OPENAI_KEY) {
       this.setState({
-        openDialog: { name: "magicSettings", source: "generation" },
+        openDialog: {
+          name: "settings",
+          tab: "diagram-to-code",
+          source: "generation",
+        },
       });
       trackEvent("ai", "generate (missing key)", "d2c");
       return;
@@ -1871,7 +1875,11 @@ class App extends React.Component<AppProps, AppState> {
   public onMagicframeToolSelect = () => {
     if (!this.OPENAI_KEY) {
       this.setState({
-        openDialog: { name: "magicSettings", source: "tool" },
+        openDialog: {
+          name: "settings",
+          tab: "diagram-to-code",
+          source: "tool",
+        },
       });
       trackEvent("ai", "tool-select (missing key)", "d2c");
       return;

+ 2 - 2
src/components/LayerUI.tsx

@@ -461,14 +461,14 @@ const LayerUI = ({
           }}
         />
       )}
-      {appState.openDialog?.name === "magicSettings" && (
+      {appState.openDialog?.name === "settings" && (
         <MagicSettings
           openAIKey={openAIKey}
           isPersisted={isOpenAIKeyPersisted}
           onChange={onOpenAIAPIKeyChange}
           onConfirm={(apiKey, shouldPersist) => {
             const source =
-              appState.openDialog?.name === "magicSettings"
+              appState.openDialog?.name === "settings"
                 ? appState.openDialog?.source
                 : "settings";
             setAppState({ openDialog: null }, () => {

+ 9 - 0
src/components/MagicSettings.scss

@@ -1,9 +1,18 @@
 .excalidraw {
+  .MagicSettings {
+    .Island {
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+    }
+  }
+
   .MagicSettings-confirm {
     padding: 0.5rem 1rem;
   }
 
   .MagicSettings__confirm {
     margin-top: 2rem;
+    margin-right: auto;
   }
 }

+ 95 - 65
src/components/MagicSettings.tsx

@@ -10,6 +10,8 @@ import { InlineIcon } from "./InlineIcon";
 import { Paragraph } from "./Paragraph";
 
 import "./MagicSettings.scss";
+import TTDDialogTabs from "./TTDDialog/TTDDialogTabs";
+import { TTDDialogTab } from "./TTDDialog/TTDDialogTab";
 
 export const MagicSettings = (props: {
   openAIKey: string | null;
@@ -18,16 +20,21 @@ export const MagicSettings = (props: {
   onConfirm: (key: string, shouldPersist: boolean) => void;
   onClose: () => void;
 }) => {
-  const { theme } = useUIAppState();
   const [keyInputValue, setKeyInputValue] = useState(props.openAIKey || "");
   const [shouldPersist, setShouldPersist] = useState<boolean>(
     props.isPersisted,
   );
 
+  const appState = useUIAppState();
+
   const onConfirm = () => {
     props.onConfirm(keyInputValue.trim(), shouldPersist);
   };
 
+  if (appState.openDialog?.name !== "settings") {
+    return null;
+  }
+
   return (
     <Dialog
       onCloseRequest={() => {
@@ -36,7 +43,7 @@ export const MagicSettings = (props: {
       }}
       title={
         <div style={{ display: "flex" }}>
-          Diagram to Code (AI){" "}
+          Wireframe to Code (AI){" "}
           <div
             style={{
               display: "flex",
@@ -46,7 +53,8 @@ export const MagicSettings = (props: {
               marginLeft: "1rem",
               fontSize: 14,
               borderRadius: "12px",
-              background: theme === "light" ? "#FFCCCC" : "#703333",
+              color: "#000",
+              background: "pink",
             }}
           >
             Experimental
@@ -56,75 +64,97 @@ export const MagicSettings = (props: {
       className="MagicSettings"
       autofocus={false}
     >
-      <Paragraph
+      {/*  <h2
         style={{
-          display: "inline-flex",
-          alignItems: "center",
-          marginBottom: 0,
+          margin: 0,
+          fontSize: "1.25rem",
+          paddingLeft: "2.5rem",
         }}
       >
-        For the diagram-to-code feature we use <InlineIcon icon={OpenAIIcon} />
-        OpenAI.
-      </Paragraph>
-      <Paragraph>
-        While the OpenAI API is in beta, its use is strictly limited — as such
-        we require you use your own API key. You can create an{" "}
-        <a
-          href="https://platform.openai.com/login?launch"
-          rel="noopener noreferrer"
-          target="_blank"
-        >
-          OpenAI account
-        </a>
-        , add a small credit (5 USD minimum), and{" "}
-        <a
-          href="https://platform.openai.com/api-keys"
-          rel="noopener noreferrer"
-          target="_blank"
+        AI Settings
+      </h2> */}
+      <TTDDialogTabs dialog="settings" tab={appState.openDialog.tab}>
+        {/* <TTDDialogTabTriggers>
+          <TTDDialogTabTrigger tab="text-to-diagram">
+            <InlineIcon icon={brainIcon} /> Text to diagram
+          </TTDDialogTabTrigger>
+          <TTDDialogTabTrigger tab="diagram-to-code">
+            <InlineIcon icon={MagicIcon} /> Wireframe to code
+          </TTDDialogTabTrigger>
+        </TTDDialogTabTriggers> */}
+        {/* <TTDDialogTab className="ttd-dialog-content" tab="text-to-diagram">
+          TODO
+        </TTDDialogTab> */}
+        <TTDDialogTab
+          //  className="ttd-dialog-content"
+          tab="diagram-to-code"
         >
-          generate your own API key
-        </a>
-        .
-      </Paragraph>
-      <Paragraph>
-        Your OpenAI key does not leave the browser, and you can also set your
-        own limit in your OpenAI account dashboard if needed.
-      </Paragraph>
-      <TextField
-        isRedacted
-        value={keyInputValue}
-        placeholder="Paste your API key here"
-        label="OpenAI API key"
-        onChange={(value) => {
-          setKeyInputValue(value);
-          props.onChange(value.trim(), shouldPersist);
-        }}
-        selectOnRender
-        onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()}
-      />
-      <Paragraph>
-        By default, your API token is not persisted anywhere so you'll need to
-        insert it again after reload. But, you can persist locally in your
-        browser below.
-      </Paragraph>
+          <Paragraph>
+            For the diagram-to-code feature we use{" "}
+            <InlineIcon icon={OpenAIIcon} />
+            OpenAI.
+          </Paragraph>
+          <Paragraph>
+            While the OpenAI API is in beta, its use is strictly limited — as
+            such we require you use your own API key. You can create an{" "}
+            <a
+              href="https://platform.openai.com/login?launch"
+              rel="noopener noreferrer"
+              target="_blank"
+            >
+              OpenAI account
+            </a>
+            , add a small credit (5 USD minimum), and{" "}
+            <a
+              href="https://platform.openai.com/api-keys"
+              rel="noopener noreferrer"
+              target="_blank"
+            >
+              generate your own API key
+            </a>
+            .
+          </Paragraph>
+          <Paragraph>
+            Your OpenAI key does not leave the browser, and you can also set
+            your own limit in your OpenAI account dashboard if needed.
+          </Paragraph>
+          <TextField
+            isRedacted
+            value={keyInputValue}
+            placeholder="Paste your API key here"
+            label="OpenAI API key"
+            onChange={(value) => {
+              setKeyInputValue(value);
+              props.onChange(value.trim(), shouldPersist);
+            }}
+            selectOnRender
+            onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()}
+          />
+          <Paragraph>
+            By default, your API token is not persisted anywhere so you'll need
+            to insert it again after reload. But, you can persist locally in
+            your browser below.
+          </Paragraph>
 
-      <CheckboxItem checked={shouldPersist} onChange={setShouldPersist}>
-        Persist API key in browser storage
-      </CheckboxItem>
+          <CheckboxItem checked={shouldPersist} onChange={setShouldPersist}>
+            Persist API key in browser storage
+          </CheckboxItem>
 
-      <Paragraph>
-        Once API key is set, you can use the <InlineIcon icon={MagicIcon} />{" "}
-        tool to wrap your elements in a frame that will then allow you to turn
-        it into code. This dialog can be accessed using the <b>AI Settings</b>{" "}
-        <InlineIcon icon={OpenAIIcon} />.
-      </Paragraph>
+          <Paragraph>
+            Once API key is set, you can use the <InlineIcon icon={MagicIcon} />{" "}
+            tool to wrap your elements in a frame that will then allow you to
+            turn it into code. This dialog can be accessed using the{" "}
+            <b>AI Settings</b> <InlineIcon icon={OpenAIIcon} />.
+          </Paragraph>
 
-      <FilledButton
-        className="MagicSettings__confirm"
-        size="large"
-        label="Confirm"
-        onClick={onConfirm}
-      />
+          <FilledButton
+            className="MagicSettings__confirm"
+            size="large"
+            label="Confirm"
+            onClick={onConfirm}
+          />
+        </TTDDialogTab>
+      </TTDDialogTabs>
     </Dialog>
   );
 };

+ 4 - 1
src/components/Modal.scss

@@ -18,8 +18,11 @@
     overflow: auto;
     padding: calc(var(--space-factor) * 10);
 
+    display: flex;
+    flex-direction: column;
+
     .Island {
-      padding: 2.5rem !important;
+      padding: 2.5rem;
     }
   }
 

+ 1 - 1
src/components/TTDDialog/MermaidToExcalidraw.tsx

@@ -63,7 +63,7 @@ const MermaidToExcalidraw = ({
       data,
       mermaidToExcalidrawLib,
       setError,
-      text: deferredText,
+      mermaidDefinition: deferredText,
     }).catch(() => {});
   }, [deferredText, mermaidToExcalidrawLib]);
 

+ 30 - 5
src/components/TTDDialog/TTDDialog.tsx

@@ -72,7 +72,7 @@ export const TTDDialogBase = withInternalFallback(
     tab,
     ...rest
   }: {
-    tab: string;
+    tab: "text-to-diagram" | "mermaid";
   } & (
     | {
         onTextSubmit(value: string): Promise<OnTestSubmitRetValue>;
@@ -150,11 +150,19 @@ export const TTDDialogBase = withInternalFallback(
             data,
             mermaidToExcalidrawLib,
             setError,
-            text: generatedResponse,
+            mermaidDefinition: generatedResponse,
           });
           trackEvent("ai", "mermaid parse success", "ttd");
           saveMermaidDataToStorage(generatedResponse);
         } catch (error: any) {
+          console.info(
+            `%cTTD mermaid render errror: ${error.message}`,
+            "color: red",
+          );
+          console.info(
+            `>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\nTTD mermaid definition render errror: ${error.message}`,
+            "color: yellow",
+          );
           trackEvent("ai", "mermaid parse failed", "ttd");
           setError(
             new Error(
@@ -206,17 +214,34 @@ export const TTDDialogBase = withInternalFallback(
           app.setOpenDialog(null);
         }}
         size={1200}
-        title=""
+        title={false}
         {...rest}
         autofocus={false}
       >
-        <TTDDialogTabs tab={tab}>
+        <TTDDialogTabs dialog="ttd" tab={tab}>
           {"__fallback" in rest && rest.__fallback ? (
             <p className="dialog-mermaid-title">{t("mermaid.title")}</p>
           ) : (
             <TTDDialogTabTriggers>
               <TTDDialogTabTrigger tab="text-to-diagram">
-                {t("labels.textToDiagram")}
+                <div style={{ display: "flex", alignItems: "center" }}>
+                  {t("labels.textToDiagram")}
+                  <div
+                    style={{
+                      display: "flex",
+                      alignItems: "center",
+                      justifyContent: "center",
+                      padding: "1px 6px",
+                      marginLeft: "10px",
+                      fontSize: 10,
+                      borderRadius: "12px",
+                      background: "pink",
+                      color: "#000",
+                    }}
+                  >
+                    AI Beta
+                  </div>
+                </div>
               </TTDDialogTabTrigger>
               <TTDDialogTabTrigger tab="mermaid">Mermaid</TTDDialogTabTrigger>
             </TTDDialogTabTriggers>

+ 40 - 14
src/components/TTDDialog/TTDDialogTabs.tsx

@@ -1,34 +1,60 @@
 import * as RadixTabs from "@radix-ui/react-tabs";
-import { ReactNode } from "react";
+import { ReactNode, useRef } from "react";
 import { useExcalidrawSetAppState } from "../App";
+import { isMemberOf } from "../../utils";
 
-const TTDDialogTabs = ({
-  children,
-  tab,
-  ...rest
-}: {
-  children: ReactNode;
-  tab: string;
-}) => {
+const TTDDialogTabs = (
+  props: {
+    children: ReactNode;
+  } & (
+    | { dialog: "ttd"; tab: "text-to-diagram" | "mermaid" }
+    | { dialog: "settings"; tab: "text-to-diagram" | "diagram-to-code" }
+  ),
+) => {
   const setAppState = useExcalidrawSetAppState();
 
+  const rootRef = useRef<HTMLDivElement>(null);
+  const minHeightRef = useRef<number>(0);
+
   return (
     <RadixTabs.Root
+      ref={rootRef}
       className="ttd-dialog-tabs-root"
-      value={tab}
+      value={props.tab}
       onValueChange={(
         // at least in test enviros, `tab` can be `undefined`
         tab: string | undefined,
       ) => {
-        if (tab) {
+        if (!tab) {
+          return;
+        }
+        const modalContentNode =
+          rootRef.current?.closest<HTMLElement>(".Modal__content");
+        if (modalContentNode) {
+          const currHeight = modalContentNode.offsetHeight || 0;
+          if (currHeight > minHeightRef.current) {
+            minHeightRef.current = currHeight;
+            modalContentNode.style.minHeight = `min(${minHeightRef.current}px, 100%)`;
+          }
+        }
+        if (
+          props.dialog === "settings" &&
+          isMemberOf(["text-to-diagram", "diagram-to-code"], tab)
+        ) {
+          setAppState({
+            openDialog: { name: props.dialog, tab, source: "settings" },
+          });
+        } else if (
+          props.dialog === "ttd" &&
+          isMemberOf(["text-to-diagram", "mermaid"], tab)
+        ) {
           setAppState({
-            openDialog: { name: "ttd", tab },
+            openDialog: { name: props.dialog, tab },
           });
         }
       }}
-      {...rest}
     >
-      {children}
+      {props.children}
     </RadixTabs.Root>
   );
 };

+ 18 - 7
src/components/TTDDialog/common.ts

@@ -43,7 +43,7 @@ export interface MermaidToExcalidrawLibProps {
 interface ConvertMermaidToExcalidrawFormatProps {
   canvasRef: React.RefObject<HTMLDivElement>;
   mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
-  text: string;
+  mermaidDefinition: string;
   setError: (error: Error | null) => void;
   data: React.MutableRefObject<{
     elements: readonly NonDeletedExcalidrawElement[];
@@ -54,7 +54,7 @@ interface ConvertMermaidToExcalidrawFormatProps {
 export const convertMermaidToExcalidraw = async ({
   canvasRef,
   mermaidToExcalidrawLib,
-  text,
+  mermaidDefinition,
   setError,
   data,
 }: ConvertMermaidToExcalidrawFormatProps) => {
@@ -65,7 +65,7 @@ export const convertMermaidToExcalidraw = async ({
     return;
   }
 
-  if (!text) {
+  if (!mermaidDefinition) {
     resetPreview({ canvasRef, setError });
     return;
   }
@@ -73,9 +73,20 @@ export const convertMermaidToExcalidraw = async ({
   try {
     const api = await mermaidToExcalidrawLib.api;
 
-    const { elements, files } = await api.parseMermaidToExcalidraw(text, {
-      fontSize: DEFAULT_FONT_SIZE,
-    });
+    let ret;
+    try {
+      ret = await api.parseMermaidToExcalidraw(mermaidDefinition, {
+        fontSize: DEFAULT_FONT_SIZE,
+      });
+    } catch (err: any) {
+      ret = await api.parseMermaidToExcalidraw(
+        mermaidDefinition.replace(/"/g, "'"),
+        {
+          fontSize: DEFAULT_FONT_SIZE,
+        },
+      );
+    }
+    const { elements, files } = ret;
     setError(null);
 
     data.current = {
@@ -101,7 +112,7 @@ export const convertMermaidToExcalidraw = async ({
   } catch (err: any) {
     console.error(err);
     parent.style.background = "var(--default-bg-color)";
-    if (text) {
+    if (mermaidDefinition) {
       setError(err);
     }
 

+ 13 - 4
src/css/styles.scss

@@ -652,6 +652,19 @@
       --button-bg: var(--color-surface-high);
     }
   }
+
+  .excalidraw__paragraph {
+    margin: 1rem 0;
+  }
+
+  .Modal__content {
+    .excalidraw__paragraph:first-child {
+      margin-top: 0;
+    }
+    .excalidraw__paragraph + .excalidraw__paragraph {
+      margin-top: 0rem;
+    }
+  }
 }
 
 .ErrorSplash.excalidraw {
@@ -735,8 +748,4 @@
     letter-spacing: 0.6px;
     font-family: "Assistant";
   }
-
-  .excalidraw__paragraph {
-    margin: 1rem 0;
-  }
 }

+ 3 - 2
src/types.ts

@@ -248,13 +248,14 @@ export interface AppState {
     | null
     | { name: "imageExport" | "help" | "jsonExport" }
     | {
-        name: "magicSettings";
+        name: "settings";
         source:
           | "tool" // when magicframe tool is selected
           | "generation" // when magicframe generate button is clicked
           | "settings"; // when AI settings dialog is explicitly invoked
+        tab: "text-to-diagram" | "diagram-to-code";
       }
-    | { name: "ttd"; tab: string };
+    | { name: "ttd"; tab: "text-to-diagram" | "mermaid" };
   /**
    * Reflects user preference for whether the default sidebar should be docked.
    *