From 14845a343bdc59b1304d02d90457a372b0b909ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20Moln=C3=A1r?= <38168628+barnabasmolnar@users.noreply.github.com> Date: Sat, 25 Nov 2023 22:11:03 +0100 Subject: [PATCH] feat: text-to-diagram (#7325) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- .env.development | 2 + .env.production | 2 + excalidraw-app/index.tsx | 60 ++++ src/components/Actions.tsx | 10 +- src/components/App.tsx | 20 +- src/components/Button.tsx | 6 +- src/components/LayerUI.tsx | 2 + src/components/MermaidToExcalidraw.scss | 221 ------------ src/components/MermaidToExcalidraw.tsx | 243 ------------- .../TTDDialog/MermaidToExcalidraw.scss | 10 + .../TTDDialog/MermaidToExcalidraw.tsx | 133 +++++++ src/components/TTDDialog/TTDDialog.scss | 301 ++++++++++++++++ src/components/TTDDialog/TTDDialog.tsx | 325 ++++++++++++++++++ src/components/TTDDialog/TTDDialogInput.tsx | 52 +++ src/components/TTDDialog/TTDDialogOutput.tsx | 39 +++ src/components/TTDDialog/TTDDialogPanel.tsx | 58 ++++ src/components/TTDDialog/TTDDialogPanels.tsx | 5 + src/components/TTDDialog/TTDDialogTab.tsx | 17 + .../TTDDialog/TTDDialogTabTrigger.tsx | 21 ++ .../TTDDialog/TTDDialogTabTriggers.tsx | 13 + src/components/TTDDialog/TTDDialogTabs.tsx | 38 ++ src/components/TTDDialog/TTDDialogTrigger.tsx | 34 ++ src/components/TTDDialog/common.ts | 153 +++++++++ src/components/dropdownMenu/DropdownMenu.scss | 4 + .../dropdownMenu/DropdownMenuItem.tsx | 28 +- src/components/icons.tsx | 13 + src/context/tunnels.ts | 2 + src/css/styles.scss | 1 + src/locales/en.json | 4 +- src/packages/excalidraw/example/App.tsx | 18 +- src/packages/excalidraw/index.tsx | 2 + src/tests/MermaidToExcalidraw.test.tsx | 32 +- .../MermaidToExcalidraw.test.tsx.snap | 4 +- src/tests/linearElementEditor.test.tsx | 8 +- src/types.ts | 5 +- src/utils.ts | 4 + src/vite-env.d.ts | 1 + 37 files changed, 1381 insertions(+), 510 deletions(-) delete mode 100644 src/components/MermaidToExcalidraw.scss delete mode 100644 src/components/MermaidToExcalidraw.tsx create mode 100644 src/components/TTDDialog/MermaidToExcalidraw.scss create mode 100644 src/components/TTDDialog/MermaidToExcalidraw.tsx create mode 100644 src/components/TTDDialog/TTDDialog.scss create mode 100644 src/components/TTDDialog/TTDDialog.tsx create mode 100644 src/components/TTDDialog/TTDDialogInput.tsx create mode 100644 src/components/TTDDialog/TTDDialogOutput.tsx create mode 100644 src/components/TTDDialog/TTDDialogPanel.tsx create mode 100644 src/components/TTDDialog/TTDDialogPanels.tsx create mode 100644 src/components/TTDDialog/TTDDialogTab.tsx create mode 100644 src/components/TTDDialog/TTDDialogTabTrigger.tsx create mode 100644 src/components/TTDDialog/TTDDialogTabTriggers.tsx create mode 100644 src/components/TTDDialog/TTDDialogTabs.tsx create mode 100644 src/components/TTDDialog/TTDDialogTrigger.tsx create mode 100644 src/components/TTDDialog/common.ts diff --git a/.env.development b/.env.development index bee5d8944..44955884f 100644 --- a/.env.development +++ b/.env.development @@ -13,6 +13,8 @@ VITE_APP_PORTAL_URL= VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_APP=https://app.excalidraw.com +VITE_APP_AI_BACKEND=http://localhost:3015 + VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}' # put these in your .env.local, or make sure you don't commit! diff --git a/.env.production b/.env.production index 19df4b96e..26b46a52a 100644 --- a/.env.production +++ b/.env.production @@ -9,6 +9,8 @@ VITE_APP_PORTAL_URL=https://portal.excalidraw.com VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_APP=https://app.excalidraw.com +VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com + # Fill to set socket server URL used for collaboration. # Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow VITE_APP_WS_SERVER_URL= diff --git a/excalidraw-app/index.tsx b/excalidraw-app/index.tsx index 7708fc190..a63c1034e 100644 --- a/excalidraw-app/index.tsx +++ b/excalidraw-app/index.tsx @@ -25,6 +25,8 @@ import { Excalidraw, defaultLang, LiveCollaborationTrigger, + TTDDialog, + TTDDialogTrigger, } from "../src/packages/excalidraw/index"; import { AppState, @@ -773,6 +775,64 @@ const ExcalidrawWrapper = () => { )} + { + try { + const response = await fetch( + `${ + import.meta.env.VITE_APP_AI_BACKEND + }/v1/ai/text-to-diagram/generate`, + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ prompt: input }), + }, + ); + + const rateLimit = response.headers.has("X-Ratelimit-Limit") + ? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10) + : undefined; + + const rateLimitRemaining = response.headers.has( + "X-Ratelimit-Remaining", + ) + ? parseInt( + response.headers.get("X-Ratelimit-Remaining") || "0", + 10, + ) + : undefined; + + const json = await response.json(); + + if (!response.ok) { + if (response.status === 429) { + return { + rateLimit, + rateLimitRemaining, + error: new Error( + "Too many requests today, please try again tomorrow!", + ), + }; + } + + throw new Error(json.message || "Generation failed..."); + } + + const generatedResponse = json.generatedResponse; + if (!generatedResponse) { + throw new Error("Generation failed..."); + } + + return { generatedResponse, rateLimit, rateLimitRemaining }; + } catch (err: any) { + throw new Error("Request failed"); + } + }} + /> + {isCollaborating && isOffline && (
{t("alerts.collabOfflineWarning")} diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 7c9a2ca7b..f0c4d8db4 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -40,6 +40,7 @@ import { MagicIcon, } from "./icons"; import { KEYS } from "../keys"; +import { useTunnels } from "../context/tunnels"; export const SelectedShapeActions = ({ appState, @@ -235,6 +236,8 @@ export const ShapesSwitcher = ({ const laserToolSelected = activeTool.type === "laser"; const embeddableToolSelected = activeTool.type === "embeddable"; + const { TTDDialogTriggerTunnel } = useTunnels(); + return ( <> {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { @@ -338,14 +341,14 @@ export const ShapesSwitcher = ({
Generate
+ {app.props.aiEnabled !== false && } app.setOpenDialog({ name: "mermaid" })} + onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })} icon={mermaidLogoIcon} data-testid="toolbar-embeddable" > {t("toolBar.mermaidToExcalidraw")} - {app.props.aiEnabled !== false && ( <> {t("toolBar.magicframe")} + AI { - trackEvent("ai", "d2c-settings", "settings"); + trackEvent("ai", "open-settings", "d2c"); app.setOpenDialog({ name: "magicSettings", source: "settings", diff --git a/src/components/App.tsx b/src/components/App.tsx index 486f32c22..87fc57e94 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -381,7 +381,6 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; import { StaticCanvas, InteractiveCanvas } from "./canvases"; import { Renderer } from "../scene/Renderer"; import { ShapeCache } from "../scene/ShapeCache"; -import MermaidToExcalidraw from "./MermaidToExcalidraw"; import { LaserToolOverlay } from "./LaserTool/LaserTool"; import { LaserPathManager } from "./LaserTool/LaserPathManager"; import { @@ -1435,9 +1434,6 @@ class App extends React.Component { onMagicSettingsConfirm={this.onMagicSettingsConfirm} > {this.props.children} - {this.state.openDialog?.name === "mermaid" && ( - - )}
@@ -1706,7 +1702,7 @@ class App extends React.Component { this.setState({ openDialog: { name: "magicSettings", source: "generation" }, }); - trackEvent("ai", "d2c-generate", "missing-key"); + trackEvent("ai", "generate (missing key)", "d2c"); return; } @@ -1719,7 +1715,7 @@ class App extends React.Component { if (!magicFrameChildren.length) { if (source === "button") { this.setState({ errorMessage: "Cannot generate from an empty frame" }); - trackEvent("ai", "d2c-generate", "no-children"); + trackEvent("ai", "generate (no-children)", "d2c"); } else { this.setActiveTool({ type: "magicframe" }); } @@ -1761,7 +1757,7 @@ class App extends React.Component { const textFromFrameChildren = this.getTextFromElements(magicFrameChildren); - trackEvent("ai", "d2c-generate", "generating"); + trackEvent("ai", "generate (start)", "d2c"); const result = await diagramToHTML({ image: dataURL, @@ -1771,7 +1767,7 @@ class App extends React.Component { }); if (!result.ok) { - trackEvent("ai", "d2c-generate", "generating-failed"); + trackEvent("ai", "generate (failed)", "d2c"); console.error(result.error); this.updateMagicGeneration({ frameElement, @@ -1783,7 +1779,7 @@ class App extends React.Component { }); return; } - trackEvent("ai", "d2c-generate", "generating-done"); + trackEvent("ai", "generate (success)", "d2c"); if (result.choices[0].message.content == null) { this.updateMagicGeneration({ @@ -1877,7 +1873,7 @@ class App extends React.Component { this.setState({ openDialog: { name: "magicSettings", source: "tool" }, }); - trackEvent("ai", "d2c-tool", "missing-key"); + trackEvent("ai", "tool-select (missing key)", "d2c"); return; } @@ -1887,7 +1883,7 @@ class App extends React.Component { if (selectedElements.length === 0) { this.setActiveTool({ type: TOOL_TYPE.magicframe }); - trackEvent("ai", "d2c-tool", "empty-selection"); + trackEvent("ai", "tool-select (empty-selection)", "d2c"); } else { const selectedMagicFrame: ExcalidrawMagicFrameElement | false = selectedElements.length === 1 && @@ -1905,7 +1901,7 @@ class App extends React.Component { return; } - trackEvent("ai", "d2c-tool", "existing-selection"); + trackEvent("ai", "tool-select (existing selection)", "d2c"); let frame: ExcalidrawMagicFrameElement; if (selectedMagicFrame) { diff --git a/src/components/Button.tsx b/src/components/Button.tsx index bf548d72f..43b6de9e1 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -2,7 +2,11 @@ import clsx from "clsx"; import { composeEventHandlers } from "../utils"; import "./Button.scss"; -interface ButtonProps extends React.HTMLAttributes { +interface ButtonProps + extends React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement + > { type?: "button" | "submit" | "reset"; onSelect: () => any; /** whether button is in active state */ diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 996fda158..4e5cc93e9 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -62,6 +62,7 @@ import { ShapeCache } from "../scene/ShapeCache"; import Scene from "../scene/Scene"; import { LaserPointerButton } from "./LaserTool/LaserPointerButton"; import { MagicSettings } from "./MagicSettings"; +import { TTDDialog } from "./TTDDialog/TTDDialog"; interface LayerUIProps { actionManager: ActionManager; @@ -396,6 +397,7 @@ const LayerUI = ({ {t("toolBar.library")} + {appState.openDialog?.name === "ttd" && } {/* ------------------------------------------------------------------ */} {appState.isLoading && } diff --git a/src/components/MermaidToExcalidraw.scss b/src/components/MermaidToExcalidraw.scss deleted file mode 100644 index 59f44ba19..000000000 --- a/src/components/MermaidToExcalidraw.scss +++ /dev/null @@ -1,221 +0,0 @@ -@import "../css/variables.module"; - -$verticalBreakpoint: 860px; - -.excalidraw { - .dialog-mermaid { - &-title { - margin-bottom: 5px; - margin-top: 2px; - } - &-desc { - font-size: 15px; - font-style: italic; - font-weight: 500; - } - - .Modal__content .Island { - box-shadow: none; - } - - @at-root .excalidraw:not(.excalidraw--mobile)#{&} { - padding: 1.25rem; - - .Modal__content { - height: 100%; - max-height: 750px; - - @media screen and (max-width: $verticalBreakpoint) { - height: auto; - // When vertical, we want the height to span whole viewport. - // This is also important for the children not to overflow the - // modal/viewport (for some reason). - max-height: 100%; - } - - .Island { - height: 100%; - display: flex; - flex-direction: column; - flex: 1 1 auto; - - .Dialog__content { - display: flex; - flex: 1 1 auto; - } - } - } - } - } - - .dialog-mermaid-body { - width: 100%; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr auto; - height: 100%; - column-gap: 4rem; - - @media screen and (max-width: $verticalBreakpoint) { - flex-direction: column; - display: flex; - gap: 1rem; - } - } - - .dialog-mermaid-panels { - display: grid; - width: 100%; - grid-template-columns: 1fr 1fr; - justify-content: space-between; - gap: 4rem; - - grid-row: 1; - grid-column: 1 / 3; - - @media screen and (max-width: $verticalBreakpoint) { - flex-direction: column; - display: flex; - gap: 1rem; - } - - label { - font-size: 14px; - font-style: normal; - font-weight: 600; - margin-bottom: 4px; - margin-left: 4px; - - @media screen and (max-width: $verticalBreakpoint) { - margin-top: 4px; - } - } - - &-text { - display: flex; - flex-direction: column; - - textarea { - width: 20rem; - height: 100%; - resize: none; - border-radius: var(--border-radius-lg); - border: 1px solid var(--dialog-border-color); - white-space: pre-wrap; - padding: 0.85rem; - box-sizing: border-box; - width: 100%; - font-family: monospace; - - @media screen and (max-width: $verticalBreakpoint) { - width: auto; - height: 10rem; - } - } - } - - &-preview-wrapper { - display: flex; - align-items: center; - justify-content: center; - padding: 0.85rem; - box-sizing: border-box; - width: 100%; - // acts as min-height - height: 200px; - flex-grow: 1; - position: relative; - - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==") - left center; - border-radius: var(--border-radius-lg); - border: 1px solid var(--dialog-border-color); - - @media screen and (max-width: $verticalBreakpoint) { - // acts as min-height - height: 400px; - width: auto; - } - - canvas { - max-width: 100%; - max-height: 100%; - } - } - - &-preview-canvas-container { - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - flex-grow: 1; - } - - &-preview { - display: flex; - flex-direction: column; - } - - .mermaid-error { - color: red; - font-weight: 800; - font-size: 30px; - word-break: break-word; - overflow: auto; - max-height: 100%; - height: 100%; - width: 100%; - text-align: center; - position: absolute; - z-index: 10; - - p { - font-weight: 500; - font-family: Cascadia; - text-align: left; - white-space: pre-wrap; - font-size: 0.875rem; - padding: 0 10px; - } - } - } - - .dialog-mermaid-buttons { - grid-column: 2; - - .dialog-mermaid-insert { - &.excalidraw-button { - font-family: "Assistant"; - font-weight: 600; - height: 2.5rem; - margin-top: 1em; - margin-bottom: 0.3em; - width: 7.5rem; - font-size: 12px; - color: $oc-white; - background-color: var(--color-primary); - - &:hover { - background-color: var(--color-primary-darker); - } - &:active { - background-color: var(--color-primary-darkest); - } - - @media screen and (max-width: $verticalBreakpoint) { - width: 100%; - } - - @at-root .excalidraw.theme--dark#{&} { - color: var(--color-gray-100); - } - } - - span { - padding-left: 0.5rem; - display: flex; - } - } - } -} diff --git a/src/components/MermaidToExcalidraw.tsx b/src/components/MermaidToExcalidraw.tsx deleted file mode 100644 index b53d07808..000000000 --- a/src/components/MermaidToExcalidraw.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { useState, useRef, useEffect, useDeferredValue } from "react"; -import { BinaryFiles } from "../types"; -import { useApp } from "./App"; -import { Button } from "./Button"; -import { Dialog } from "./Dialog"; -import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../constants"; -import { - convertToExcalidrawElements, - exportToCanvas, -} from "../packages/excalidraw/index"; -import { NonDeletedExcalidrawElement } from "../element/types"; -import { canvasToBlob } from "../data/blob"; -import { ArrowRightIcon } from "./icons"; -import Spinner from "./Spinner"; -import "./MermaidToExcalidraw.scss"; - -import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces"; -import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw"; -import { t } from "../i18n"; -import Trans from "./Trans"; - -const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw"; -const MERMAID_EXAMPLE = - "flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]"; - -const saveMermaidDataToStorage = (data: string) => { - try { - localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data); - } catch (error: any) { - // Unable to access window.localStorage - console.error(error); - } -}; - -const importMermaidDataFromStorage = () => { - try { - const data = localStorage.getItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW); - if (data) { - return data; - } - } catch (error: any) { - // Unable to access localStorage - console.error(error); - } - - return null; -}; - -const ErrorComp = ({ error }: { error: string }) => { - return ( -
- Error!

{error}

-
- ); -}; - -const MermaidToExcalidraw = () => { - const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState<{ - loaded: boolean; - api: { - parseMermaidToExcalidraw: ( - defination: string, - options: MermaidOptions, - ) => Promise; - } | null; - }>({ loaded: false, api: null }); - - const [text, setText] = useState(""); - const deferredText = useDeferredValue(text.trim()); - const [error, setError] = useState(null); - - const canvasRef = useRef(null); - const data = useRef<{ - elements: readonly NonDeletedExcalidrawElement[]; - files: BinaryFiles | null; - }>({ elements: [], files: null }); - - const app = useApp(); - - const resetPreview = () => { - const canvasNode = canvasRef.current; - - if (!canvasNode) { - return; - } - const parent = canvasNode.parentElement; - if (!parent) { - return; - } - parent.style.background = ""; - setError(null); - canvasNode.replaceChildren(); - }; - - useEffect(() => { - const loadMermaidToExcalidrawLib = async () => { - const api = await import( - /* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw" - ); - setMermaidToExcalidrawLib({ loaded: true, api }); - }; - loadMermaidToExcalidrawLib(); - }, []); - - useEffect(() => { - const data = importMermaidDataFromStorage() || MERMAID_EXAMPLE; - setText(data); - }, []); - - useEffect(() => { - const renderExcalidrawPreview = async () => { - const canvasNode = canvasRef.current; - const parent = canvasNode?.parentElement; - if ( - !mermaidToExcalidrawLib.loaded || - !canvasNode || - !parent || - !mermaidToExcalidrawLib.api - ) { - return; - } - if (!deferredText) { - resetPreview(); - return; - } - try { - const { elements, files } = - await mermaidToExcalidrawLib.api.parseMermaidToExcalidraw( - deferredText, - { - fontSize: DEFAULT_FONT_SIZE, - }, - ); - setError(null); - - data.current = { - elements: convertToExcalidrawElements(elements, { - regenerateIds: true, - }), - files, - }; - - const canvas = await exportToCanvas({ - elements: data.current.elements, - files: data.current.files, - exportPadding: DEFAULT_EXPORT_PADDING, - maxWidthOrHeight: - Math.max(parent.offsetWidth, parent.offsetHeight) * - window.devicePixelRatio, - }); - // if converting to blob fails, there's some problem that will - // likely prevent preview and export (e.g. canvas too big) - await canvasToBlob(canvas); - parent.style.background = "var(--default-bg-color)"; - canvasNode.replaceChildren(canvas); - } catch (e: any) { - parent.style.background = "var(--default-bg-color)"; - if (deferredText) { - setError(e.message); - } - } - }; - renderExcalidrawPreview(); - }, [deferredText, mermaidToExcalidrawLib]); - - const onClose = () => { - app.setOpenDialog(null); - saveMermaidDataToStorage(text); - }; - - const onSelect = () => { - const { elements: newElements, files } = data.current; - app.addElementsFromPasteOrLibrary({ - elements: newElements, - files, - position: "center", - fitToContent: true, - }); - onClose(); - }; - - return ( - -

{t("mermaid.title")}

- - ( - {el} - )} - sequenceLink={(el) => ( - - {el} - - )} - /> -
-
- - } - > -
-
-
- - -
" + C -->|Three| F[Car]
" `; diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index 1e7ebeb0e..9b11bfbdd 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -273,7 +273,7 @@ describe("Test Linear Elements", () => { // drag line from midpoint drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); - expect(renderInteractiveScene).toHaveBeenCalledTimes(13); + expect(renderInteractiveScene).toHaveBeenCalledTimes(14); expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(line.points.length).toEqual(3); @@ -416,7 +416,7 @@ describe("Test Linear Elements", () => { lastSegmentMidpoint[1] + delta, ]); - expect(renderInteractiveScene).toHaveBeenCalledTimes(19); + expect(renderInteractiveScene).toHaveBeenCalledTimes(21); expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(line.points.length).toEqual(5); @@ -519,7 +519,7 @@ describe("Test Linear Elements", () => { // delete 3rd point deletePoint(points[2]); expect(line.points.length).toEqual(3); - expect(renderInteractiveScene).toHaveBeenCalledTimes(20); + expect(renderInteractiveScene).toHaveBeenCalledTimes(21); expect(renderStaticScene).toHaveBeenCalledTimes(9); const newMidPoints = LinearElementEditor.getEditorMidPoints( @@ -566,7 +566,7 @@ describe("Test Linear Elements", () => { lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta, ]); - expect(renderInteractiveScene).toHaveBeenCalledTimes(19); + expect(renderInteractiveScene).toHaveBeenCalledTimes(21); expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(line.points.length).toEqual(5); diff --git a/src/types.ts b/src/types.ts index 507123ba9..94a484604 100644 --- a/src/types.ts +++ b/src/types.ts @@ -246,14 +246,15 @@ export interface AppState { openSidebar: { name: SidebarName; tab?: SidebarTabName } | null; openDialog: | null - | { name: "imageExport" | "help" | "jsonExport" | "mermaid" } + | { name: "imageExport" | "help" | "jsonExport" } | { name: "magicSettings"; source: | "tool" // when magicframe tool is selected | "generation" // when magicframe generate button is clicked | "settings"; // when AI settings dialog is explicitly invoked - }; + } + | { name: "ttd"; tab: string }; /** * Reflects user preference for whether the default sidebar should be docked. * diff --git a/src/utils.ts b/src/utils.ts index 223cbab9d..c092fea7a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -925,3 +925,7 @@ export const isMemberOf = ( }; export const cloneJSON = (obj: T): T => JSON.parse(JSON.stringify(obj)); + +export const isFiniteNumber = (value: any): value is number => { + return typeof value === "number" && Number.isFinite(value); +}; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index cd4030b13..c4ab79c37 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -17,6 +17,7 @@ interface ImportMetaEnv { // set this only if using the collaboration workflow we use on excalidraw.com VITE_APP_PORTAL_URL: string; + VITE_APP_AI_BACKEND: string; VITE_APP_FIREBASE_CONFIG: string;