diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index f0c4d8db..0f21794d 100644 --- a/src/components/Actions.tsx +++ b/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} diff --git a/src/components/App.tsx b/src/components/App.tsx index 87fc57e9..2c3f7a6e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1700,7 +1700,11 @@ class App extends React.Component { ) { 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 { 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; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 4e5cc93e..77be711a 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -461,14 +461,14 @@ const LayerUI = ({ }} /> )} - {appState.openDialog?.name === "magicSettings" && ( + {appState.openDialog?.name === "settings" && ( { const source = - appState.openDialog?.name === "magicSettings" + appState.openDialog?.name === "settings" ? appState.openDialog?.source : "settings"; setAppState({ openDialog: null }, () => { diff --git a/src/components/MagicSettings.scss b/src/components/MagicSettings.scss index 1b2b8dda..bd07d840 100644 --- a/src/components/MagicSettings.scss +++ b/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; } } diff --git a/src/components/MagicSettings.tsx b/src/components/MagicSettings.tsx index 190d7b24..af476037 100644 --- a/src/components/MagicSettings.tsx +++ b/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( props.isPersisted, ); + const appState = useUIAppState(); + const onConfirm = () => { props.onConfirm(keyInputValue.trim(), shouldPersist); }; + if (appState.openDialog?.name !== "settings") { + return null; + } + return ( { @@ -36,7 +43,7 @@ export const MagicSettings = (props: { }} title={
- Diagram to Code (AI){" "} + Wireframe to Code (AI){" "}
Experimental @@ -56,75 +64,97 @@ export const MagicSettings = (props: { className="MagicSettings" autofocus={false} > - - For the diagram-to-code feature we use - OpenAI. - - - 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{" "} - */} + + {/* + + Text to diagram + + + Wireframe to code + + */} + {/* + TODO + */} + - OpenAI account - - , add a small credit (5 USD minimum), and{" "} - - generate your own API key - - . - - - Your OpenAI key does not leave the browser, and you can also set your - own limit in your OpenAI account dashboard if needed. - - { - setKeyInputValue(value); - props.onChange(value.trim(), shouldPersist); - }} - selectOnRender - onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()} - /> - - 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. - + + For the diagram-to-code feature we use{" "} + + OpenAI. + + + 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{" "} + + OpenAI account + + , add a small credit (5 USD minimum), and{" "} + + generate your own API key + + . + + + Your OpenAI key does not leave the browser, and you can also set + your own limit in your OpenAI account dashboard if needed. + + { + setKeyInputValue(value); + props.onChange(value.trim(), shouldPersist); + }} + selectOnRender + onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()} + /> + + 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. + - - Persist API key in browser storage - + + Persist API key in browser storage + - - Once API key is set, you can use the {" "} - 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 AI Settings{" "} - . - + + Once API key is set, you can use the {" "} + 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{" "} + AI Settings . + - + + +
); }; diff --git a/src/components/Modal.scss b/src/components/Modal.scss index f71af71a..7dc62c11 100644 --- a/src/components/Modal.scss +++ b/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; } } diff --git a/src/components/TTDDialog/MermaidToExcalidraw.tsx b/src/components/TTDDialog/MermaidToExcalidraw.tsx index a5074fdb..342bf38b 100644 --- a/src/components/TTDDialog/MermaidToExcalidraw.tsx +++ b/src/components/TTDDialog/MermaidToExcalidraw.tsx @@ -63,7 +63,7 @@ const MermaidToExcalidraw = ({ data, mermaidToExcalidrawLib, setError, - text: deferredText, + mermaidDefinition: deferredText, }).catch(() => {}); }, [deferredText, mermaidToExcalidrawLib]); diff --git a/src/components/TTDDialog/TTDDialog.tsx b/src/components/TTDDialog/TTDDialog.tsx index 3a32ee55..8572a930 100644 --- a/src/components/TTDDialog/TTDDialog.tsx +++ b/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; @@ -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} > - + {"__fallback" in rest && rest.__fallback ? (

{t("mermaid.title")}

) : ( - {t("labels.textToDiagram")} +
+ {t("labels.textToDiagram")} +
+ AI Beta +
+
Mermaid
diff --git a/src/components/TTDDialog/TTDDialogTabs.tsx b/src/components/TTDDialog/TTDDialogTabs.tsx index 87a9c168..324f4e53 100644 --- a/src/components/TTDDialog/TTDDialogTabs.tsx +++ b/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(null); + const minHeightRef = useRef(0); + return ( { - if (tab) { + if (!tab) { + return; + } + const modalContentNode = + rootRef.current?.closest(".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: "ttd", tab }, + openDialog: { name: props.dialog, tab, source: "settings" }, + }); + } else if ( + props.dialog === "ttd" && + isMemberOf(["text-to-diagram", "mermaid"], tab) + ) { + setAppState({ + openDialog: { name: props.dialog, tab }, }); } }} - {...rest} > - {children} + {props.children} ); }; diff --git a/src/components/TTDDialog/common.ts b/src/components/TTDDialog/common.ts index 4e11931e..9d90a043 100644 --- a/src/components/TTDDialog/common.ts +++ b/src/components/TTDDialog/common.ts @@ -43,7 +43,7 @@ export interface MermaidToExcalidrawLibProps { interface ConvertMermaidToExcalidrawFormatProps { canvasRef: React.RefObject; 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); } diff --git a/src/css/styles.scss b/src/css/styles.scss index 43f30b40..d7202c6a 100644 --- a/src/css/styles.scss +++ b/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; - } } diff --git a/src/types.ts b/src/types.ts index 94a48460..ee1287b1 100644 --- a/src/types.ts +++ b/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. *