mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			are/tte
			...
			zsviczian-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 44390cb146 | ||
|   | 9da3e47877 | ||
|   | 9f6edd8eaa | ||
|   | 0d60253de7 | ||
|   | 4027a5b245 | ||
|   | 49f2c88978 | ||
|   | 156b8b422b | ||
|   | e2982a2968 | 
| @@ -13,8 +13,6 @@ 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! | ||||
|   | ||||
| @@ -9,8 +9,6 @@ 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= | ||||
|   | ||||
| @@ -39,7 +39,7 @@ Since Vite removes env variables by default, you can update the vite config to e | ||||
|  | ||||
| ``` | ||||
|  define: { | ||||
|     "process.env.IS_PREACT": JSON.stringify("true"), | ||||
|     "process.env.IS_PREACT": process.env.IS_PREACT, | ||||
|   }, | ||||
| ``` | ||||
|  | ||||
|   | ||||
| @@ -93,7 +93,7 @@ Since Vite removes env variables by default, you can update the vite config to e | ||||
|  | ||||
| ``` | ||||
|  define: { | ||||
|     "process.env.IS_PREACT": JSON.stringify("true"), | ||||
|     "process.env.IS_PREACT": process.env.IS_PREACT, | ||||
|   }, | ||||
| ``` | ||||
| :::  | ||||
|   | ||||
| @@ -25,8 +25,6 @@ import { | ||||
|   Excalidraw, | ||||
|   defaultLang, | ||||
|   LiveCollaborationTrigger, | ||||
|   TTDDialog, | ||||
|   TTDDialogTrigger, | ||||
| } from "../src/packages/excalidraw/index"; | ||||
| import { | ||||
|   AppState, | ||||
| @@ -104,7 +102,6 @@ import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog"; | ||||
| import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState"; | ||||
| import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm"; | ||||
| import Trans from "../src/components/Trans"; | ||||
| import { drawingIcon } from "../src/components/icons"; | ||||
|  | ||||
| polyfill(); | ||||
|  | ||||
| @@ -776,65 +773,6 @@ const ExcalidrawWrapper = () => { | ||||
|           )} | ||||
|         </OverwriteConfirmDialog> | ||||
|         <AppFooter /> | ||||
|         <TTDDialog | ||||
|           onTextSubmit={async (input, type) => { | ||||
|             try { | ||||
|               const response = await fetch( | ||||
|                 `${import.meta.env.VITE_APP_AI_BACKEND}/v1/ai/${type}/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"); | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|         <TTDDialogTrigger /> | ||||
|         <TTDDialogTrigger tab="text-to-drawing" icon={drawingIcon}> | ||||
|           {t("labels.textToDrawing")} | ||||
|         </TTDDialogTrigger> | ||||
|         {isCollaborating && isOffline && ( | ||||
|           <div className="collab-offline-warning"> | ||||
|             {t("alerts.collabOfflineWarning")} | ||||
|   | ||||
| @@ -40,7 +40,6 @@ import { | ||||
|   MagicIcon, | ||||
| } from "./icons"; | ||||
| import { KEYS } from "../keys"; | ||||
| import { useTunnels } from "../context/tunnels"; | ||||
|  | ||||
| export const SelectedShapeActions = ({ | ||||
|   appState, | ||||
| @@ -236,8 +235,6 @@ 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) => { | ||||
| @@ -341,14 +338,14 @@ export const ShapesSwitcher = ({ | ||||
|           <div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}> | ||||
|             Generate | ||||
|           </div> | ||||
|           {app.props.aiEnabled !== false && <TTDDialogTriggerTunnel.Out />} | ||||
|           <DropdownMenu.Item | ||||
|             onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })} | ||||
|             onSelect={() => app.setOpenDialog({ name: "mermaid" })} | ||||
|             icon={mermaidLogoIcon} | ||||
|             data-testid="toolbar-embeddable" | ||||
|           > | ||||
|             {t("toolBar.mermaidToExcalidraw")} | ||||
|           </DropdownMenu.Item> | ||||
|  | ||||
|           {app.props.aiEnabled !== false && ( | ||||
|             <> | ||||
|               <DropdownMenu.Item | ||||
| @@ -357,15 +354,13 @@ export const ShapesSwitcher = ({ | ||||
|                 data-testid="toolbar-magicframe" | ||||
|               > | ||||
|                 {t("toolBar.magicframe")} | ||||
|                 <DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge> | ||||
|               </DropdownMenu.Item> | ||||
|               <DropdownMenu.Item | ||||
|                 onSelect={() => { | ||||
|                   trackEvent("ai", "open-settings", "d2c"); | ||||
|                   trackEvent("ai", "d2c-settings", "settings"); | ||||
|                   app.setOpenDialog({ | ||||
|                     name: "settings", | ||||
|                     name: "magicSettings", | ||||
|                     source: "settings", | ||||
|                     tab: "diagram-to-code", | ||||
|                   }); | ||||
|                 }} | ||||
|                 icon={OpenAIIcon} | ||||
|   | ||||
| @@ -381,6 +381,7 @@ 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 { | ||||
| @@ -1037,6 +1038,10 @@ class App extends React.Component<AppProps, AppState> { | ||||
|             this.state.activeEmbeddable?.element === el && | ||||
|             this.state.activeEmbeddable?.state === "hover"; | ||||
|  | ||||
|           // Modify the scale based on el.scale property | ||||
|           const [xScale, yScale] = el.scale ?? [1, 1]; | ||||
|           const scaledTransform = `scale(${scale * xScale}, ${scale * yScale})`; | ||||
|  | ||||
|           return ( | ||||
|             <div | ||||
|               key={el.id} | ||||
| @@ -1047,14 +1052,13 @@ class App extends React.Component<AppProps, AppState> { | ||||
|                 transform: isVisible | ||||
|                   ? `translate(${x - this.state.offsetLeft}px, ${ | ||||
|                       y - this.state.offsetTop | ||||
|                     }px) scale(${scale})` | ||||
|                     }px) ${scaledTransform}` | ||||
|                   : "none", | ||||
|                 display: isVisible ? "block" : "none", | ||||
|                 opacity: el.opacity / 100, | ||||
|                 ["--embeddable-radius" as string]: `${getCornerRadius( | ||||
|                   Math.min(el.width, el.height), | ||||
|                   el, | ||||
|                 )}px`, | ||||
|                 ["--embeddable-radius" as string]: `${ | ||||
|                   getCornerRadius(Math.min(el.width, el.height), el) / xScale | ||||
|                 }px`, | ||||
|               }} | ||||
|             > | ||||
|               <div | ||||
| @@ -1076,8 +1080,8 @@ class App extends React.Component<AppProps, AppState> { | ||||
|                 }}*/ | ||||
|                 className="excalidraw__embeddable-container__inner" | ||||
|                 style={{ | ||||
|                   width: isVisible ? `${el.width}px` : 0, | ||||
|                   height: isVisible ? `${el.height}px` : 0, | ||||
|                   width: isVisible ? `${el.width / xScale}px` : 0, | ||||
|                   height: isVisible ? `${el.height / yScale}px` : 0, | ||||
|                   transform: isVisible ? `rotate(${el.angle}rad)` : "none", | ||||
|                   pointerEvents: isActive | ||||
|                     ? POINTER_EVENTS.enabled | ||||
| @@ -1092,7 +1096,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|                 <div | ||||
|                   className="excalidraw__embeddable__outer" | ||||
|                   style={{ | ||||
|                     padding: `${el.strokeWidth}px`, | ||||
|                     padding: `${el.strokeWidth / el.scale[0]}px`, | ||||
|                   }} | ||||
|                 > | ||||
|                   {(isEmbeddableElement(el) | ||||
| @@ -1434,6 +1438,9 @@ class App extends React.Component<AppProps, AppState> { | ||||
|                           onMagicSettingsConfirm={this.onMagicSettingsConfirm} | ||||
|                         > | ||||
|                           {this.props.children} | ||||
|                           {this.state.openDialog?.name === "mermaid" && ( | ||||
|                             <MermaidToExcalidraw /> | ||||
|                           )} | ||||
|                         </LayerUI> | ||||
|  | ||||
|                         <div className="excalidraw-textEditorContainer" /> | ||||
| @@ -1700,13 +1707,9 @@ class App extends React.Component<AppProps, AppState> { | ||||
|   ) { | ||||
|     if (!this.OPENAI_KEY) { | ||||
|       this.setState({ | ||||
|         openDialog: { | ||||
|           name: "settings", | ||||
|           tab: "diagram-to-code", | ||||
|           source: "generation", | ||||
|         }, | ||||
|         openDialog: { name: "magicSettings", source: "generation" }, | ||||
|       }); | ||||
|       trackEvent("ai", "generate (missing key)", "d2c"); | ||||
|       trackEvent("ai", "d2c-generate", "missing-key"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -1719,7 +1722,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|     if (!magicFrameChildren.length) { | ||||
|       if (source === "button") { | ||||
|         this.setState({ errorMessage: "Cannot generate from an empty frame" }); | ||||
|         trackEvent("ai", "generate (no-children)", "d2c"); | ||||
|         trackEvent("ai", "d2c-generate", "no-children"); | ||||
|       } else { | ||||
|         this.setActiveTool({ type: "magicframe" }); | ||||
|       } | ||||
| @@ -1761,7 +1764,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|  | ||||
|     const textFromFrameChildren = this.getTextFromElements(magicFrameChildren); | ||||
|  | ||||
|     trackEvent("ai", "generate (start)", "d2c"); | ||||
|     trackEvent("ai", "d2c-generate", "generating"); | ||||
|  | ||||
|     const result = await diagramToHTML({ | ||||
|       image: dataURL, | ||||
| @@ -1771,7 +1774,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|     }); | ||||
|  | ||||
|     if (!result.ok) { | ||||
|       trackEvent("ai", "generate (failed)", "d2c"); | ||||
|       trackEvent("ai", "d2c-generate", "generating-failed"); | ||||
|       console.error(result.error); | ||||
|       this.updateMagicGeneration({ | ||||
|         frameElement, | ||||
| @@ -1783,7 +1786,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     trackEvent("ai", "generate (success)", "d2c"); | ||||
|     trackEvent("ai", "d2c-generate", "generating-done"); | ||||
|  | ||||
|     if (result.choices[0].message.content == null) { | ||||
|       this.updateMagicGeneration({ | ||||
| @@ -1875,13 +1878,9 @@ class App extends React.Component<AppProps, AppState> { | ||||
|   public onMagicframeToolSelect = () => { | ||||
|     if (!this.OPENAI_KEY) { | ||||
|       this.setState({ | ||||
|         openDialog: { | ||||
|           name: "settings", | ||||
|           tab: "diagram-to-code", | ||||
|           source: "tool", | ||||
|         }, | ||||
|         openDialog: { name: "magicSettings", source: "tool" }, | ||||
|       }); | ||||
|       trackEvent("ai", "tool-select (missing key)", "d2c"); | ||||
|       trackEvent("ai", "d2c-tool", "missing-key"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -1891,7 +1890,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|  | ||||
|     if (selectedElements.length === 0) { | ||||
|       this.setActiveTool({ type: TOOL_TYPE.magicframe }); | ||||
|       trackEvent("ai", "tool-select (empty-selection)", "d2c"); | ||||
|       trackEvent("ai", "d2c-tool", "empty-selection"); | ||||
|     } else { | ||||
|       const selectedMagicFrame: ExcalidrawMagicFrameElement | false = | ||||
|         selectedElements.length === 1 && | ||||
| @@ -1909,7 +1908,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       trackEvent("ai", "tool-select (existing selection)", "d2c"); | ||||
|       trackEvent("ai", "d2c-tool", "existing-selection"); | ||||
|  | ||||
|       let frame: ExcalidrawMagicFrameElement; | ||||
|       if (selectedMagicFrame) { | ||||
|   | ||||
| @@ -2,11 +2,7 @@ import clsx from "clsx"; | ||||
| import { composeEventHandlers } from "../utils"; | ||||
| import "./Button.scss"; | ||||
|  | ||||
| interface ButtonProps | ||||
|   extends React.DetailedHTMLProps< | ||||
|     React.ButtonHTMLAttributes<HTMLButtonElement>, | ||||
|     HTMLButtonElement | ||||
|   > { | ||||
| interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> { | ||||
|   type?: "button" | "submit" | "reset"; | ||||
|   onSelect: () => any; | ||||
|   /** whether button is in active state */ | ||||
|   | ||||
| @@ -62,7 +62,6 @@ 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; | ||||
| @@ -397,7 +396,6 @@ const LayerUI = ({ | ||||
|         {t("toolBar.library")} | ||||
|       </DefaultSidebar.Trigger> | ||||
|       <DefaultOverwriteConfirmDialog /> | ||||
|       {appState.openDialog?.name === "ttd" && <TTDDialog __fallback />} | ||||
|       {/* ------------------------------------------------------------------ */} | ||||
|  | ||||
|       {appState.isLoading && <LoadingMessage delay={250} />} | ||||
| @@ -461,14 +459,14 @@ const LayerUI = ({ | ||||
|           }} | ||||
|         /> | ||||
|       )} | ||||
|       {appState.openDialog?.name === "settings" && ( | ||||
|       {appState.openDialog?.name === "magicSettings" && ( | ||||
|         <MagicSettings | ||||
|           openAIKey={openAIKey} | ||||
|           isPersisted={isOpenAIKeyPersisted} | ||||
|           onChange={onOpenAIAPIKeyChange} | ||||
|           onConfirm={(apiKey, shouldPersist) => { | ||||
|             const source = | ||||
|               appState.openDialog?.name === "settings" | ||||
|               appState.openDialog?.name === "magicSettings" | ||||
|                 ? appState.openDialog?.source | ||||
|                 : "settings"; | ||||
|             setAppState({ openDialog: null }, () => { | ||||
|   | ||||
| @@ -1,18 +1,9 @@ | ||||
| .excalidraw { | ||||
|   .MagicSettings { | ||||
|     .Island { | ||||
|       height: 100%; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .MagicSettings-confirm { | ||||
|     padding: 0.5rem 1rem; | ||||
|   } | ||||
|  | ||||
|   .MagicSettings__confirm { | ||||
|     margin-top: 2rem; | ||||
|     margin-right: auto; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -10,8 +10,6 @@ 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; | ||||
| @@ -20,21 +18,16 @@ 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={() => { | ||||
| @@ -43,7 +36,7 @@ export const MagicSettings = (props: { | ||||
|       }} | ||||
|       title={ | ||||
|         <div style={{ display: "flex" }}> | ||||
|           Wireframe to Code (AI){" "} | ||||
|           Diagram to Code (AI){" "} | ||||
|           <div | ||||
|             style={{ | ||||
|               display: "flex", | ||||
| @@ -53,8 +46,7 @@ export const MagicSettings = (props: { | ||||
|               marginLeft: "1rem", | ||||
|               fontSize: 14, | ||||
|               borderRadius: "12px", | ||||
|               color: "#000", | ||||
|               background: "pink", | ||||
|               background: theme === "light" ? "#FFCCCC" : "#703333", | ||||
|             }} | ||||
|           > | ||||
|             Experimental | ||||
| @@ -64,97 +56,75 @@ export const MagicSettings = (props: { | ||||
|       className="MagicSettings" | ||||
|       autofocus={false} | ||||
|     > | ||||
|       {/*  <h2 | ||||
|       <Paragraph | ||||
|         style={{ | ||||
|           margin: 0, | ||||
|           fontSize: "1.25rem", | ||||
|           paddingLeft: "2.5rem", | ||||
|           display: "inline-flex", | ||||
|           alignItems: "center", | ||||
|           marginBottom: 0, | ||||
|         }} | ||||
|       > | ||||
|         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" | ||||
|         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" | ||||
|         > | ||||
|           <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> | ||||
|           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} | ||||
|           /> | ||||
|         </TTDDialogTab> | ||||
|       </TTDDialogTabs> | ||||
|       <FilledButton | ||||
|         className="MagicSettings__confirm" | ||||
|         size="large" | ||||
|         label="Confirm" | ||||
|         onClick={onConfirm} | ||||
|       /> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										221
									
								
								src/components/MermaidToExcalidraw.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								src/components/MermaidToExcalidraw.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | ||||
| @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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										243
									
								
								src/components/MermaidToExcalidraw.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								src/components/MermaidToExcalidraw.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,243 @@ | ||||
| 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 ( | ||||
|     <div data-testid="mermaid-error" className="mermaid-error"> | ||||
|       Error! <p>{error}</p> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const MermaidToExcalidraw = () => { | ||||
|   const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState<{ | ||||
|     loaded: boolean; | ||||
|     api: { | ||||
|       parseMermaidToExcalidraw: ( | ||||
|         defination: string, | ||||
|         options: MermaidOptions, | ||||
|       ) => Promise<MermaidToExcalidrawResult>; | ||||
|     } | null; | ||||
|   }>({ loaded: false, api: null }); | ||||
|  | ||||
|   const [text, setText] = useState(""); | ||||
|   const deferredText = useDeferredValue(text.trim()); | ||||
|   const [error, setError] = useState(null); | ||||
|  | ||||
|   const canvasRef = useRef<HTMLDivElement>(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 ( | ||||
|     <Dialog | ||||
|       className="dialog-mermaid" | ||||
|       onCloseRequest={onClose} | ||||
|       size={1200} | ||||
|       title={ | ||||
|         <> | ||||
|           <p className="dialog-mermaid-title">{t("mermaid.title")}</p> | ||||
|           <span className="dialog-mermaid-desc"> | ||||
|             <Trans | ||||
|               i18nKey="mermaid.description" | ||||
|               flowchartLink={(el) => ( | ||||
|                 <a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a> | ||||
|               )} | ||||
|               sequenceLink={(el) => ( | ||||
|                 <a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> | ||||
|                   {el} | ||||
|                 </a> | ||||
|               )} | ||||
|             /> | ||||
|             <br /> | ||||
|           </span> | ||||
|         </> | ||||
|       } | ||||
|     > | ||||
|       <div className="dialog-mermaid-body"> | ||||
|         <div className="dialog-mermaid-panels"> | ||||
|           <div className="dialog-mermaid-panels-text"> | ||||
|             <label>{t("mermaid.syntax")}</label> | ||||
|  | ||||
|             <textarea | ||||
|               onChange={(event) => setText(event.target.value)} | ||||
|               value={text} | ||||
|             /> | ||||
|           </div> | ||||
|           <div className="dialog-mermaid-panels-preview"> | ||||
|             <label>{t("mermaid.preview")}</label> | ||||
|             <div className="dialog-mermaid-panels-preview-wrapper"> | ||||
|               {error && <ErrorComp error={error} />} | ||||
|               {mermaidToExcalidrawLib.loaded ? ( | ||||
|                 <div | ||||
|                   ref={canvasRef} | ||||
|                   style={{ opacity: error ? "0.15" : 1 }} | ||||
|                   className="dialog-mermaid-panels-preview-canvas-container" | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <Spinner size="2rem" /> | ||||
|               )} | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="dialog-mermaid-buttons"> | ||||
|           <Button className="dialog-mermaid-insert" onSelect={onSelect}> | ||||
|             {t("mermaid.button")} | ||||
|             <span>{ArrowRightIcon}</span> | ||||
|           </Button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
| export default MermaidToExcalidraw; | ||||
| @@ -18,11 +18,8 @@ | ||||
|     overflow: auto; | ||||
|     padding: calc(var(--space-factor) * 10); | ||||
|  | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|  | ||||
|     .Island { | ||||
|       padding: 2.5rem; | ||||
|       padding: 2.5rem !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| .excalidraw { | ||||
|   .dialog-mermaid { | ||||
|     &-title { | ||||
|       margin-block: 0.25rem; | ||||
|       font-size: 1.25rem; | ||||
|       font-weight: 700; | ||||
|       padding-inline: 2.5rem; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,133 +0,0 @@ | ||||
| import { useState, useRef, useEffect, useDeferredValue } from "react"; | ||||
| import { BinaryFiles } from "../../types"; | ||||
| import { useApp } from "../App"; | ||||
| import { NonDeletedExcalidrawElement } from "../../element/types"; | ||||
| import { ArrowRightIcon } from "../icons"; | ||||
| import "./MermaidToExcalidraw.scss"; | ||||
| import { t } from "../../i18n"; | ||||
| import Trans from "../Trans"; | ||||
| import { | ||||
|   LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, | ||||
|   MermaidToExcalidrawLibProps, | ||||
|   convertMermaidToExcalidraw, | ||||
|   insertToEditor, | ||||
|   saveMermaidDataToStorage, | ||||
| } from "./common"; | ||||
| import { TTDDialogPanels } from "./TTDDialogPanels"; | ||||
| import { TTDDialogPanel } from "./TTDDialogPanel"; | ||||
| import { TTDDialogInput } from "./TTDDialogInput"; | ||||
| import { TTDDialogOutput } from "./TTDDialogOutput"; | ||||
|  | ||||
| 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 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 MermaidToExcalidraw = ({ | ||||
|   mermaidToExcalidrawLib, | ||||
| }: { | ||||
|   mermaidToExcalidrawLib: MermaidToExcalidrawLibProps; | ||||
| }) => { | ||||
|   const [text, setText] = useState(""); | ||||
|   const deferredText = useDeferredValue(text.trim()); | ||||
|   const [error, setError] = useState<Error | null>(null); | ||||
|  | ||||
|   const canvasRef = useRef<HTMLDivElement>(null); | ||||
|   const data = useRef<{ | ||||
|     elements: readonly NonDeletedExcalidrawElement[]; | ||||
|     files: BinaryFiles | null; | ||||
|   }>({ elements: [], files: null }); | ||||
|  | ||||
|   const app = useApp(); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const data = importMermaidDataFromStorage() || MERMAID_EXAMPLE; | ||||
|     setText(data); | ||||
|   }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     convertMermaidToExcalidraw({ | ||||
|       canvasRef, | ||||
|       data, | ||||
|       mermaidToExcalidrawLib, | ||||
|       setError, | ||||
|       mermaidDefinition: deferredText, | ||||
|     }).catch(() => {}); | ||||
|   }, [deferredText, mermaidToExcalidrawLib]); | ||||
|  | ||||
|   const textRef = useRef(text); | ||||
|  | ||||
|   // slightly hacky but really quite simple | ||||
|   // essentially, we want to save the text to LS when the component unmounts | ||||
|   useEffect(() => { | ||||
|     textRef.current = text; | ||||
|   }, [text]); | ||||
|   useEffect(() => { | ||||
|     return () => { | ||||
|       if (textRef.current) { | ||||
|         saveMermaidDataToStorage(textRef.current); | ||||
|       } | ||||
|     }; | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="ttd-dialog-desc"> | ||||
|         <Trans | ||||
|           i18nKey="mermaid.description" | ||||
|           flowchartLink={(el) => ( | ||||
|             <a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a> | ||||
|           )} | ||||
|           sequenceLink={(el) => ( | ||||
|             <a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> | ||||
|               {el} | ||||
|             </a> | ||||
|           )} | ||||
|         /> | ||||
|       </div> | ||||
|       <TTDDialogPanels> | ||||
|         <TTDDialogPanel label={t("mermaid.syntax")}> | ||||
|           <TTDDialogInput | ||||
|             input={text} | ||||
|             placeholder={"Write Mermaid diagram defintion here..."} | ||||
|             onChange={(event) => setText(event.target.value)} | ||||
|           /> | ||||
|         </TTDDialogPanel> | ||||
|         <TTDDialogPanel | ||||
|           label={t("mermaid.preview")} | ||||
|           panelAction={{ | ||||
|             action: () => { | ||||
|               insertToEditor({ | ||||
|                 app, | ||||
|                 data: data.current, | ||||
|                 text, | ||||
|                 shouldSaveMermaidDataToStorage: true, | ||||
|               }); | ||||
|             }, | ||||
|             label: t("mermaid.button"), | ||||
|             icon: ArrowRightIcon, | ||||
|           }} | ||||
|         > | ||||
|           <TTDDialogOutput | ||||
|             canvasRef={canvasRef} | ||||
|             loaded={mermaidToExcalidrawLib.loaded} | ||||
|             error={error} | ||||
|           /> | ||||
|         </TTDDialogPanel> | ||||
|       </TTDDialogPanels> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| export default MermaidToExcalidraw; | ||||
| @@ -1,301 +0,0 @@ | ||||
| @import "../../css/variables.module"; | ||||
|  | ||||
| $verticalBreakpoint: 861px; | ||||
|  | ||||
| .excalidraw { | ||||
|   .Modal.Dialog.ttd-dialog { | ||||
|     padding: 1.25rem; | ||||
|  | ||||
|     &.Dialog--fullscreen { | ||||
|       margin-top: 0; | ||||
|     } | ||||
|  | ||||
|     .Island { | ||||
|       padding-inline: 0 !important; | ||||
|       height: 100%; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       flex: 1 1 auto; | ||||
|       box-shadow: none; | ||||
|     } | ||||
|  | ||||
|     .Modal__content { | ||||
|       height: auto; | ||||
|       max-height: 100%; | ||||
|  | ||||
|       @media screen and (min-width: $verticalBreakpoint) { | ||||
|         max-height: 750px; | ||||
|         height: 100%; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .Dialog__content { | ||||
|       flex: 1 1 auto; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-desc { | ||||
|     font-size: 15px; | ||||
|     font-style: italic; | ||||
|     font-weight: 500; | ||||
|     margin-bottom: 1.5rem; | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-tabs-root { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-tab-trigger { | ||||
|     color: var(--color-on-surface); | ||||
|     font-size: 0.875rem; | ||||
|     margin: 0; | ||||
|     padding: 0 1rem; | ||||
|     background-color: transparent; | ||||
|     border: 0; | ||||
|     height: 2.875rem; | ||||
|     font-weight: 600; | ||||
|     font-family: inherit; | ||||
|     letter-spacing: 0.4px; | ||||
|  | ||||
|     &[data-state="active"] { | ||||
|       border-bottom: 2px solid var(--color-primary); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-triggers { | ||||
|     border-bottom: 1px solid var(--color-surface-high); | ||||
|     margin-bottom: 1.5rem; | ||||
|     padding-inline: 2.5rem; | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-content { | ||||
|     padding-inline: 2.5rem; | ||||
|     height: 100%; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|  | ||||
|     &[hidden] { | ||||
|       display: none; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-input { | ||||
|     width: auto; | ||||
|     height: 10rem; | ||||
|     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; | ||||
|     font-family: monospace; | ||||
|  | ||||
|     @media screen and (min-width: $verticalBreakpoint) { | ||||
|       width: 100%; | ||||
|       height: 100%; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-output-wrapper { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     padding: 0.85rem; | ||||
|     box-sizing: border-box; | ||||
|     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); | ||||
|  | ||||
|     height: 400px; | ||||
|     width: auto; | ||||
|  | ||||
|     @media screen and (min-width: $verticalBreakpoint) { | ||||
|       width: 100%; | ||||
|       // acts as min-height | ||||
|       height: 200px; | ||||
|     } | ||||
|  | ||||
|     canvas { | ||||
|       max-width: 100%; | ||||
|       max-height: 100%; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-output-canvas-container { | ||||
|     display: flex; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     flex-grow: 1; | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-output-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; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-panels { | ||||
|     height: 100%; | ||||
|  | ||||
|     @media screen and (min-width: $verticalBreakpoint) { | ||||
|       display: grid; | ||||
|       grid-template-columns: 1fr 1fr; | ||||
|       gap: 4rem; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-panel { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     width: 100%; | ||||
|  | ||||
|     &__header { | ||||
|       display: flex; | ||||
|       margin: 0px 4px 4px 4px; | ||||
|       align-items: center; | ||||
|       gap: 1rem; | ||||
|  | ||||
|       label { | ||||
|         font-size: 14px; | ||||
|         font-style: normal; | ||||
|         font-weight: 600; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &:first-child { | ||||
|       .ttd-dialog-panel-button-container:not(.invisible) { | ||||
|         margin-bottom: 4rem; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     @media screen and (min-width: $verticalBreakpoint) { | ||||
|       .ttd-dialog-panel-button-container:not(.invisible) { | ||||
|         margin-bottom: 0.5rem !important; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     textarea { | ||||
|       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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-panel-button-container { | ||||
|     margin-top: 1rem; | ||||
|     margin-bottom: 0.5rem; | ||||
|  | ||||
|     &.invisible { | ||||
|       .ttd-dialog-panel-button { | ||||
|         display: none; | ||||
|  | ||||
|         @media screen and (min-width: $verticalBreakpoint) { | ||||
|           display: block; | ||||
|           visibility: hidden; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .ttd-dialog-panel-button { | ||||
|     &.excalidraw-button { | ||||
|       font-family: inherit; | ||||
|       font-weight: 600; | ||||
|       height: 2.5rem; | ||||
|  | ||||
|       font-size: 12px; | ||||
|       color: $oc-white; | ||||
|       background-color: var(--color-primary); | ||||
|       width: 100%; | ||||
|  | ||||
|       &:hover { | ||||
|         background-color: var(--color-primary-darker); | ||||
|       } | ||||
|       &:active { | ||||
|         background-color: var(--color-primary-darkest); | ||||
|       } | ||||
|  | ||||
|       &:disabled { | ||||
|         opacity: 0.5; | ||||
|         cursor: not-allowed; | ||||
|  | ||||
|         &:hover { | ||||
|           background-color: var(--color-primary); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       @media screen and (min-width: $verticalBreakpoint) { | ||||
|         width: auto; | ||||
|         min-width: 7.5rem; | ||||
|       } | ||||
|  | ||||
|       @at-root .excalidraw.theme--dark#{&} { | ||||
|         color: var(--color-gray-100); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     position: relative; | ||||
|  | ||||
|     div { | ||||
|       display: contents; | ||||
|  | ||||
|       &.invisible { | ||||
|         visibility: hidden; | ||||
|       } | ||||
|  | ||||
|       &.Spinner { | ||||
|         display: flex !important; | ||||
|         position: absolute; | ||||
|         inset: 0; | ||||
|  | ||||
|         --spinner-color: white; | ||||
|  | ||||
|         @at-root .excalidraw.theme--dark#{&} { | ||||
|           --spinner-color: var(--color-gray-100); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       span { | ||||
|         padding-left: 0.5rem; | ||||
|         display: flex; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,139 +0,0 @@ | ||||
| import { Dialog } from "../Dialog"; | ||||
| import { useApp } from "../App"; | ||||
| import MermaidToExcalidraw from "./MermaidToExcalidraw"; | ||||
| import TTDDialogTabs from "./TTDDialogTabs"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useUIAppState } from "../../context/ui-appState"; | ||||
| import { withInternalFallback } from "../hoc/withInternalFallback"; | ||||
| import { TTDDialogTabTriggers } from "./TTDDialogTabTriggers"; | ||||
| import { TTDDialogTabTrigger } from "./TTDDialogTabTrigger"; | ||||
| import { TTDDialogTab } from "./TTDDialogTab"; | ||||
| import { t } from "../../i18n"; | ||||
| import { CommonDialogProps, MermaidToExcalidrawLibProps } from "./common"; | ||||
|  | ||||
| import "./TTDDialog.scss"; | ||||
| import { TextToDiagram } from "./TextToDiagram"; | ||||
| import { TextToDrawing } from "./TextToDrawing"; | ||||
|  | ||||
| export const TTDDialog = (props: CommonDialogProps | { __fallback: true }) => { | ||||
|   const appState = useUIAppState(); | ||||
|  | ||||
|   if (appState.openDialog?.name !== "ttd") { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return <TTDDialogBase {...props} tab={appState.openDialog.tab} />; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Text to diagram (TTD) dialog | ||||
|  */ | ||||
| export const TTDDialogBase = withInternalFallback( | ||||
|   "TTDDialogBase", | ||||
|   ({ | ||||
|     tab, | ||||
|     ...rest | ||||
|   }: { | ||||
|     tab: "text-to-diagram" | "mermaid" | "text-to-drawing"; | ||||
|   } & (CommonDialogProps | { __fallback: true })) => { | ||||
|     const app = useApp(); | ||||
|  | ||||
|     const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = | ||||
|       useState<MermaidToExcalidrawLibProps>({ | ||||
|         loaded: false, | ||||
|         api: import( | ||||
|           /* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw" | ||||
|         ), | ||||
|       }); | ||||
|  | ||||
|     useEffect(() => { | ||||
|       const fn = async () => { | ||||
|         await mermaidToExcalidrawLib.api; | ||||
|         setMermaidToExcalidrawLib((prev) => ({ ...prev, loaded: true })); | ||||
|       }; | ||||
|       fn(); | ||||
|     }, [mermaidToExcalidrawLib.api]); | ||||
|  | ||||
|     return ( | ||||
|       <Dialog | ||||
|         className="ttd-dialog" | ||||
|         onCloseRequest={() => { | ||||
|           app.setOpenDialog(null); | ||||
|         }} | ||||
|         size={1200} | ||||
|         title={false} | ||||
|         {...rest} | ||||
|         autofocus={false} | ||||
|       > | ||||
|         <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"> | ||||
|                 <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="text-to-drawing"> | ||||
|                 <div style={{ display: "flex", alignItems: "center" }}> | ||||
|                   {t("labels.textToDrawing")} | ||||
|                   <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> | ||||
|           )} | ||||
|  | ||||
|           <TTDDialogTab className="ttd-dialog-content" tab="mermaid"> | ||||
|             <MermaidToExcalidraw | ||||
|               mermaidToExcalidrawLib={mermaidToExcalidrawLib} | ||||
|             /> | ||||
|           </TTDDialogTab> | ||||
|           {!("__fallback" in rest) && ( | ||||
|             <TTDDialogTab className="ttd-dialog-content" tab="text-to-diagram"> | ||||
|               <TextToDiagram | ||||
|                 onTextSubmit={rest.onTextSubmit} | ||||
|                 mermaidToExcalidrawLib={mermaidToExcalidrawLib} | ||||
|               /> | ||||
|             </TTDDialogTab> | ||||
|           )} | ||||
|           {!("__fallback" in rest) && ( | ||||
|             <TTDDialogTab className="ttd-dialog-content" tab="text-to-drawing"> | ||||
|               <TextToDrawing onTextSubmit={rest.onTextSubmit} /> | ||||
|             </TTDDialogTab> | ||||
|           )} | ||||
|         </TTDDialogTabs> | ||||
|       </Dialog> | ||||
|     ); | ||||
|   }, | ||||
| ); | ||||
| @@ -1,52 +0,0 @@ | ||||
| import { ChangeEventHandler, useEffect, useRef } from "react"; | ||||
| import { EVENT } from "../../constants"; | ||||
| import { KEYS } from "../../keys"; | ||||
|  | ||||
| interface TTDDialogInputProps { | ||||
|   input: string; | ||||
|   placeholder: string; | ||||
|   onChange: ChangeEventHandler<HTMLTextAreaElement>; | ||||
|   onKeyboardSubmit?: () => void; | ||||
| } | ||||
|  | ||||
| export const TTDDialogInput = ({ | ||||
|   input, | ||||
|   placeholder, | ||||
|   onChange, | ||||
|   onKeyboardSubmit, | ||||
| }: TTDDialogInputProps) => { | ||||
|   const ref = useRef<HTMLTextAreaElement>(null); | ||||
|  | ||||
|   const callbackRef = useRef(onKeyboardSubmit); | ||||
|   callbackRef.current = onKeyboardSubmit; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!callbackRef.current) { | ||||
|       return; | ||||
|     } | ||||
|     const textarea = ref.current; | ||||
|     if (textarea) { | ||||
|       const handleKeyDown = (event: KeyboardEvent) => { | ||||
|         if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.ENTER) { | ||||
|           event.preventDefault(); | ||||
|           callbackRef.current?.(); | ||||
|         } | ||||
|       }; | ||||
|       textarea.addEventListener(EVENT.KEYDOWN, handleKeyDown); | ||||
|       return () => { | ||||
|         textarea.removeEventListener(EVENT.KEYDOWN, handleKeyDown); | ||||
|       }; | ||||
|     } | ||||
|   }, []); | ||||
|  | ||||
|   return ( | ||||
|     <textarea | ||||
|       className="ttd-dialog-input" | ||||
|       onChange={onChange} | ||||
|       value={input} | ||||
|       placeholder={placeholder} | ||||
|       autoFocus | ||||
|       ref={ref} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,39 +0,0 @@ | ||||
| import Spinner from "../Spinner"; | ||||
|  | ||||
| const ErrorComp = ({ error }: { error: string }) => { | ||||
|   return ( | ||||
|     <div | ||||
|       data-testid="ttd-dialog-output-error" | ||||
|       className="ttd-dialog-output-error" | ||||
|     > | ||||
|       Error! <p>{error}</p> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| interface TTDDialogOutputProps { | ||||
|   error: Error | null; | ||||
|   canvasRef: React.RefObject<HTMLDivElement>; | ||||
|   loaded: boolean; | ||||
| } | ||||
|  | ||||
| export const TTDDialogOutput = ({ | ||||
|   error, | ||||
|   canvasRef, | ||||
|   loaded, | ||||
| }: TTDDialogOutputProps) => { | ||||
|   return ( | ||||
|     <div className="ttd-dialog-output-wrapper"> | ||||
|       {error && <ErrorComp error={error.message} />} | ||||
|       {loaded ? ( | ||||
|         <div | ||||
|           ref={canvasRef} | ||||
|           style={{ opacity: error ? "0.15" : 1 }} | ||||
|           className="ttd-dialog-output-canvas-container" | ||||
|         /> | ||||
|       ) : ( | ||||
|         <Spinner size="2rem" /> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,58 +0,0 @@ | ||||
| import { ReactNode } from "react"; | ||||
| import { Button } from "../Button"; | ||||
| import clsx from "clsx"; | ||||
| import Spinner from "../Spinner"; | ||||
|  | ||||
| interface TTDDialogPanelProps { | ||||
|   label: string; | ||||
|   children: ReactNode; | ||||
|   panelAction?: { | ||||
|     label: string; | ||||
|     action: () => void; | ||||
|     icon?: ReactNode; | ||||
|   }; | ||||
|   panelActionDisabled?: boolean; | ||||
|   onTextSubmitInProgess?: boolean; | ||||
|   renderTopRight?: () => ReactNode; | ||||
|   renderBottomRight?: () => ReactNode; | ||||
| } | ||||
|  | ||||
| export const TTDDialogPanel = ({ | ||||
|   label, | ||||
|   children, | ||||
|   panelAction, | ||||
|   panelActionDisabled = false, | ||||
|   onTextSubmitInProgess, | ||||
|   renderTopRight, | ||||
|   renderBottomRight, | ||||
| }: TTDDialogPanelProps) => { | ||||
|   return ( | ||||
|     <div className="ttd-dialog-panel"> | ||||
|       <div className="ttd-dialog-panel__header"> | ||||
|         <label>{label}</label> | ||||
|         {renderTopRight?.()} | ||||
|       </div> | ||||
|  | ||||
|       {children} | ||||
|       <div | ||||
|         className={clsx("ttd-dialog-panel-button-container", { | ||||
|           invisible: !panelAction, | ||||
|         })} | ||||
|         style={{ display: "flex", alignItems: "center" }} | ||||
|       > | ||||
|         <Button | ||||
|           className="ttd-dialog-panel-button" | ||||
|           onSelect={panelAction ? panelAction.action : () => {}} | ||||
|           disabled={panelActionDisabled || onTextSubmitInProgess} | ||||
|         > | ||||
|           <div className={clsx({ invisible: onTextSubmitInProgess })}> | ||||
|             {panelAction?.label} | ||||
|             {panelAction?.icon && <span>{panelAction.icon}</span>} | ||||
|           </div> | ||||
|           {onTextSubmitInProgess && <Spinner />} | ||||
|         </Button> | ||||
|         {renderBottomRight?.()} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,5 +0,0 @@ | ||||
| import { ReactNode } from "react"; | ||||
|  | ||||
| export const TTDDialogPanels = ({ children }: { children: ReactNode }) => { | ||||
|   return <div className="ttd-dialog-panels">{children}</div>; | ||||
| }; | ||||
| @@ -1,17 +0,0 @@ | ||||
| import * as RadixTabs from "@radix-ui/react-tabs"; | ||||
|  | ||||
| export const TTDDialogTab = ({ | ||||
|   tab, | ||||
|   children, | ||||
|   ...rest | ||||
| }: { | ||||
|   tab: string; | ||||
|   children: React.ReactNode; | ||||
| } & React.HTMLAttributes<HTMLDivElement>) => { | ||||
|   return ( | ||||
|     <RadixTabs.Content {...rest} value={tab}> | ||||
|       {children} | ||||
|     </RadixTabs.Content> | ||||
|   ); | ||||
| }; | ||||
| TTDDialogTab.displayName = "TTDDialogTab"; | ||||
| @@ -1,21 +0,0 @@ | ||||
| import * as RadixTabs from "@radix-ui/react-tabs"; | ||||
|  | ||||
| export const TTDDialogTabTrigger = ({ | ||||
|   children, | ||||
|   tab, | ||||
|   onSelect, | ||||
|   ...rest | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
|   tab: string; | ||||
|   onSelect?: React.ReactEventHandler<HTMLButtonElement> | undefined; | ||||
| } & Omit<React.HTMLAttributes<HTMLButtonElement>, "onSelect">) => { | ||||
|   return ( | ||||
|     <RadixTabs.Trigger value={tab} asChild onSelect={onSelect}> | ||||
|       <button type="button" className="ttd-dialog-tab-trigger" {...rest}> | ||||
|         {children} | ||||
|       </button> | ||||
|     </RadixTabs.Trigger> | ||||
|   ); | ||||
| }; | ||||
| TTDDialogTabTrigger.displayName = "TTDDialogTabTrigger"; | ||||
| @@ -1,13 +0,0 @@ | ||||
| import * as RadixTabs from "@radix-ui/react-tabs"; | ||||
|  | ||||
| export const TTDDialogTabTriggers = ({ | ||||
|   children, | ||||
|   ...rest | ||||
| }: { children: React.ReactNode } & React.HTMLAttributes<HTMLDivElement>) => { | ||||
|   return ( | ||||
|     <RadixTabs.List className="ttd-dialog-triggers" {...rest}> | ||||
|       {children} | ||||
|     </RadixTabs.List> | ||||
|   ); | ||||
| }; | ||||
| TTDDialogTabTriggers.displayName = "TTDDialogTabTriggers"; | ||||
| @@ -1,67 +0,0 @@ | ||||
| import * as RadixTabs from "@radix-ui/react-tabs"; | ||||
| import { ReactNode, useRef } from "react"; | ||||
| import { useExcalidrawSetAppState } from "../App"; | ||||
| import { isMemberOf } from "../../utils"; | ||||
|  | ||||
| const TTDDialogTabs = ( | ||||
|   props: { | ||||
|     children: ReactNode; | ||||
|   } & ( | ||||
|     | { dialog: "ttd"; tab: "text-to-diagram" | "mermaid" | "text-to-drawing" } | ||||
|     | { | ||||
|         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={props.tab} | ||||
|       onValueChange={( | ||||
|         // at least in test enviros, `tab` can be `undefined` | ||||
|         tab: string | undefined, | ||||
|       ) => { | ||||
|         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", "text-to-drawing"], tab) | ||||
|         ) { | ||||
|           setAppState({ | ||||
|             openDialog: { name: props.dialog, tab }, | ||||
|           }); | ||||
|         } | ||||
|       }} | ||||
|     > | ||||
|       {props.children} | ||||
|     </RadixTabs.Root> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| TTDDialogTabs.displayName = "TTDDialogTabs"; | ||||
|  | ||||
| export default TTDDialogTabs; | ||||
| @@ -1,38 +0,0 @@ | ||||
| import { ReactNode } from "react"; | ||||
| import { useTunnels } from "../../context/tunnels"; | ||||
| import DropdownMenu from "../dropdownMenu/DropdownMenu"; | ||||
| import { useExcalidrawSetAppState } from "../App"; | ||||
| import { brainIcon } from "../icons"; | ||||
| import { t } from "../../i18n"; | ||||
| import { trackEvent } from "../../analytics"; | ||||
|  | ||||
| export const TTDDialogTrigger = ({ | ||||
|   children, | ||||
|   icon, | ||||
|   tab, | ||||
| }: { | ||||
|   children?: ReactNode; | ||||
|   icon?: JSX.Element; | ||||
|   tab?: "mermaid" | "text-to-diagram" | "text-to-drawing"; | ||||
| }) => { | ||||
|   const { TTDDialogTriggerTunnel } = useTunnels(); | ||||
|   const setAppState = useExcalidrawSetAppState(); | ||||
|  | ||||
|   return ( | ||||
|     <TTDDialogTriggerTunnel.In> | ||||
|       <DropdownMenu.Item | ||||
|         onSelect={() => { | ||||
|           trackEvent("ai", "dialog open", "ttd"); | ||||
|           setAppState({ | ||||
|             openDialog: { name: "ttd", tab: tab ?? "text-to-diagram" }, | ||||
|           }); | ||||
|         }} | ||||
|         icon={icon ?? brainIcon} | ||||
|       > | ||||
|         {children ?? t("labels.textToDiagram")} | ||||
|         <DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge> | ||||
|       </DropdownMenu.Item> | ||||
|     </TTDDialogTriggerTunnel.In> | ||||
|   ); | ||||
| }; | ||||
| TTDDialogTrigger.displayName = "TTDDialogTrigger"; | ||||
| @@ -1,228 +0,0 @@ | ||||
| import { useAtom } from "jotai"; | ||||
| import { useRef, useState, ChangeEventHandler } from "react"; | ||||
| import { trackEvent } from "../../analytics"; | ||||
| import { t } from "../../i18n"; | ||||
| import { isFiniteNumber } from "../../utils"; | ||||
| import { ArrowRightIcon } from "../icons"; | ||||
| import { TTDDialogInput } from "./TTDDialogInput"; | ||||
| import { TTDDialogOutput } from "./TTDDialogOutput"; | ||||
| import { TTDDialogPanel } from "./TTDDialogPanel"; | ||||
| import { TTDDialogPanels } from "./TTDDialogPanels"; | ||||
| import { | ||||
|   CommonDialogProps, | ||||
|   MAX_PROMPT_LENGTH, | ||||
|   MIN_PROMPT_LENGTH, | ||||
|   MermaidToExcalidrawLibProps, | ||||
|   convertMermaidToExcalidraw, | ||||
|   insertToEditor, | ||||
|   rateLimitsAtom, | ||||
|   saveMermaidDataToStorage, | ||||
| } from "./common"; | ||||
| import { useApp } from "../App"; | ||||
| import { NonDeletedExcalidrawElement } from "../../element/types"; | ||||
| import { BinaryFiles } from "../../types"; | ||||
|  | ||||
| export type TextToDiagramProps = CommonDialogProps & { | ||||
|   mermaidToExcalidrawLib: MermaidToExcalidrawLibProps; | ||||
| }; | ||||
|  | ||||
| export const TextToDiagram = ({ | ||||
|   onTextSubmit, | ||||
|   mermaidToExcalidrawLib, | ||||
| }: TextToDiagramProps) => { | ||||
|   const app = useApp(); | ||||
|  | ||||
|   const someRandomDivRef = useRef<HTMLDivElement>(null); | ||||
|  | ||||
|   const [text, setText] = useState(""); | ||||
|  | ||||
|   const prompt = text.trim(); | ||||
|  | ||||
|   const handleTextChange: ChangeEventHandler<HTMLTextAreaElement> = (event) => { | ||||
|     setText(event.target.value); | ||||
|   }; | ||||
|  | ||||
|   const [onTextSubmitInProgess, setOnTextSubmitInProgess] = useState(false); | ||||
|   const [rateLimits, setRateLimits] = useAtom(rateLimitsAtom); | ||||
|  | ||||
|   const data = useRef<{ | ||||
|     elements: readonly NonDeletedExcalidrawElement[]; | ||||
|     files: BinaryFiles | null; | ||||
|   }>({ elements: [], files: null }); | ||||
|  | ||||
|   const [error, setError] = useState<Error | null>(null); | ||||
|  | ||||
|   const onGenerate = async () => { | ||||
|     if ( | ||||
|       prompt.length > MAX_PROMPT_LENGTH || | ||||
|       prompt.length < MIN_PROMPT_LENGTH || | ||||
|       onTextSubmitInProgess || | ||||
|       rateLimits?.rateLimitRemaining === 0 | ||||
|     ) { | ||||
|       if (prompt.length < MIN_PROMPT_LENGTH) { | ||||
|         setError( | ||||
|           new Error( | ||||
|             `Prompt is too short (min ${MIN_PROMPT_LENGTH} characters)`, | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|       if (prompt.length > MAX_PROMPT_LENGTH) { | ||||
|         setError( | ||||
|           new Error(`Prompt is too long (max ${MAX_PROMPT_LENGTH} characters)`), | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       setOnTextSubmitInProgess(true); | ||||
|  | ||||
|       trackEvent("ai", "generate", "ttd"); | ||||
|  | ||||
|       const { generatedResponse, error, rateLimit, rateLimitRemaining } = | ||||
|         await onTextSubmit(prompt, "text-to-diagram"); | ||||
|  | ||||
|       if (isFiniteNumber(rateLimit) && isFiniteNumber(rateLimitRemaining)) { | ||||
|         setRateLimits({ rateLimit, rateLimitRemaining }); | ||||
|       } | ||||
|  | ||||
|       if (error) { | ||||
|         setError(error); | ||||
|         return; | ||||
|       } | ||||
|       if (!generatedResponse) { | ||||
|         setError(new Error("Generation failed")); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       try { | ||||
|         await convertMermaidToExcalidraw({ | ||||
|           canvasRef: someRandomDivRef, | ||||
|           data, | ||||
|           mermaidToExcalidrawLib, | ||||
|           setError, | ||||
|           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( | ||||
|             "Generated an invalid diagram :(. You may also try a different prompt.", | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|     } catch (error: any) { | ||||
|       let message: string | undefined = error.message; | ||||
|       if (!message || message === "Failed to fetch") { | ||||
|         message = "Request failed"; | ||||
|       } | ||||
|       setError(new Error(message)); | ||||
|     } finally { | ||||
|       setOnTextSubmitInProgess(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const refOnGenerate = useRef(onGenerate); | ||||
|   refOnGenerate.current = onGenerate; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="ttd-dialog-desc"> | ||||
|         Currently we use Mermaid as a middle step, so you'll get best results if | ||||
|         you describe a diagram, workflow, flow chart, and similar. | ||||
|       </div> | ||||
|       <TTDDialogPanels> | ||||
|         <TTDDialogPanel | ||||
|           label={t("labels.prompt")} | ||||
|           panelAction={{ | ||||
|             action: onGenerate, | ||||
|             label: "Generate", | ||||
|             icon: ArrowRightIcon, | ||||
|           }} | ||||
|           onTextSubmitInProgess={onTextSubmitInProgess} | ||||
|           panelActionDisabled={ | ||||
|             prompt.length > MAX_PROMPT_LENGTH || | ||||
|             rateLimits?.rateLimitRemaining === 0 | ||||
|           } | ||||
|           renderTopRight={() => { | ||||
|             if (!rateLimits) { | ||||
|               return null; | ||||
|             } | ||||
|  | ||||
|             return ( | ||||
|               <div | ||||
|                 className="ttd-dialog-rate-limit" | ||||
|                 style={{ | ||||
|                   fontSize: 12, | ||||
|                   marginLeft: "auto", | ||||
|                   color: | ||||
|                     rateLimits.rateLimitRemaining === 0 | ||||
|                       ? "var(--color-danger)" | ||||
|                       : undefined, | ||||
|                 }} | ||||
|               > | ||||
|                 {rateLimits.rateLimitRemaining} requests left today | ||||
|               </div> | ||||
|             ); | ||||
|           }} | ||||
|           renderBottomRight={() => { | ||||
|             const ratio = prompt.length / MAX_PROMPT_LENGTH; | ||||
|             if (ratio > 0.8) { | ||||
|               return ( | ||||
|                 <div | ||||
|                   style={{ | ||||
|                     marginLeft: "auto", | ||||
|                     fontSize: 12, | ||||
|                     fontFamily: "monospace", | ||||
|                     color: ratio > 1 ? "var(--color-danger)" : undefined, | ||||
|                   }} | ||||
|                 > | ||||
|                   Length: {prompt.length}/{MAX_PROMPT_LENGTH} | ||||
|                 </div> | ||||
|               ); | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|           }} | ||||
|         > | ||||
|           <TTDDialogInput | ||||
|             onChange={handleTextChange} | ||||
|             input={text} | ||||
|             placeholder={"Describe what you want to see..."} | ||||
|             onKeyboardSubmit={() => { | ||||
|               refOnGenerate.current(); | ||||
|             }} | ||||
|           /> | ||||
|         </TTDDialogPanel> | ||||
|         <TTDDialogPanel | ||||
|           label="Preview" | ||||
|           panelAction={{ | ||||
|             action: () => { | ||||
|               console.info("Panel action clicked"); | ||||
|               insertToEditor({ app, data: data.current }); | ||||
|             }, | ||||
|             label: "Insert", | ||||
|             icon: ArrowRightIcon, | ||||
|           }} | ||||
|         > | ||||
|           <TTDDialogOutput | ||||
|             canvasRef={someRandomDivRef} | ||||
|             error={error} | ||||
|             loaded={mermaidToExcalidrawLib.loaded} | ||||
|           /> | ||||
|         </TTDDialogPanel> | ||||
|       </TTDDialogPanels> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,248 +0,0 @@ | ||||
| import { useAtom } from "jotai"; | ||||
| import { useRef, useState, ChangeEventHandler } from "react"; | ||||
| import { trackEvent } from "../../analytics"; | ||||
| import { NonDeletedExcalidrawElement } from "../../element/types"; | ||||
| import { t } from "../../i18n"; | ||||
| import { isFiniteNumber } from "../../utils"; | ||||
| import { useApp } from "../App"; | ||||
| import { ArrowRightIcon } from "../icons"; | ||||
| import { TTDDialogInput } from "./TTDDialogInput"; | ||||
| import { TTDDialogOutput } from "./TTDDialogOutput"; | ||||
| import { TTDDialogPanel } from "./TTDDialogPanel"; | ||||
| import { TTDDialogPanels } from "./TTDDialogPanels"; | ||||
| import { | ||||
|   CommonDialogProps, | ||||
|   MAX_PROMPT_LENGTH, | ||||
|   MIN_PROMPT_LENGTH, | ||||
|   insertToEditor, | ||||
|   rateLimitsAtom, | ||||
|   resetPreview, | ||||
| } from "./common"; | ||||
| import { | ||||
|   convertToExcalidrawElements, | ||||
|   exportToCanvas, | ||||
| } from "../../packages/excalidraw/index"; | ||||
| import { DEFAULT_EXPORT_PADDING } from "../../constants"; | ||||
| import { canvasToBlob } from "../../data/blob"; | ||||
|  | ||||
| export type TextToDrawingProps = CommonDialogProps; | ||||
|  | ||||
| export const TextToDrawing = ({ onTextSubmit }: TextToDrawingProps) => { | ||||
|   const app = useApp(); | ||||
|   const containerRef = useRef<HTMLDivElement>(null); | ||||
|  | ||||
|   const [text, setText] = useState(""); | ||||
|  | ||||
|   const prompt = text.trim(); | ||||
|  | ||||
|   const handleTextChange: ChangeEventHandler<HTMLTextAreaElement> = (event) => { | ||||
|     setText(event.target.value); | ||||
|   }; | ||||
|  | ||||
|   const [onTextSubmitInProgess, setOnTextSubmitInProgess] = useState(false); | ||||
|   const [rateLimits, setRateLimits] = useAtom(rateLimitsAtom); | ||||
|  | ||||
|   const [data, setData] = useState< | ||||
|     readonly NonDeletedExcalidrawElement[] | null | ||||
|   >(null); | ||||
|  | ||||
|   const [error, setError] = useState<Error | null>(null); | ||||
|  | ||||
|   const onGenerate = async () => { | ||||
|     if ( | ||||
|       prompt.length > MAX_PROMPT_LENGTH || | ||||
|       prompt.length < MIN_PROMPT_LENGTH || | ||||
|       onTextSubmitInProgess || | ||||
|       rateLimits?.rateLimitRemaining === 0 | ||||
|     ) { | ||||
|       if (prompt.length < MIN_PROMPT_LENGTH) { | ||||
|         setError( | ||||
|           new Error( | ||||
|             `Prompt is too short (min ${MIN_PROMPT_LENGTH} characters)`, | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|       if (prompt.length > MAX_PROMPT_LENGTH) { | ||||
|         setError( | ||||
|           new Error(`Prompt is too long (max ${MAX_PROMPT_LENGTH} characters)`), | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       setOnTextSubmitInProgess(true); | ||||
|  | ||||
|       trackEvent("ai", "generate", "text-to-drawing"); | ||||
|  | ||||
|       const { generatedResponse, error, rateLimit, rateLimitRemaining } = | ||||
|         await onTextSubmit(prompt, "text-to-drawing"); | ||||
|  | ||||
|       if (isFiniteNumber(rateLimit) && isFiniteNumber(rateLimitRemaining)) { | ||||
|         setRateLimits({ rateLimit, rateLimitRemaining }); | ||||
|       } | ||||
|  | ||||
|       if (error) { | ||||
|         setError(error); | ||||
|         return; | ||||
|       } | ||||
|       if (!generatedResponse) { | ||||
|         setError(new Error("Generation failed")); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const canvasNode = containerRef.current; | ||||
|       const parent = canvasNode?.parentElement; | ||||
|  | ||||
|       if (!canvasNode || !parent) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!text) { | ||||
|         resetPreview({ canvasRef: containerRef, setError }); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (!Array.isArray(generatedResponse)) { | ||||
|         setError(new Error("Generation failed to return an array!")); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       try { | ||||
|         const elements = convertToExcalidrawElements(generatedResponse, { | ||||
|           regenerateIds: true, | ||||
|         }); | ||||
|  | ||||
|         setData(elements); | ||||
|  | ||||
|         const canvas = await exportToCanvas({ | ||||
|           elements, | ||||
|           files: null, | ||||
|           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 (err: any) { | ||||
|         console.error(err); | ||||
|         parent.style.background = "var(--default-bg-color)"; | ||||
|         if (text) { | ||||
|           setError(err); | ||||
|         } | ||||
|  | ||||
|         throw err; | ||||
|       } | ||||
|     } catch (error: any) { | ||||
|       let message: string | undefined = error.message; | ||||
|       if (!message || message === "Failed to fetch") { | ||||
|         message = "Request failed"; | ||||
|       } | ||||
|       setError(new Error(message)); | ||||
|     } finally { | ||||
|       setOnTextSubmitInProgess(false); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const refOnGenerate = useRef(onGenerate); | ||||
|   refOnGenerate.current = onGenerate; | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="ttd-dialog-desc">This is text to drawing.</div> | ||||
|       <TTDDialogPanels> | ||||
|         <TTDDialogPanel | ||||
|           label={t("labels.prompt")} | ||||
|           panelAction={{ | ||||
|             action: onGenerate, | ||||
|             label: "Generate", | ||||
|             icon: ArrowRightIcon, | ||||
|           }} | ||||
|           onTextSubmitInProgess={onTextSubmitInProgess} | ||||
|           panelActionDisabled={ | ||||
|             prompt.length > MAX_PROMPT_LENGTH || | ||||
|             rateLimits?.rateLimitRemaining === 0 | ||||
|           } | ||||
|           renderTopRight={() => { | ||||
|             if (!rateLimits) { | ||||
|               return null; | ||||
|             } | ||||
|  | ||||
|             return ( | ||||
|               <div | ||||
|                 className="ttd-dialog-rate-limit" | ||||
|                 style={{ | ||||
|                   fontSize: 12, | ||||
|                   marginLeft: "auto", | ||||
|                   color: | ||||
|                     rateLimits.rateLimitRemaining === 0 | ||||
|                       ? "var(--color-danger)" | ||||
|                       : undefined, | ||||
|                 }} | ||||
|               > | ||||
|                 {rateLimits.rateLimitRemaining} requests left today | ||||
|               </div> | ||||
|             ); | ||||
|           }} | ||||
|           renderBottomRight={() => { | ||||
|             const ratio = prompt.length / MAX_PROMPT_LENGTH; | ||||
|             if (ratio > 0.8) { | ||||
|               return ( | ||||
|                 <div | ||||
|                   style={{ | ||||
|                     marginLeft: "auto", | ||||
|                     fontSize: 12, | ||||
|                     fontFamily: "monospace", | ||||
|                     color: ratio > 1 ? "var(--color-danger)" : undefined, | ||||
|                   }} | ||||
|                 > | ||||
|                   Length: {prompt.length}/{MAX_PROMPT_LENGTH} | ||||
|                 </div> | ||||
|               ); | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|           }} | ||||
|         > | ||||
|           <TTDDialogInput | ||||
|             onChange={handleTextChange} | ||||
|             input={text} | ||||
|             placeholder={"Describe what you want to see..."} | ||||
|             onKeyboardSubmit={() => { | ||||
|               refOnGenerate.current(); | ||||
|             }} | ||||
|           /> | ||||
|         </TTDDialogPanel> | ||||
|         <TTDDialogPanel | ||||
|           label="Preview" | ||||
|           panelAction={{ | ||||
|             action: () => { | ||||
|               if (data) { | ||||
|                 insertToEditor({ | ||||
|                   app, | ||||
|                   data: { | ||||
|                     elements: data, | ||||
|                     files: null, | ||||
|                   }, | ||||
|                 }); | ||||
|               } | ||||
|             }, | ||||
|             label: "Insert", | ||||
|             icon: ArrowRightIcon, | ||||
|           }} | ||||
|         > | ||||
|           <TTDDialogOutput | ||||
|             canvasRef={containerRef} | ||||
|             error={error} | ||||
|             loaded={true} | ||||
|           /> | ||||
|         </TTDDialogPanel> | ||||
|       </TTDDialogPanels> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,193 +0,0 @@ | ||||
| import { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw"; | ||||
| import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces"; | ||||
| import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../../constants"; | ||||
| import { | ||||
|   convertToExcalidrawElements, | ||||
|   exportToCanvas, | ||||
| } from "../../packages/excalidraw/index"; | ||||
| import { NonDeletedExcalidrawElement } from "../../element/types"; | ||||
| import { AppClassProperties, BinaryFiles } from "../../types"; | ||||
| import { canvasToBlob } from "../../data/blob"; | ||||
| import { atom } from "jotai"; | ||||
|  | ||||
| export const resetPreview = ({ | ||||
|   canvasRef, | ||||
|   setError, | ||||
| }: { | ||||
|   canvasRef: React.RefObject<HTMLDivElement>; | ||||
|   setError: (error: Error | null) => void; | ||||
| }) => { | ||||
|   const canvasNode = canvasRef.current; | ||||
|  | ||||
|   if (!canvasNode) { | ||||
|     return; | ||||
|   } | ||||
|   const parent = canvasNode.parentElement; | ||||
|   if (!parent) { | ||||
|     return; | ||||
|   } | ||||
|   parent.style.background = ""; | ||||
|   setError(null); | ||||
|   canvasNode.replaceChildren(); | ||||
| }; | ||||
|  | ||||
| export type OnTestSubmitRetValue = { | ||||
|   rateLimit?: number | null; | ||||
|   rateLimitRemaining?: number | null; | ||||
| } & ( | ||||
|   | { | ||||
|       generatedResponse: any | string | undefined; | ||||
|       error?: null | undefined; | ||||
|     } | ||||
|   | { | ||||
|       error: Error; | ||||
|       generatedResponse?: null | undefined; | ||||
|     } | ||||
| ); | ||||
| export interface CommonDialogProps { | ||||
|   onTextSubmit( | ||||
|     value: string, | ||||
|     type: "text-to-diagram" | "text-to-drawing", | ||||
|   ): Promise<OnTestSubmitRetValue>; | ||||
| } | ||||
|  | ||||
| export interface MermaidToExcalidrawLibProps { | ||||
|   loaded: boolean; | ||||
|   api: Promise<{ | ||||
|     parseMermaidToExcalidraw: ( | ||||
|       definition: string, | ||||
|       options: MermaidOptions, | ||||
|     ) => Promise<MermaidToExcalidrawResult>; | ||||
|   }>; | ||||
| } | ||||
|  | ||||
| interface ConvertMermaidToExcalidrawFormatProps { | ||||
|   canvasRef: React.RefObject<HTMLDivElement>; | ||||
|   mermaidToExcalidrawLib: MermaidToExcalidrawLibProps; | ||||
|   mermaidDefinition: string; | ||||
|   setError: (error: Error | null) => void; | ||||
|   data: React.MutableRefObject<{ | ||||
|     elements: readonly NonDeletedExcalidrawElement[]; | ||||
|     files: BinaryFiles | null; | ||||
|   }>; | ||||
| } | ||||
|  | ||||
| export const convertMermaidToExcalidraw = async ({ | ||||
|   canvasRef, | ||||
|   mermaidToExcalidrawLib, | ||||
|   mermaidDefinition, | ||||
|   setError, | ||||
|   data, | ||||
| }: ConvertMermaidToExcalidrawFormatProps) => { | ||||
|   const canvasNode = canvasRef.current; | ||||
|   const parent = canvasNode?.parentElement; | ||||
|  | ||||
|   if (!canvasNode || !parent) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (!mermaidDefinition) { | ||||
|     resetPreview({ canvasRef, setError }); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const api = await mermaidToExcalidrawLib.api; | ||||
|  | ||||
|     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 = { | ||||
|       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 (err: any) { | ||||
|     console.error(err); | ||||
|     parent.style.background = "var(--default-bg-color)"; | ||||
|     if (mermaidDefinition) { | ||||
|       setError(err); | ||||
|     } | ||||
|  | ||||
|     throw err; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw"; | ||||
| export 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); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const insertToEditor = ({ | ||||
|   app, | ||||
|   data, | ||||
|   text, | ||||
|   shouldSaveMermaidDataToStorage, | ||||
| }: { | ||||
|   app: AppClassProperties; | ||||
|   data: { | ||||
|     elements: readonly NonDeletedExcalidrawElement[]; | ||||
|     files: BinaryFiles | null; | ||||
|   }; | ||||
|   text?: string; | ||||
|   shouldSaveMermaidDataToStorage?: boolean; | ||||
| }) => { | ||||
|   const { elements: newElements, files } = data; | ||||
|  | ||||
|   if (!newElements.length) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   app.addElementsFromPasteOrLibrary({ | ||||
|     elements: newElements, | ||||
|     files, | ||||
|     position: "center", | ||||
|     fitToContent: true, | ||||
|   }); | ||||
|   app.setOpenDialog(null); | ||||
|  | ||||
|   if (shouldSaveMermaidDataToStorage && text) { | ||||
|     saveMermaidDataToStorage(text); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const MIN_PROMPT_LENGTH = 3; | ||||
| export const MAX_PROMPT_LENGTH = 1000; | ||||
|  | ||||
| export const rateLimitsAtom = atom<{ | ||||
|   rateLimit: number; | ||||
|   rateLimitRemaining: number; | ||||
| } | null>(null); | ||||
| @@ -63,13 +63,9 @@ | ||||
|       } | ||||
|  | ||||
|       &__text { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         width: 100%; | ||||
|         text-overflow: ellipsis; | ||||
|         overflow: hidden; | ||||
|         white-space: nowrap; | ||||
|         gap: 0.75rem; | ||||
|       } | ||||
|  | ||||
|       &__shortcut { | ||||
|   | ||||
| @@ -37,32 +37,6 @@ const DropdownMenuItem = ({ | ||||
|     </button> | ||||
|   ); | ||||
| }; | ||||
| DropdownMenuItem.displayName = "DropdownMenuItem"; | ||||
|  | ||||
| export const DropDownMenuItemBadge = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode; | ||||
| }) => { | ||||
|   return ( | ||||
|     <div | ||||
|       style={{ | ||||
|         display: "inline-flex", | ||||
|         marginLeft: "auto", | ||||
|         padding: "1px 4px", | ||||
|         background: "pink", | ||||
|         borderRadius: 6, | ||||
|         fontSize: 11, | ||||
|         color: "black", | ||||
|         fontFamily: "monospace", | ||||
|       }} | ||||
|     > | ||||
|       {children} | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| DropDownMenuItemBadge.displayName = "DropdownMenuItemBadge"; | ||||
|  | ||||
| DropdownMenuItem.Badge = DropDownMenuItemBadge; | ||||
|  | ||||
| export default DropdownMenuItem; | ||||
| DropdownMenuItem.displayName = "DropdownMenuItem"; | ||||
|   | ||||
| @@ -1742,26 +1742,3 @@ export const eyeClosedIcon = createIcon( | ||||
|   </g>, | ||||
|   tablerIconProps, | ||||
| ); | ||||
|  | ||||
| export const brainIcon = createIcon( | ||||
|   <g stroke="currentColor" fill="none"> | ||||
|     <path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
|     <path d="M15.5 13a3.5 3.5 0 0 0 -3.5 3.5v1a3.5 3.5 0 0 0 7 0v-1.8" /> | ||||
|     <path d="M8.5 13a3.5 3.5 0 0 1 3.5 3.5v1a3.5 3.5 0 0 1 -7 0v-1.8" /> | ||||
|     <path d="M17.5 16a3.5 3.5 0 0 0 0 -7h-.5" /> | ||||
|     <path d="M19 9.3v-2.8a3.5 3.5 0 0 0 -7 0" /> | ||||
|     <path d="M6.5 16a3.5 3.5 0 0 1 0 -7h.5" /> | ||||
|     <path d="M5 9.3v-2.8a3.5 3.5 0 0 1 7 0v10" /> | ||||
|   </g>, | ||||
|   tablerIconProps, | ||||
| ); | ||||
|  | ||||
| export const drawingIcon = createIcon( | ||||
|   <g stroke="currentColor" fill="none"> | ||||
|     <path stroke="none" d="M0 0h24v24H0z" fill="none" /> | ||||
|     <path d="M20 17v-12c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v12l2 2l2 -2z" /> | ||||
|     <path d="M16 7h4" /> | ||||
|     <path d="M18 19h-13a2 2 0 1 1 0 -4h4a2 2 0 1 0 0 -4h-3" /> | ||||
|   </g>, | ||||
|   tablerIconProps, | ||||
| ); | ||||
|   | ||||
| @@ -13,7 +13,6 @@ type TunnelsContextValue = { | ||||
|   DefaultSidebarTriggerTunnel: Tunnel; | ||||
|   DefaultSidebarTabTriggersTunnel: Tunnel; | ||||
|   OverwriteConfirmDialogTunnel: Tunnel; | ||||
|   TTDDialogTriggerTunnel: Tunnel; | ||||
|   jotaiScope: symbol; | ||||
| }; | ||||
|  | ||||
| @@ -33,7 +32,6 @@ export const useInitializeTunnels = () => { | ||||
|       DefaultSidebarTriggerTunnel: tunnel(), | ||||
|       DefaultSidebarTabTriggersTunnel: tunnel(), | ||||
|       OverwriteConfirmDialogTunnel: tunnel(), | ||||
|       TTDDialogTriggerTunnel: tunnel(), | ||||
|       jotaiScope: Symbol(), | ||||
|     }; | ||||
|   }, []); | ||||
|   | ||||
| @@ -39,7 +39,6 @@ | ||||
|  | ||||
|   button { | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
|   } | ||||
|  | ||||
|   &:focus { | ||||
| @@ -652,19 +651,6 @@ | ||||
|       --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 { | ||||
| @@ -748,4 +734,8 @@ | ||||
|     letter-spacing: 0.6px; | ||||
|     font-family: "Assistant"; | ||||
|   } | ||||
|  | ||||
|   .excalidraw__paragraph { | ||||
|     margin: 1rem 0; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -294,11 +294,15 @@ const restoreElement = ( | ||||
|     case "ellipse": | ||||
|     case "rectangle": | ||||
|     case "diamond": | ||||
|     case "iframe": | ||||
|       return restoreElementWithProperties(element, {}); | ||||
|     case "iframe": | ||||
|       return restoreElementWithProperties(element, { | ||||
|         scale: element.scale ?? [1, 1], | ||||
|       }); | ||||
|     case "embeddable": | ||||
|       return restoreElementWithProperties(element, { | ||||
|         validated: null, | ||||
|         scale: element.scale ?? [1, 1], | ||||
|       }); | ||||
|     case "magicframe": | ||||
|     case "frame": | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import { Point } from "../types"; | ||||
| import { generateRoughOptions } from "../scene/Shape"; | ||||
| import { | ||||
|   isArrowElement, | ||||
|   isBoundToContainer, | ||||
|   isFreeDrawElement, | ||||
|   isLinearElement, | ||||
|   isTextElement, | ||||
| @@ -55,13 +54,7 @@ export class ElementBounds { | ||||
|   static getBounds(element: ExcalidrawElement) { | ||||
|     const cachedBounds = ElementBounds.boundsCache.get(element); | ||||
|  | ||||
|     if ( | ||||
|       cachedBounds?.version && | ||||
|       cachedBounds.version === element.version && | ||||
|       // we don't invalidate cache when we update containers and not labels, | ||||
|       // which is causing problems down the line. Fix TBA. | ||||
|       !isBoundToContainer(element) | ||||
|     ) { | ||||
|     if (cachedBounds?.version && cachedBounds.version === element.version) { | ||||
|       return cachedBounds.bounds; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,7 @@ import { | ||||
| const embeddedLinkCache = new Map<string, IframeData>(); | ||||
|  | ||||
| const RE_YOUTUBE = | ||||
|   /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/; | ||||
|   /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|.*&t=|\?start=|.*&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/; | ||||
|  | ||||
| const RE_VIMEO = | ||||
|   /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/; | ||||
|   | ||||
| @@ -142,6 +142,7 @@ export const newEmbeddableElement = ( | ||||
|   return { | ||||
|     ..._newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts), | ||||
|     validated: opts.validated, | ||||
|     scale: [1, 1], | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @@ -152,6 +153,7 @@ export const newIframeElement = ( | ||||
| ): NonDeleted<ExcalidrawIframeElement> => { | ||||
|   return { | ||||
|     ..._newElementBase<ExcalidrawIframeElement>("iframe", opts), | ||||
|     scale: [1, 1], | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import { | ||||
| import { | ||||
|   isArrowElement, | ||||
|   isBoundToContainer, | ||||
|   isIframeLikeElement, | ||||
|   isFrameLikeElement, | ||||
|   isFreeDrawElement, | ||||
|   isImageElement, | ||||
| @@ -586,15 +587,31 @@ export const resizeSingleElement = ( | ||||
|   }; | ||||
|  | ||||
|   if ("scale" in element && "scale" in stateAtResizeStart) { | ||||
|     mutateElement(element, { | ||||
|       scale: [ | ||||
|         // defaulting because scaleX/Y can be 0/-0 | ||||
|         (Math.sign(newBoundsX2 - stateAtResizeStart.x) || | ||||
|           stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0], | ||||
|         (Math.sign(newBoundsY2 - stateAtResizeStart.y) || | ||||
|           stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1], | ||||
|       ], | ||||
|     }); | ||||
|     if (isIframeLikeElement(element)) { | ||||
|       if (shouldMaintainAspectRatio) { | ||||
|         const scale: [number, number] = [ | ||||
|           Math.abs( | ||||
|             eleNewWidth / | ||||
|               (stateAtResizeStart.width / stateAtResizeStart.scale[0]), | ||||
|           ), | ||||
|           Math.abs( | ||||
|             eleNewHeight / | ||||
|               (stateAtResizeStart.height / stateAtResizeStart.scale[1]), | ||||
|           ), | ||||
|         ]; | ||||
|         mutateElement(element, { scale }); | ||||
|       } | ||||
|     } else { | ||||
|       mutateElement(element, { | ||||
|         scale: [ | ||||
|           // defaulting because scaleX/Y can be 0/-0 | ||||
|           (Math.sign(newBoundsX2 - stateAtResizeStart.x) || | ||||
|             stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0], | ||||
|           (Math.sign(newBoundsY2 - stateAtResizeStart.y) || | ||||
|             stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1], | ||||
|         ], | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ( | ||||
|   | ||||
| @@ -96,6 +96,7 @@ export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase & | ||||
|      * may not have access to host-app supplied url validator during restore. | ||||
|      */ | ||||
|     validated: boolean | null; | ||||
|     scale: [number, number]; | ||||
|   }>; | ||||
|  | ||||
| export type ExcalidrawIframeElement = _ExcalidrawElementBase & | ||||
| @@ -103,6 +104,7 @@ export type ExcalidrawIframeElement = _ExcalidrawElementBase & | ||||
|     type: "iframe"; | ||||
|     // TODO move later to AI-specific frame | ||||
|     customData?: { generationData?: MagicCacheData }; | ||||
|     scale: [number, number]; | ||||
|   }>; | ||||
|  | ||||
| export type ExcalidrawIframeLikeElement = | ||||
|   | ||||
| @@ -132,10 +132,7 @@ | ||||
|     "sidebarLock": "Keep sidebar open", | ||||
|     "selectAllElementsInFrame": "Select all elements in frame", | ||||
|     "removeAllElementsFromFrame": "Remove all elements from frame", | ||||
|     "eyeDropper": "Pick color from canvas", | ||||
|     "textToDiagram": "Text to diagram", | ||||
|     "prompt": "Prompt", | ||||
|     "textToDrawing": "Text to drawing" | ||||
|     "eyeDropper": "Pick color from canvas" | ||||
|   }, | ||||
|   "library": { | ||||
|     "noItems": "No items added yet...", | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| [ | ||||
|   { | ||||
|     "path": "dist/excalidraw.production.min.js", | ||||
|     "limit": "335 kB" | ||||
|     "limit": "325 kB" | ||||
|   }, | ||||
|   { | ||||
|     "path": "dist/excalidraw-assets/locales", | ||||
|   | ||||
| @@ -17,26 +17,6 @@ Please add the latest change on the top under the correct section. | ||||
|  | ||||
| - `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336) | ||||
|  | ||||
| ## 0.17.1 (2023-11-28) | ||||
|  | ||||
| ### Fixes | ||||
|  | ||||
| - Umd build for browser since it was breaking in v0.17.0 [#7349](https://github.com/excalidraw/excalidraw/pull/7349). Also make sure that when using `Vite`, the `process.env.IS_PREACT` is set as `"true"` (string) and not a boolean. | ||||
|  | ||||
| ``` | ||||
| define: { | ||||
|   "process.env.IS_PREACT": JSON.stringify("true"), | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Excalidraw Library | ||||
|  | ||||
| ### Fixes | ||||
|  | ||||
| - Disable caching bounds for arrow labels [#7343](https://github.com/excalidraw/excalidraw/pull/7343) | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## 0.17.0 (2023-11-14) | ||||
|  | ||||
| ### Features | ||||
|   | ||||
| @@ -76,8 +76,6 @@ const { | ||||
|   MainMenu, | ||||
|   LiveCollaborationTrigger, | ||||
|   convertToExcalidrawElements, | ||||
|   TTDDialog, | ||||
|   TTDDialogTrigger, | ||||
| } = window.ExcalidrawLib; | ||||
|  | ||||
| const COMMENT_ICON_DIMENSION = 32; | ||||
| @@ -683,7 +681,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|             } | ||||
|             initialData={initialStatePromiseRef.current.promise} | ||||
|             onChange={(elements, state) => { | ||||
|               // console.info("Elements :", elements, "State : ", state); | ||||
|               console.info("Elements :", elements, "State : ", state); | ||||
|             }} | ||||
|             onPointerUpdate={(payload: { | ||||
|               pointer: { x: number; y: number }; | ||||
| @@ -739,20 +737,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { | ||||
|               Toggle Custom Sidebar | ||||
|             </Sidebar.Trigger> | ||||
|             {renderMenu()} | ||||
|             {excalidrawAPI && ( | ||||
|               <TTDDialogTrigger icon={<span>😀</span>}> | ||||
|                 Text to diagram | ||||
|               </TTDDialogTrigger> | ||||
|             )} | ||||
|             <TTDDialog | ||||
|               onTextSubmit={async (_) => { | ||||
|                 console.info("submit"); | ||||
|                 // sleep for 2s | ||||
|                 await new Promise((resolve) => setTimeout(resolve, 2000)); | ||||
|                 throw new Error("error, go away now"); | ||||
|                 // return "dummy"; | ||||
|               }} | ||||
|             /> | ||||
|           </Excalidraw> | ||||
|           {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} | ||||
|           {comment && renderComment()} | ||||
|   | ||||
| @@ -246,8 +246,6 @@ export { WelcomeScreen }; | ||||
| export { LiveCollaborationTrigger }; | ||||
|  | ||||
| export { DefaultSidebar } from "../../components/DefaultSidebar"; | ||||
| export { TTDDialog } from "../../components/TTDDialog/TTDDialog"; | ||||
| export { TTDDialogTrigger } from "../../components/TTDDialog/TTDDialogTrigger"; | ||||
|  | ||||
| export { normalizeLink } from "../../data/url"; | ||||
| export { convertToExcalidrawElements } from "../../data/transform"; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@excalidraw/excalidraw", | ||||
|   "version": "0.17.1", | ||||
|   "version": "0.17.0", | ||||
|   "main": "main.js", | ||||
|   "types": "types/packages/excalidraw/index.d.ts", | ||||
|   "files": [ | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| const { merge } = require("webpack-merge"); | ||||
|  | ||||
| const prodConfig = require("./webpack.prod.config"); | ||||
| const devConfig = require("./webpack.dev.config"); | ||||
|  | ||||
| @@ -9,7 +11,6 @@ const outputFile = isProd | ||||
|   : "excalidraw-with-preact.development"; | ||||
|  | ||||
| const preactWebpackConfig = { | ||||
|   ...config, | ||||
|   entry: { | ||||
|     [outputFile]: "./entry.js", | ||||
|   }, | ||||
| @@ -29,4 +30,4 @@ const preactWebpackConfig = { | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| module.exports = preactWebpackConfig; | ||||
| module.exports = merge(config, preactWebpackConfig); | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import { | ||||
|   isInitializedImageElement, | ||||
|   isArrowElement, | ||||
|   hasBoundTextElement, | ||||
|   isIframeLikeElement, | ||||
|   isMagicFrameElement, | ||||
| } from "../element/typeChecks"; | ||||
| import { getElementAbsoluteCoords } from "../element/bounds"; | ||||
| @@ -520,7 +521,8 @@ const drawElementFromCanvas = ( | ||||
|  | ||||
|     if ( | ||||
|       "scale" in elementWithCanvas.element && | ||||
|       !isPendingImageElement(element, renderConfig) | ||||
|       !isPendingImageElement(element, renderConfig) && | ||||
|       !isIframeLikeElement(element) | ||||
|     ) { | ||||
|       context.scale( | ||||
|         elementWithCanvas.element.scale[0], | ||||
| @@ -996,6 +998,8 @@ export const renderElementToSvg = ( | ||||
|         renderConfig, | ||||
|       ); | ||||
|  | ||||
|       const scaleX = element.scale?.[0] || 1; | ||||
|       const scaleY = element.scale?.[1] || 1; | ||||
|       // render embeddable element + iframe | ||||
|       const embeddableNode = roughSVGDrawWithPrecision( | ||||
|         rsvg, | ||||
| @@ -1007,7 +1011,7 @@ export const renderElementToSvg = ( | ||||
|         "transform", | ||||
|         `translate(${offsetX || 0} ${ | ||||
|           offsetY || 0 | ||||
|         }) rotate(${degree} ${cx} ${cy})`, | ||||
|         }) rotate(${degree} ${cx} ${cy}) scale(${scaleX}, ${scaleY})`, | ||||
|       ); | ||||
|       while (embeddableNode.firstChild) { | ||||
|         embeddableNode.removeChild(embeddableNode.firstChild); | ||||
| @@ -1038,8 +1042,8 @@ export const renderElementToSvg = ( | ||||
|           SVG_NS, | ||||
|           "foreignObject", | ||||
|         ); | ||||
|         foreignObject.style.width = `${element.width}px`; | ||||
|         foreignObject.style.height = `${element.height}px`; | ||||
|         foreignObject.style.width = `${element.width / scaleX}px`; | ||||
|         foreignObject.style.height = `${element.height / scaleY}px`; | ||||
|         foreignObject.style.border = "none"; | ||||
|         const div = foreignObject.ownerDocument!.createElementNS(SVG_NS, "div"); | ||||
|         div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); | ||||
|   | ||||
| @@ -102,7 +102,7 @@ describe("Test <MermaidToExcalidraw/>", () => { | ||||
|       <Excalidraw | ||||
|         initialData={{ | ||||
|           appState: { | ||||
|             openDialog: { name: "ttd", tab: "mermaid" }, | ||||
|             openDialog: { name: "mermaid" }, | ||||
|           }, | ||||
|         }} | ||||
|       />, | ||||
| @@ -110,16 +110,16 @@ describe("Test <MermaidToExcalidraw/>", () => { | ||||
|   }); | ||||
|  | ||||
|   it("should open mermaid popup when active tool is mermaid", async () => { | ||||
|     const dialog = document.querySelector(".ttd-dialog")!; | ||||
|     const dialog = document.querySelector(".dialog-mermaid")!; | ||||
|     await waitFor(() => dialog.querySelector("canvas")); | ||||
|     expect(dialog.outerHTML).toMatchSnapshot(); | ||||
|   }); | ||||
|  | ||||
|   it("should close the popup and set the tool to selection when close button clicked", () => { | ||||
|     const dialog = document.querySelector(".ttd-dialog")!; | ||||
|     const dialog = document.querySelector(".dialog-mermaid")!; | ||||
|     const closeBtn = dialog.querySelector(".Dialog__close")!; | ||||
|     fireEvent.click(closeBtn); | ||||
|     expect(document.querySelector(".ttd-dialog")).toBe(null); | ||||
|     expect(document.querySelector(".dialog-mermaid")).toBe(null); | ||||
|     expect(window.h.state.activeTool).toStrictEqual({ | ||||
|       customType: null, | ||||
|       lastActiveTool: null, | ||||
| @@ -129,12 +129,9 @@ describe("Test <MermaidToExcalidraw/>", () => { | ||||
|   }); | ||||
|  | ||||
|   it("should show error in preview when mermaid library throws error", async () => { | ||||
|     const dialog = document.querySelector(".ttd-dialog")!; | ||||
|  | ||||
|     expect(dialog).not.toBeNull(); | ||||
|  | ||||
|     const selector = ".ttd-dialog-input"; | ||||
|     let editor = await getTextEditor(selector, true); | ||||
|     const dialog = document.querySelector(".dialog-mermaid")!; | ||||
|     const selector = ".dialog-mermaid-panels-text textarea"; | ||||
|     let editor = await getTextEditor(selector, false); | ||||
|  | ||||
|     expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull(); | ||||
|  | ||||
| @@ -154,8 +151,17 @@ describe("Test <MermaidToExcalidraw/>", () => { | ||||
|     editor = await getTextEditor(selector, false); | ||||
|  | ||||
|     expect(editor.textContent).toBe("flowchart TD1"); | ||||
|     expect( | ||||
|       dialog.querySelector('[data-testid="mermaid-error"]'), | ||||
|     ).toMatchInlineSnapshot("null"); | ||||
|     expect(dialog.querySelector('[data-testid="mermaid-error"]')) | ||||
|       .toMatchInlineSnapshot(` | ||||
|         <div | ||||
|           class="mermaid-error" | ||||
|           data-testid="mermaid-error" | ||||
|         > | ||||
|           Error!  | ||||
|           <p> | ||||
|             ERROR | ||||
|           </p> | ||||
|         </div> | ||||
|       `); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||||
|  | ||||
| exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = ` | ||||
| "<div class=\\"Modal Dialog ttd-dialog\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-labelledby=\\"dialog-title\\" data-prevent-outside-click=\\"true\\"><div class=\\"Modal__background\\"></div><div class=\\"Modal__content\\" style=\\"--max-width: 1200px;\\" tabindex=\\"0\\"><div class=\\"Island\\"><button class=\\"Dialog__close\\" title=\\"Close\\" aria-label=\\"Close\\"><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g clip-path=\\"url(#a)\\" stroke=\\"currentColor\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M15 5 5 15M5 5l10 10\\"></path></g><defs><clipPath id=\\"a\\"><path fill=\\"#fff\\" d=\\"M0 0h20v20H0z\\"></path></clipPath></defs></svg></button><div class=\\"Dialog__content\\"><div dir=\\"ltr\\" data-orientation=\\"horizontal\\" class=\\"ttd-dialog-tabs-root\\"><p class=\\"dialog-mermaid-title\\">Mermaid to Excalidraw</p><div data-state=\\"active\\" data-orientation=\\"horizontal\\" role=\\"tabpanel\\" aria-labelledby=\\"radix-:r0:-trigger-mermaid\\" id=\\"radix-:r0:-content-mermaid\\" tabindex=\\"0\\" class=\\"ttd-dialog-content\\" style=\\"animation-duration: 0s;\\"><div class=\\"ttd-dialog-desc\\">Currently only <a href=\\"https://mermaid.js.org/syntax/flowchart.html\\">Flowcharts</a> and <a href=\\"https://mermaid.js.org/syntax/sequenceDiagram.html\\">Sequence Diagrams</a> are supported. The other types will be rendered as image in Excalidraw.</div><div class=\\"ttd-dialog-panels\\"><div class=\\"ttd-dialog-panel\\"><div class=\\"ttd-dialog-panel__header\\"><label>Mermaid Syntax</label></div><textarea class=\\"ttd-dialog-input\\" placeholder=\\"Write Mermaid diagram defintion here...\\">flowchart TD | ||||
| "<div class=\\"Modal Dialog dialog-mermaid\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-labelledby=\\"dialog-title\\" data-prevent-outside-click=\\"true\\"><div class=\\"Modal__background\\"></div><div class=\\"Modal__content\\" style=\\"--max-width: 1200px;\\" tabindex=\\"0\\"><div class=\\"Island\\"><h2 id=\\"test-id-dialog-title\\" class=\\"Dialog__title\\"><span class=\\"Dialog__titleContent\\"><p class=\\"dialog-mermaid-title\\">Mermaid to Excalidraw</p><span class=\\"dialog-mermaid-desc\\">Currently only <a href=\\"https://mermaid.js.org/syntax/flowchart.html\\">Flowcharts</a> and <a href=\\"https://mermaid.js.org/syntax/sequenceDiagram.html\\">Sequence Diagrams</a> are supported. The other types will be rendered as image in Excalidraw.<br></span></span></h2><button class=\\"Dialog__close\\" title=\\"Close\\" aria-label=\\"Close\\"><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g clip-path=\\"url(#a)\\" stroke=\\"currentColor\\" stroke-width=\\"1.25\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><path d=\\"M15 5 5 15M5 5l10 10\\"></path></g><defs><clipPath id=\\"a\\"><path fill=\\"#fff\\" d=\\"M0 0h20v20H0z\\"></path></clipPath></defs></svg></button><div class=\\"Dialog__content\\"><div class=\\"dialog-mermaid-body\\"><div class=\\"dialog-mermaid-panels\\"><div class=\\"dialog-mermaid-panels-text\\"><label>Mermaid Syntax</label><textarea>flowchart TD | ||||
|  A[Christmas] -->|Get money| B(Go shopping) | ||||
|  B --> C{Let me think} | ||||
|  C -->|One| D[Laptop] | ||||
|  C -->|Two| E[iPhone] | ||||
|  C -->|Three| F[Car]</textarea><div class=\\"ttd-dialog-panel-button-container invisible\\" style=\\"display: flex; align-items: center;\\"><button type=\\"button\\" class=\\"excalidraw-button ttd-dialog-panel-button\\"><div class=\\"\\"></div></button></div></div><div class=\\"ttd-dialog-panel\\"><div class=\\"ttd-dialog-panel__header\\"><label>Preview</label></div><div class=\\"ttd-dialog-output-wrapper\\"><div style=\\"opacity: 1;\\" class=\\"ttd-dialog-output-canvas-container\\"><canvas width=\\"89\\" height=\\"158\\" dir=\\"ltr\\"></canvas></div></div><div class=\\"ttd-dialog-panel-button-container\\" style=\\"display: flex; align-items: center;\\"><button type=\\"button\\" class=\\"excalidraw-button ttd-dialog-panel-button\\"><div class=\\"\\">Insert<span><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g stroke-width=\\"1.25\\"><path d=\\"M4.16602 10H15.8327\\"></path><path d=\\"M12.5 13.3333L15.8333 10\\"></path><path d=\\"M12.5 6.66666L15.8333 9.99999\\"></path></g></svg></span></div></button></div></div></div></div></div></div></div></div></div>" | ||||
|  C -->|Three| F[Car]</textarea></div><div class=\\"dialog-mermaid-panels-preview\\"><label>Preview</label><div class=\\"dialog-mermaid-panels-preview-wrapper\\"><div style=\\"opacity: 1;\\" class=\\"dialog-mermaid-panels-preview-canvas-container\\"><canvas width=\\"89\\" height=\\"158\\" dir=\\"ltr\\"></canvas></div></div></div></div><div class=\\"dialog-mermaid-buttons\\"><button type=\\"button\\" class=\\"excalidraw-button dialog-mermaid-insert\\">Insert<span><svg aria-hidden=\\"true\\" focusable=\\"false\\" role=\\"img\\" viewBox=\\"0 0 20 20\\" class=\\"\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\"><g stroke-width=\\"1.25\\"><path d=\\"M4.16602 10H15.8327\\"></path><path d=\\"M12.5 13.3333L15.8333 10\\"></path><path d=\\"M12.5 6.66666L15.8333 9.99999\\"></path></g></svg></span></button></div></div></div></div></div></div>" | ||||
| `; | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/tests/fixtures/elementFixture.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/tests/fixtures/elementFixture.ts
									
									
									
									
										vendored
									
									
								
							| @@ -35,6 +35,7 @@ export const embeddableFixture: ExcalidrawElement = { | ||||
|   ...elementBase, | ||||
|   type: "embeddable", | ||||
|   validated: null, | ||||
|   scale: [1, 1], | ||||
| }; | ||||
| export const ellipseFixture: ExcalidrawElement = { | ||||
|   ...elementBase, | ||||
|   | ||||
| @@ -273,7 +273,7 @@ describe("Test Linear Elements", () => { | ||||
|  | ||||
|       // drag line from midpoint | ||||
|       drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); | ||||
|       expect(renderInteractiveScene).toHaveBeenCalledTimes(14); | ||||
|       expect(renderInteractiveScene).toHaveBeenCalledTimes(13); | ||||
|       expect(renderStaticScene).toHaveBeenCalledTimes(6); | ||||
|  | ||||
|       expect(line.points.length).toEqual(3); | ||||
| @@ -416,7 +416,7 @@ describe("Test Linear Elements", () => { | ||||
|           lastSegmentMidpoint[1] + delta, | ||||
|         ]); | ||||
|  | ||||
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(21); | ||||
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(19); | ||||
|         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(21); | ||||
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(20); | ||||
|         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(21); | ||||
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(19); | ||||
|         expect(renderStaticScene).toHaveBeenCalledTimes(9); | ||||
|         expect(line.points.length).toEqual(5); | ||||
|  | ||||
|   | ||||
| @@ -246,16 +246,14 @@ export interface AppState { | ||||
|   openSidebar: { name: SidebarName; tab?: SidebarTabName } | null; | ||||
|   openDialog: | ||||
|     | null | ||||
|     | { name: "imageExport" | "help" | "jsonExport" } | ||||
|     | { name: "imageExport" | "help" | "jsonExport" | "mermaid" } | ||||
|     | { | ||||
|         name: "settings"; | ||||
|         name: "magicSettings"; | ||||
|         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: "text-to-diagram" | "mermaid" | "text-to-drawing" }; | ||||
|       }; | ||||
|   /** | ||||
|    * Reflects user preference for whether the default sidebar should be docked. | ||||
|    * | ||||
|   | ||||
| @@ -925,7 +925,3 @@ export const isMemberOf = <T extends string>( | ||||
| }; | ||||
|  | ||||
| export const cloneJSON = <T>(obj: T): T => JSON.parse(JSON.stringify(obj)); | ||||
|  | ||||
| export const isFiniteNumber = (value: any): value is number => { | ||||
|   return typeof value === "number" && Number.isFinite(value); | ||||
| }; | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,6 @@ 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; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user