Compare commits

..
Author SHA1 Message Date
zsviczian 44390cb146 suport iFrame 2023-11-25 05:43:25 +00:00
zsviczianandGitHub 9da3e47877 Merge branch 'master' into zsviczian-embeddable-scaling 2023-11-25 06:15:24 +01:00
zsviczian 9f6edd8eaa lint 2023-11-17 18:50:26 +00:00
zsviczian 0d60253de7 fix border sizing 2023-11-17 18:47:01 +00:00
zsviczian 4027a5b245 don't flip video when negative scaling 2023-11-17 06:43:23 +00:00
zsviczian 49f2c88978 adjusted renderElement 2023-11-16 22:15:53 +00:00
zsviczian 156b8b422b fix - restore 2023-11-16 20:10:22 +00:00
zsviczian e2982a2968 scale embeddable and fix youtube start bug 2023-11-16 18:27:28 +00:00
54 changed files with 650 additions and 1637 deletions
-2
View File
@@ -13,8 +13,6 @@ VITE_APP_PORTAL_URL=
VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_LP=https://plus.excalidraw.com
VITE_APP_PLUS_APP=https://app.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"}' 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! # put these in your .env.local, or make sure you don't commit!
-2
View File
@@ -9,8 +9,6 @@ VITE_APP_PORTAL_URL=https://portal.excalidraw.com
VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_LP=https://plus.excalidraw.com
VITE_APP_PLUS_APP=https://app.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. # Fill to set socket server URL used for collaboration.
# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow # Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow
VITE_APP_WS_SERVER_URL= VITE_APP_WS_SERVER_URL=
+1 -1
View File
@@ -39,7 +39,7 @@ Since Vite removes env variables by default, you can update the vite config to e
``` ```
define: { 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: { define: {
"process.env.IS_PREACT": JSON.stringify("true"), "process.env.IS_PREACT": process.env.IS_PREACT,
}, },
``` ```
::: :::
-60
View File
@@ -25,8 +25,6 @@ import {
Excalidraw, Excalidraw,
defaultLang, defaultLang,
LiveCollaborationTrigger, LiveCollaborationTrigger,
TTDDialog,
TTDDialogTrigger,
} from "../src/packages/excalidraw/index"; } from "../src/packages/excalidraw/index";
import { import {
AppState, AppState,
@@ -775,64 +773,6 @@ const ExcalidrawWrapper = () => {
)} )}
</OverwriteConfirmDialog> </OverwriteConfirmDialog>
<AppFooter /> <AppFooter />
<TTDDialog
onTextSubmit={async (input) => {
try {
const response = await fetch(
`${
import.meta.env.VITE_APP_AI_BACKEND
}/v1/ai/text-to-diagram/generate`,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ prompt: input }),
},
);
const rateLimit = response.headers.has("X-Ratelimit-Limit")
? parseInt(response.headers.get("X-Ratelimit-Limit") || "0", 10)
: undefined;
const rateLimitRemaining = response.headers.has(
"X-Ratelimit-Remaining",
)
? parseInt(
response.headers.get("X-Ratelimit-Remaining") || "0",
10,
)
: undefined;
const json = await response.json();
if (!response.ok) {
if (response.status === 429) {
return {
rateLimit,
rateLimitRemaining,
error: new Error(
"Too many requests today, please try again tomorrow!",
),
};
}
throw new Error(json.message || "Generation failed...");
}
const generatedResponse = json.generatedResponse;
if (!generatedResponse) {
throw new Error("Generation failed...");
}
return { generatedResponse, rateLimit, rateLimitRemaining };
} catch (err: any) {
throw new Error("Request failed");
}
}}
/>
<TTDDialogTrigger />
{isCollaborating && isOffline && ( {isCollaborating && isOffline && (
<div className="collab-offline-warning"> <div className="collab-offline-warning">
{t("alerts.collabOfflineWarning")} {t("alerts.collabOfflineWarning")}
+4 -9
View File
@@ -40,7 +40,6 @@ import {
MagicIcon, MagicIcon,
} from "./icons"; } from "./icons";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { useTunnels } from "../context/tunnels";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
@@ -236,8 +235,6 @@ export const ShapesSwitcher = ({
const laserToolSelected = activeTool.type === "laser"; const laserToolSelected = activeTool.type === "laser";
const embeddableToolSelected = activeTool.type === "embeddable"; const embeddableToolSelected = activeTool.type === "embeddable";
const { TTDDialogTriggerTunnel } = useTunnels();
return ( return (
<> <>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
@@ -341,14 +338,14 @@ export const ShapesSwitcher = ({
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}> <div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
Generate Generate
</div> </div>
{app.props.aiEnabled !== false && <TTDDialogTriggerTunnel.Out />}
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => app.setOpenDialog({ name: "ttd", tab: "mermaid" })} onSelect={() => app.setOpenDialog({ name: "mermaid" })}
icon={mermaidLogoIcon} icon={mermaidLogoIcon}
data-testid="toolbar-embeddable" data-testid="toolbar-embeddable"
> >
{t("toolBar.mermaidToExcalidraw")} {t("toolBar.mermaidToExcalidraw")}
</DropdownMenu.Item> </DropdownMenu.Item>
{app.props.aiEnabled !== false && ( {app.props.aiEnabled !== false && (
<> <>
<DropdownMenu.Item <DropdownMenu.Item
@@ -357,15 +354,13 @@ export const ShapesSwitcher = ({
data-testid="toolbar-magicframe" data-testid="toolbar-magicframe"
> >
{t("toolBar.magicframe")} {t("toolBar.magicframe")}
<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => { onSelect={() => {
trackEvent("ai", "open-settings", "d2c"); trackEvent("ai", "d2c-settings", "settings");
app.setOpenDialog({ app.setOpenDialog({
name: "settings", name: "magicSettings",
source: "settings", source: "settings",
tab: "diagram-to-code",
}); });
}} }}
icon={OpenAIIcon} icon={OpenAIIcon}
+25 -26
View File
@@ -381,6 +381,7 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
import { StaticCanvas, InteractiveCanvas } from "./canvases"; import { StaticCanvas, InteractiveCanvas } from "./canvases";
import { Renderer } from "../scene/Renderer"; import { Renderer } from "../scene/Renderer";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
import MermaidToExcalidraw from "./MermaidToExcalidraw";
import { LaserToolOverlay } from "./LaserTool/LaserTool"; import { LaserToolOverlay } from "./LaserTool/LaserTool";
import { LaserPathManager } from "./LaserTool/LaserPathManager"; import { LaserPathManager } from "./LaserTool/LaserPathManager";
import { import {
@@ -1037,6 +1038,10 @@ class App extends React.Component<AppProps, AppState> {
this.state.activeEmbeddable?.element === el && this.state.activeEmbeddable?.element === el &&
this.state.activeEmbeddable?.state === "hover"; 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 ( return (
<div <div
key={el.id} key={el.id}
@@ -1047,14 +1052,13 @@ class App extends React.Component<AppProps, AppState> {
transform: isVisible transform: isVisible
? `translate(${x - this.state.offsetLeft}px, ${ ? `translate(${x - this.state.offsetLeft}px, ${
y - this.state.offsetTop y - this.state.offsetTop
}px) scale(${scale})` }px) ${scaledTransform}`
: "none", : "none",
display: isVisible ? "block" : "none", display: isVisible ? "block" : "none",
opacity: el.opacity / 100, opacity: el.opacity / 100,
["--embeddable-radius" as string]: `${getCornerRadius( ["--embeddable-radius" as string]: `${
Math.min(el.width, el.height), getCornerRadius(Math.min(el.width, el.height), el) / xScale
el, }px`,
)}px`,
}} }}
> >
<div <div
@@ -1076,8 +1080,8 @@ class App extends React.Component<AppProps, AppState> {
}}*/ }}*/
className="excalidraw__embeddable-container__inner" className="excalidraw__embeddable-container__inner"
style={{ style={{
width: isVisible ? `${el.width}px` : 0, width: isVisible ? `${el.width / xScale}px` : 0,
height: isVisible ? `${el.height}px` : 0, height: isVisible ? `${el.height / yScale}px` : 0,
transform: isVisible ? `rotate(${el.angle}rad)` : "none", transform: isVisible ? `rotate(${el.angle}rad)` : "none",
pointerEvents: isActive pointerEvents: isActive
? POINTER_EVENTS.enabled ? POINTER_EVENTS.enabled
@@ -1092,7 +1096,7 @@ class App extends React.Component<AppProps, AppState> {
<div <div
className="excalidraw__embeddable__outer" className="excalidraw__embeddable__outer"
style={{ style={{
padding: `${el.strokeWidth}px`, padding: `${el.strokeWidth / el.scale[0]}px`,
}} }}
> >
{(isEmbeddableElement(el) {(isEmbeddableElement(el)
@@ -1434,6 +1438,9 @@ class App extends React.Component<AppProps, AppState> {
onMagicSettingsConfirm={this.onMagicSettingsConfirm} onMagicSettingsConfirm={this.onMagicSettingsConfirm}
> >
{this.props.children} {this.props.children}
{this.state.openDialog?.name === "mermaid" && (
<MermaidToExcalidraw />
)}
</LayerUI> </LayerUI>
<div className="excalidraw-textEditorContainer" /> <div className="excalidraw-textEditorContainer" />
@@ -1700,13 +1707,9 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
if (!this.OPENAI_KEY) { if (!this.OPENAI_KEY) {
this.setState({ this.setState({
openDialog: { openDialog: { name: "magicSettings", source: "generation" },
name: "settings",
tab: "diagram-to-code",
source: "generation",
},
}); });
trackEvent("ai", "generate (missing key)", "d2c"); trackEvent("ai", "d2c-generate", "missing-key");
return; return;
} }
@@ -1719,7 +1722,7 @@ class App extends React.Component<AppProps, AppState> {
if (!magicFrameChildren.length) { if (!magicFrameChildren.length) {
if (source === "button") { if (source === "button") {
this.setState({ errorMessage: "Cannot generate from an empty frame" }); this.setState({ errorMessage: "Cannot generate from an empty frame" });
trackEvent("ai", "generate (no-children)", "d2c"); trackEvent("ai", "d2c-generate", "no-children");
} else { } else {
this.setActiveTool({ type: "magicframe" }); this.setActiveTool({ type: "magicframe" });
} }
@@ -1761,7 +1764,7 @@ class App extends React.Component<AppProps, AppState> {
const textFromFrameChildren = this.getTextFromElements(magicFrameChildren); const textFromFrameChildren = this.getTextFromElements(magicFrameChildren);
trackEvent("ai", "generate (start)", "d2c"); trackEvent("ai", "d2c-generate", "generating");
const result = await diagramToHTML({ const result = await diagramToHTML({
image: dataURL, image: dataURL,
@@ -1771,7 +1774,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
if (!result.ok) { if (!result.ok) {
trackEvent("ai", "generate (failed)", "d2c"); trackEvent("ai", "d2c-generate", "generating-failed");
console.error(result.error); console.error(result.error);
this.updateMagicGeneration({ this.updateMagicGeneration({
frameElement, frameElement,
@@ -1783,7 +1786,7 @@ class App extends React.Component<AppProps, AppState> {
}); });
return; return;
} }
trackEvent("ai", "generate (success)", "d2c"); trackEvent("ai", "d2c-generate", "generating-done");
if (result.choices[0].message.content == null) { if (result.choices[0].message.content == null) {
this.updateMagicGeneration({ this.updateMagicGeneration({
@@ -1875,13 +1878,9 @@ class App extends React.Component<AppProps, AppState> {
public onMagicframeToolSelect = () => { public onMagicframeToolSelect = () => {
if (!this.OPENAI_KEY) { if (!this.OPENAI_KEY) {
this.setState({ this.setState({
openDialog: { openDialog: { name: "magicSettings", source: "tool" },
name: "settings",
tab: "diagram-to-code",
source: "tool",
},
}); });
trackEvent("ai", "tool-select (missing key)", "d2c"); trackEvent("ai", "d2c-tool", "missing-key");
return; return;
} }
@@ -1891,7 +1890,7 @@ class App extends React.Component<AppProps, AppState> {
if (selectedElements.length === 0) { if (selectedElements.length === 0) {
this.setActiveTool({ type: TOOL_TYPE.magicframe }); this.setActiveTool({ type: TOOL_TYPE.magicframe });
trackEvent("ai", "tool-select (empty-selection)", "d2c"); trackEvent("ai", "d2c-tool", "empty-selection");
} else { } else {
const selectedMagicFrame: ExcalidrawMagicFrameElement | false = const selectedMagicFrame: ExcalidrawMagicFrameElement | false =
selectedElements.length === 1 && selectedElements.length === 1 &&
@@ -1909,7 +1908,7 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
trackEvent("ai", "tool-select (existing selection)", "d2c"); trackEvent("ai", "d2c-tool", "existing-selection");
let frame: ExcalidrawMagicFrameElement; let frame: ExcalidrawMagicFrameElement;
if (selectedMagicFrame) { if (selectedMagicFrame) {
+1 -5
View File
@@ -2,11 +2,7 @@ import clsx from "clsx";
import { composeEventHandlers } from "../utils"; import { composeEventHandlers } from "../utils";
import "./Button.scss"; import "./Button.scss";
interface ButtonProps interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
type?: "button" | "submit" | "reset"; type?: "button" | "submit" | "reset";
onSelect: () => any; onSelect: () => any;
/** whether button is in active state */ /** whether button is in active state */
+2 -4
View File
@@ -62,7 +62,6 @@ import { ShapeCache } from "../scene/ShapeCache";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { LaserPointerButton } from "./LaserTool/LaserPointerButton"; import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
import { MagicSettings } from "./MagicSettings"; import { MagicSettings } from "./MagicSettings";
import { TTDDialog } from "./TTDDialog/TTDDialog";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@@ -397,7 +396,6 @@ const LayerUI = ({
{t("toolBar.library")} {t("toolBar.library")}
</DefaultSidebar.Trigger> </DefaultSidebar.Trigger>
<DefaultOverwriteConfirmDialog /> <DefaultOverwriteConfirmDialog />
{appState.openDialog?.name === "ttd" && <TTDDialog __fallback />}
{/* ------------------------------------------------------------------ */} {/* ------------------------------------------------------------------ */}
{appState.isLoading && <LoadingMessage delay={250} />} {appState.isLoading && <LoadingMessage delay={250} />}
@@ -461,14 +459,14 @@ const LayerUI = ({
}} }}
/> />
)} )}
{appState.openDialog?.name === "settings" && ( {appState.openDialog?.name === "magicSettings" && (
<MagicSettings <MagicSettings
openAIKey={openAIKey} openAIKey={openAIKey}
isPersisted={isOpenAIKeyPersisted} isPersisted={isOpenAIKeyPersisted}
onChange={onOpenAIAPIKeyChange} onChange={onOpenAIAPIKeyChange}
onConfirm={(apiKey, shouldPersist) => { onConfirm={(apiKey, shouldPersist) => {
const source = const source =
appState.openDialog?.name === "settings" appState.openDialog?.name === "magicSettings"
? appState.openDialog?.source ? appState.openDialog?.source
: "settings"; : "settings";
setAppState({ openDialog: null }, () => { setAppState({ openDialog: null }, () => {
-9
View File
@@ -1,18 +1,9 @@
.excalidraw { .excalidraw {
.MagicSettings {
.Island {
height: 100%;
display: flex;
flex-direction: column;
}
}
.MagicSettings-confirm { .MagicSettings-confirm {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
} }
.MagicSettings__confirm { .MagicSettings__confirm {
margin-top: 2rem; margin-top: 2rem;
margin-right: auto;
} }
} }
+18 -48
View File
@@ -10,8 +10,6 @@ import { InlineIcon } from "./InlineIcon";
import { Paragraph } from "./Paragraph"; import { Paragraph } from "./Paragraph";
import "./MagicSettings.scss"; import "./MagicSettings.scss";
import TTDDialogTabs from "./TTDDialog/TTDDialogTabs";
import { TTDDialogTab } from "./TTDDialog/TTDDialogTab";
export const MagicSettings = (props: { export const MagicSettings = (props: {
openAIKey: string | null; openAIKey: string | null;
@@ -20,21 +18,16 @@ export const MagicSettings = (props: {
onConfirm: (key: string, shouldPersist: boolean) => void; onConfirm: (key: string, shouldPersist: boolean) => void;
onClose: () => void; onClose: () => void;
}) => { }) => {
const { theme } = useUIAppState();
const [keyInputValue, setKeyInputValue] = useState(props.openAIKey || ""); const [keyInputValue, setKeyInputValue] = useState(props.openAIKey || "");
const [shouldPersist, setShouldPersist] = useState<boolean>( const [shouldPersist, setShouldPersist] = useState<boolean>(
props.isPersisted, props.isPersisted,
); );
const appState = useUIAppState();
const onConfirm = () => { const onConfirm = () => {
props.onConfirm(keyInputValue.trim(), shouldPersist); props.onConfirm(keyInputValue.trim(), shouldPersist);
}; };
if (appState.openDialog?.name !== "settings") {
return null;
}
return ( return (
<Dialog <Dialog
onCloseRequest={() => { onCloseRequest={() => {
@@ -43,7 +36,7 @@ export const MagicSettings = (props: {
}} }}
title={ title={
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
Wireframe to Code (AI){" "} Diagram to Code (AI){" "}
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -53,8 +46,7 @@ export const MagicSettings = (props: {
marginLeft: "1rem", marginLeft: "1rem",
fontSize: 14, fontSize: 14,
borderRadius: "12px", borderRadius: "12px",
color: "#000", background: theme === "light" ? "#FFCCCC" : "#703333",
background: "pink",
}} }}
> >
Experimental Experimental
@@ -64,39 +56,19 @@ export const MagicSettings = (props: {
className="MagicSettings" className="MagicSettings"
autofocus={false} autofocus={false}
> >
{/* <h2 <Paragraph
style={{ style={{
margin: 0, display: "inline-flex",
fontSize: "1.25rem", alignItems: "center",
paddingLeft: "2.5rem", marginBottom: 0,
}} }}
> >
AI Settings For the diagram-to-code feature we use <InlineIcon icon={OpenAIIcon} />
</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"
>
<Paragraph>
For the diagram-to-code feature we use{" "}
<InlineIcon icon={OpenAIIcon} />
OpenAI. OpenAI.
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
While the OpenAI API is in beta, its use is strictly limited — as While the OpenAI API is in beta, its use is strictly limited — as such
such we require you use your own API key. You can create an{" "} we require you use your own API key. You can create an{" "}
<a <a
href="https://platform.openai.com/login?launch" href="https://platform.openai.com/login?launch"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -115,8 +87,8 @@ export const MagicSettings = (props: {
. .
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
Your OpenAI key does not leave the browser, and you can also set Your OpenAI key does not leave the browser, and you can also set your
your own limit in your OpenAI account dashboard if needed. own limit in your OpenAI account dashboard if needed.
</Paragraph> </Paragraph>
<TextField <TextField
isRedacted isRedacted
@@ -131,9 +103,9 @@ export const MagicSettings = (props: {
onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()} onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()}
/> />
<Paragraph> <Paragraph>
By default, your API token is not persisted anywhere so you'll need By default, your API token is not persisted anywhere so you'll need to
to insert it again after reload. But, you can persist locally in insert it again after reload. But, you can persist locally in your
your browser below. browser below.
</Paragraph> </Paragraph>
<CheckboxItem checked={shouldPersist} onChange={setShouldPersist}> <CheckboxItem checked={shouldPersist} onChange={setShouldPersist}>
@@ -142,9 +114,9 @@ export const MagicSettings = (props: {
<Paragraph> <Paragraph>
Once API key is set, you can use the <InlineIcon icon={MagicIcon} />{" "} 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 tool to wrap your elements in a frame that will then allow you to turn
turn it into code. This dialog can be accessed using the{" "} it into code. This dialog can be accessed using the <b>AI Settings</b>{" "}
<b>AI Settings</b> <InlineIcon icon={OpenAIIcon} />. <InlineIcon icon={OpenAIIcon} />.
</Paragraph> </Paragraph>
<FilledButton <FilledButton
@@ -153,8 +125,6 @@ export const MagicSettings = (props: {
label="Confirm" label="Confirm"
onClick={onConfirm} onClick={onConfirm}
/> />
</TTDDialogTab>
</TTDDialogTabs>
</Dialog> </Dialog>
); );
}; };
+221
View 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
View 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;
+1 -4
View File
@@ -18,11 +18,8 @@
overflow: auto; overflow: auto;
padding: calc(var(--space-factor) * 10); padding: calc(var(--space-factor) * 10);
display: flex;
flex-direction: column;
.Island { .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,
text,
shouldSaveMermaidDataToStorage: true,
});
},
label: t("mermaid.button"),
icon: ArrowRightIcon,
}}
>
<TTDDialogOutput
canvasRef={canvasRef}
loaded={mermaidToExcalidrawLib.loaded}
error={error}
/>
</TTDDialogPanel>
</TTDDialogPanels>
</>
);
};
export default MermaidToExcalidraw;
-301
View File
@@ -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;
}
}
}
}
-350
View File
@@ -1,350 +0,0 @@
import { Dialog } from "../Dialog";
import { useApp } from "../App";
import MermaidToExcalidraw from "./MermaidToExcalidraw";
import TTDDialogTabs from "./TTDDialogTabs";
import { ChangeEventHandler, useEffect, useRef, 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 { TTDDialogInput } from "./TTDDialogInput";
import { TTDDialogOutput } from "./TTDDialogOutput";
import { TTDDialogPanel } from "./TTDDialogPanel";
import { TTDDialogPanels } from "./TTDDialogPanels";
import {
MermaidToExcalidrawLibProps,
convertMermaidToExcalidraw,
insertToEditor,
saveMermaidDataToStorage,
} from "./common";
import { NonDeletedExcalidrawElement } from "../../element/types";
import { BinaryFiles } from "../../types";
import { ArrowRightIcon } from "../icons";
import "./TTDDialog.scss";
import { isFiniteNumber } from "../../utils";
import { atom, useAtom } from "jotai";
import { trackEvent } from "../../analytics";
const MIN_PROMPT_LENGTH = 3;
const MAX_PROMPT_LENGTH = 1000;
const rateLimitsAtom = atom<{
rateLimit: number;
rateLimitRemaining: number;
} | null>(null);
type OnTestSubmitRetValue = {
rateLimit?: number | null;
rateLimitRemaining?: number | null;
} & (
| { generatedResponse: string | undefined; error?: null | undefined }
| {
error: Error;
generatedResponse?: null | undefined;
}
);
export const TTDDialog = (
props:
| {
onTextSubmit(value: string): Promise<OnTestSubmitRetValue>;
}
| { __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";
} & (
| {
onTextSubmit(value: string): Promise<OnTestSubmitRetValue>;
}
| { __fallback: true }
)) => {
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 onGenerate = async () => {
if (
prompt.length > MAX_PROMPT_LENGTH ||
prompt.length < MIN_PROMPT_LENGTH ||
onTextSubmitInProgess ||
rateLimits?.rateLimitRemaining === 0 ||
// means this is not a text-to-diagram dialog (needed for TS only)
"__fallback" in rest
) {
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 rest.onTextSubmit(prompt);
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;
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]);
const data = useRef<{
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles | null;
}>({ elements: [], files: null });
const [error, setError] = useState<Error | null>(null);
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="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">
<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 });
},
label: "Insert",
icon: ArrowRightIcon,
}}
>
<TTDDialogOutput
canvasRef={someRandomDivRef}
error={error}
loaded={mermaidToExcalidrawLib.loaded}
/>
</TTDDialogPanel>
</TTDDialogPanels>
</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>;
};
-17
View File
@@ -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,64 +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" }
| { 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"], tab)
) {
setAppState({
openDialog: { name: props.dialog, tab },
});
}
}}
>
{props.children}
</RadixTabs.Root>
);
};
TTDDialogTabs.displayName = "TTDDialogTabs";
export default TTDDialogTabs;
@@ -1,34 +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,
}: {
children?: ReactNode;
icon?: JSX.Element;
}) => {
const { TTDDialogTriggerTunnel } = useTunnels();
const setAppState = useExcalidrawSetAppState();
return (
<TTDDialogTriggerTunnel.In>
<DropdownMenu.Item
onSelect={() => {
trackEvent("ai", "dialog open", "ttd");
setAppState({ openDialog: { name: "ttd", 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";
-164
View File
@@ -1,164 +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";
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 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: React.MutableRefObject<{
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles | null;
}>;
text?: string;
shouldSaveMermaidDataToStorage?: boolean;
}) => {
const { elements: newElements, files } = data.current;
if (!newElements.length) {
return;
}
app.addElementsFromPasteOrLibrary({
elements: newElements,
files,
position: "center",
fitToContent: true,
});
app.setOpenDialog(null);
if (shouldSaveMermaidDataToStorage && text) {
saveMermaidDataToStorage(text);
}
};
@@ -63,13 +63,9 @@
} }
&__text { &__text {
display: flex;
align-items: center;
width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
gap: 0.75rem;
} }
&__shortcut { &__shortcut {
@@ -37,32 +37,6 @@ const DropdownMenuItem = ({
</button> </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; export default DropdownMenuItem;
DropdownMenuItem.displayName = "DropdownMenuItem";
-13
View File
@@ -1742,16 +1742,3 @@ export const eyeClosedIcon = createIcon(
</g>, </g>,
tablerIconProps, 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,
);
-2
View File
@@ -13,7 +13,6 @@ type TunnelsContextValue = {
DefaultSidebarTriggerTunnel: Tunnel; DefaultSidebarTriggerTunnel: Tunnel;
DefaultSidebarTabTriggersTunnel: Tunnel; DefaultSidebarTabTriggersTunnel: Tunnel;
OverwriteConfirmDialogTunnel: Tunnel; OverwriteConfirmDialogTunnel: Tunnel;
TTDDialogTriggerTunnel: Tunnel;
jotaiScope: symbol; jotaiScope: symbol;
}; };
@@ -33,7 +32,6 @@ export const useInitializeTunnels = () => {
DefaultSidebarTriggerTunnel: tunnel(), DefaultSidebarTriggerTunnel: tunnel(),
DefaultSidebarTabTriggersTunnel: tunnel(), DefaultSidebarTabTriggersTunnel: tunnel(),
OverwriteConfirmDialogTunnel: tunnel(), OverwriteConfirmDialogTunnel: tunnel(),
TTDDialogTriggerTunnel: tunnel(),
jotaiScope: Symbol(), jotaiScope: Symbol(),
}; };
}, []); }, []);
+4 -14
View File
@@ -39,7 +39,6 @@
button { button {
cursor: pointer; cursor: pointer;
user-select: none;
} }
&:focus { &:focus {
@@ -652,19 +651,6 @@
--button-bg: var(--color-surface-high); --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 { .ErrorSplash.excalidraw {
@@ -748,4 +734,8 @@
letter-spacing: 0.6px; letter-spacing: 0.6px;
font-family: "Assistant"; font-family: "Assistant";
} }
.excalidraw__paragraph {
margin: 1rem 0;
}
} }
+5 -1
View File
@@ -294,11 +294,15 @@ const restoreElement = (
case "ellipse": case "ellipse":
case "rectangle": case "rectangle":
case "diamond": case "diamond":
case "iframe":
return restoreElementWithProperties(element, {}); return restoreElementWithProperties(element, {});
case "iframe":
return restoreElementWithProperties(element, {
scale: element.scale ?? [1, 1],
});
case "embeddable": case "embeddable":
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
validated: null, validated: null,
scale: element.scale ?? [1, 1],
}); });
case "magicframe": case "magicframe":
case "frame": case "frame":
+1 -8
View File
@@ -13,7 +13,6 @@ import { Point } from "../types";
import { generateRoughOptions } from "../scene/Shape"; import { generateRoughOptions } from "../scene/Shape";
import { import {
isArrowElement, isArrowElement,
isBoundToContainer,
isFreeDrawElement, isFreeDrawElement,
isLinearElement, isLinearElement,
isTextElement, isTextElement,
@@ -55,13 +54,7 @@ export class ElementBounds {
static getBounds(element: ExcalidrawElement) { static getBounds(element: ExcalidrawElement) {
const cachedBounds = ElementBounds.boundsCache.get(element); const cachedBounds = ElementBounds.boundsCache.get(element);
if ( if (cachedBounds?.version && cachedBounds.version === element.version) {
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)
) {
return cachedBounds.bounds; return cachedBounds.bounds;
} }
+1 -1
View File
@@ -21,7 +21,7 @@ import {
const embeddedLinkCache = new Map<string, IframeData>(); const embeddedLinkCache = new Map<string, IframeData>();
const RE_YOUTUBE = 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 = const RE_VIMEO =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/; /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
+2
View File
@@ -142,6 +142,7 @@ export const newEmbeddableElement = (
return { return {
..._newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts), ..._newElementBase<ExcalidrawEmbeddableElement>("embeddable", opts),
validated: opts.validated, validated: opts.validated,
scale: [1, 1],
}; };
}; };
@@ -152,6 +153,7 @@ export const newIframeElement = (
): NonDeleted<ExcalidrawIframeElement> => { ): NonDeleted<ExcalidrawIframeElement> => {
return { return {
..._newElementBase<ExcalidrawIframeElement>("iframe", opts), ..._newElementBase<ExcalidrawIframeElement>("iframe", opts),
scale: [1, 1],
}; };
}; };
+17
View File
@@ -27,6 +27,7 @@ import {
import { import {
isArrowElement, isArrowElement,
isBoundToContainer, isBoundToContainer,
isIframeLikeElement,
isFrameLikeElement, isFrameLikeElement,
isFreeDrawElement, isFreeDrawElement,
isImageElement, isImageElement,
@@ -586,6 +587,21 @@ export const resizeSingleElement = (
}; };
if ("scale" in element && "scale" in stateAtResizeStart) { if ("scale" in element && "scale" in stateAtResizeStart) {
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, { mutateElement(element, {
scale: [ scale: [
// defaulting because scaleX/Y can be 0/-0 // defaulting because scaleX/Y can be 0/-0
@@ -596,6 +612,7 @@ export const resizeSingleElement = (
], ],
}); });
} }
}
if ( if (
isArrowElement(element) && isArrowElement(element) &&
+2
View File
@@ -96,6 +96,7 @@ export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
* may not have access to host-app supplied url validator during restore. * may not have access to host-app supplied url validator during restore.
*/ */
validated: boolean | null; validated: boolean | null;
scale: [number, number];
}>; }>;
export type ExcalidrawIframeElement = _ExcalidrawElementBase & export type ExcalidrawIframeElement = _ExcalidrawElementBase &
@@ -103,6 +104,7 @@ export type ExcalidrawIframeElement = _ExcalidrawElementBase &
type: "iframe"; type: "iframe";
// TODO move later to AI-specific frame // TODO move later to AI-specific frame
customData?: { generationData?: MagicCacheData }; customData?: { generationData?: MagicCacheData };
scale: [number, number];
}>; }>;
export type ExcalidrawIframeLikeElement = export type ExcalidrawIframeLikeElement =
+1 -3
View File
@@ -132,9 +132,7 @@
"sidebarLock": "Keep sidebar open", "sidebarLock": "Keep sidebar open",
"selectAllElementsInFrame": "Select all elements in frame", "selectAllElementsInFrame": "Select all elements in frame",
"removeAllElementsFromFrame": "Remove all elements from frame", "removeAllElementsFromFrame": "Remove all elements from frame",
"eyeDropper": "Pick color from canvas", "eyeDropper": "Pick color from canvas"
"textToDiagram": "Text to diagram",
"prompt": "Prompt"
}, },
"library": { "library": {
"noItems": "No items added yet...", "noItems": "No items added yet...",
+1 -1
View File
@@ -1,7 +1,7 @@
[ [
{ {
"path": "dist/excalidraw.production.min.js", "path": "dist/excalidraw.production.min.js",
"limit": "335 kB" "limit": "325 kB"
}, },
{ {
"path": "dist/excalidraw-assets/locales", "path": "dist/excalidraw-assets/locales",
-20
View File
@@ -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) - `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336)
## 0.17.2 (2023-12-04)
### 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) ## 0.17.0 (2023-11-14)
### Features ### Features
+1 -17
View File
@@ -76,8 +76,6 @@ const {
MainMenu, MainMenu,
LiveCollaborationTrigger, LiveCollaborationTrigger,
convertToExcalidrawElements, convertToExcalidrawElements,
TTDDialog,
TTDDialogTrigger,
} = window.ExcalidrawLib; } = window.ExcalidrawLib;
const COMMENT_ICON_DIMENSION = 32; const COMMENT_ICON_DIMENSION = 32;
@@ -683,7 +681,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
} }
initialData={initialStatePromiseRef.current.promise} initialData={initialStatePromiseRef.current.promise}
onChange={(elements, state) => { onChange={(elements, state) => {
// console.info("Elements :", elements, "State : ", state); console.info("Elements :", elements, "State : ", state);
}} }}
onPointerUpdate={(payload: { onPointerUpdate={(payload: {
pointer: { x: number; y: number }; pointer: { x: number; y: number };
@@ -739,20 +737,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
Toggle Custom Sidebar Toggle Custom Sidebar
</Sidebar.Trigger> </Sidebar.Trigger>
{renderMenu()} {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> </Excalidraw>
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
{comment && renderComment()} {comment && renderComment()}
-2
View File
@@ -246,8 +246,6 @@ export { WelcomeScreen };
export { LiveCollaborationTrigger }; export { LiveCollaborationTrigger };
export { DefaultSidebar } from "../../components/DefaultSidebar"; export { DefaultSidebar } from "../../components/DefaultSidebar";
export { TTDDialog } from "../../components/TTDDialog/TTDDialog";
export { TTDDialogTrigger } from "../../components/TTDDialog/TTDDialogTrigger";
export { normalizeLink } from "../../data/url"; export { normalizeLink } from "../../data/url";
export { convertToExcalidrawElements } from "../../data/transform"; export { convertToExcalidrawElements } from "../../data/transform";
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@excalidraw/excalidraw", "name": "@excalidraw/excalidraw",
"version": "0.17.2", "version": "0.17.0",
"main": "main.js", "main": "main.js",
"types": "types/packages/excalidraw/index.d.ts", "types": "types/packages/excalidraw/index.d.ts",
"files": [ "files": [
@@ -1,3 +1,5 @@
const { merge } = require("webpack-merge");
const prodConfig = require("./webpack.prod.config"); const prodConfig = require("./webpack.prod.config");
const devConfig = require("./webpack.dev.config"); const devConfig = require("./webpack.dev.config");
@@ -9,7 +11,6 @@ const outputFile = isProd
: "excalidraw-with-preact.development"; : "excalidraw-with-preact.development";
const preactWebpackConfig = { const preactWebpackConfig = {
...config,
entry: { entry: {
[outputFile]: "./entry.js", [outputFile]: "./entry.js",
}, },
@@ -29,4 +30,4 @@ const preactWebpackConfig = {
}, },
}, },
}; };
module.exports = preactWebpackConfig; module.exports = merge(config, preactWebpackConfig);
+8 -4
View File
@@ -13,6 +13,7 @@ import {
isInitializedImageElement, isInitializedImageElement,
isArrowElement, isArrowElement,
hasBoundTextElement, hasBoundTextElement,
isIframeLikeElement,
isMagicFrameElement, isMagicFrameElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { getElementAbsoluteCoords } from "../element/bounds"; import { getElementAbsoluteCoords } from "../element/bounds";
@@ -520,7 +521,8 @@ const drawElementFromCanvas = (
if ( if (
"scale" in elementWithCanvas.element && "scale" in elementWithCanvas.element &&
!isPendingImageElement(element, renderConfig) !isPendingImageElement(element, renderConfig) &&
!isIframeLikeElement(element)
) { ) {
context.scale( context.scale(
elementWithCanvas.element.scale[0], elementWithCanvas.element.scale[0],
@@ -996,6 +998,8 @@ export const renderElementToSvg = (
renderConfig, renderConfig,
); );
const scaleX = element.scale?.[0] || 1;
const scaleY = element.scale?.[1] || 1;
// render embeddable element + iframe // render embeddable element + iframe
const embeddableNode = roughSVGDrawWithPrecision( const embeddableNode = roughSVGDrawWithPrecision(
rsvg, rsvg,
@@ -1007,7 +1011,7 @@ export const renderElementToSvg = (
"transform", "transform",
`translate(${offsetX || 0} ${ `translate(${offsetX || 0} ${
offsetY || 0 offsetY || 0
}) rotate(${degree} ${cx} ${cy})`, }) rotate(${degree} ${cx} ${cy}) scale(${scaleX}, ${scaleY})`,
); );
while (embeddableNode.firstChild) { while (embeddableNode.firstChild) {
embeddableNode.removeChild(embeddableNode.firstChild); embeddableNode.removeChild(embeddableNode.firstChild);
@@ -1038,8 +1042,8 @@ export const renderElementToSvg = (
SVG_NS, SVG_NS,
"foreignObject", "foreignObject",
); );
foreignObject.style.width = `${element.width}px`; foreignObject.style.width = `${element.width / scaleX}px`;
foreignObject.style.height = `${element.height}px`; foreignObject.style.height = `${element.height / scaleY}px`;
foreignObject.style.border = "none"; foreignObject.style.border = "none";
const div = foreignObject.ownerDocument!.createElementNS(SVG_NS, "div"); const div = foreignObject.ownerDocument!.createElementNS(SVG_NS, "div");
div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); div.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
+19 -13
View File
@@ -102,7 +102,7 @@ describe("Test <MermaidToExcalidraw/>", () => {
<Excalidraw <Excalidraw
initialData={{ initialData={{
appState: { 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 () => { 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")); await waitFor(() => dialog.querySelector("canvas"));
expect(dialog.outerHTML).toMatchSnapshot(); expect(dialog.outerHTML).toMatchSnapshot();
}); });
it("should close the popup and set the tool to selection when close button clicked", () => { 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")!; const closeBtn = dialog.querySelector(".Dialog__close")!;
fireEvent.click(closeBtn); fireEvent.click(closeBtn);
expect(document.querySelector(".ttd-dialog")).toBe(null); expect(document.querySelector(".dialog-mermaid")).toBe(null);
expect(window.h.state.activeTool).toStrictEqual({ expect(window.h.state.activeTool).toStrictEqual({
customType: null, customType: null,
lastActiveTool: null, lastActiveTool: null,
@@ -129,12 +129,9 @@ describe("Test <MermaidToExcalidraw/>", () => {
}); });
it("should show error in preview when mermaid library throws error", async () => { it("should show error in preview when mermaid library throws error", async () => {
const dialog = document.querySelector(".ttd-dialog")!; const dialog = document.querySelector(".dialog-mermaid")!;
const selector = ".dialog-mermaid-panels-text textarea";
expect(dialog).not.toBeNull(); let editor = await getTextEditor(selector, false);
const selector = ".ttd-dialog-input";
let editor = await getTextEditor(selector, true);
expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull(); expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
@@ -154,8 +151,17 @@ describe("Test <MermaidToExcalidraw/>", () => {
editor = await getTextEditor(selector, false); editor = await getTextEditor(selector, false);
expect(editor.textContent).toBe("flowchart TD1"); expect(editor.textContent).toBe("flowchart TD1");
expect( expect(dialog.querySelector('[data-testid="mermaid-error"]'))
dialog.querySelector('[data-testid="mermaid-error"]'), .toMatchInlineSnapshot(`
).toMatchInlineSnapshot("null"); <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 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = ` 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] --&gt;|Get money| B(Go shopping) A[Christmas] --&gt;|Get money| B(Go shopping)
B --&gt; C{Let me think} B --&gt; C{Let me think}
C --&gt;|One| D[Laptop] C --&gt;|One| D[Laptop]
C --&gt;|Two| E[iPhone] C --&gt;|Two| E[iPhone]
C --&gt;|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 --&gt;|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
View File
@@ -35,6 +35,7 @@ export const embeddableFixture: ExcalidrawElement = {
...elementBase, ...elementBase,
type: "embeddable", type: "embeddable",
validated: null, validated: null,
scale: [1, 1],
}; };
export const ellipseFixture: ExcalidrawElement = { export const ellipseFixture: ExcalidrawElement = {
...elementBase, ...elementBase,
+4 -4
View File
@@ -273,7 +273,7 @@ describe("Test Linear Elements", () => {
// drag line from midpoint // drag line from midpoint
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
expect(renderInteractiveScene).toHaveBeenCalledTimes(14); expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(renderStaticScene).toHaveBeenCalledTimes(6);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
@@ -416,7 +416,7 @@ describe("Test Linear Elements", () => {
lastSegmentMidpoint[1] + delta, lastSegmentMidpoint[1] + delta,
]); ]);
expect(renderInteractiveScene).toHaveBeenCalledTimes(21); expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(renderStaticScene).toHaveBeenCalledTimes(9);
expect(line.points.length).toEqual(5); expect(line.points.length).toEqual(5);
@@ -519,7 +519,7 @@ describe("Test Linear Elements", () => {
// delete 3rd point // delete 3rd point
deletePoint(points[2]); deletePoint(points[2]);
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
expect(renderInteractiveScene).toHaveBeenCalledTimes(21); expect(renderInteractiveScene).toHaveBeenCalledTimes(20);
expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(renderStaticScene).toHaveBeenCalledTimes(9);
const newMidPoints = LinearElementEditor.getEditorMidPoints( const newMidPoints = LinearElementEditor.getEditorMidPoints(
@@ -566,7 +566,7 @@ describe("Test Linear Elements", () => {
lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta, lastSegmentMidpoint[1] + delta,
]); ]);
expect(renderInteractiveScene).toHaveBeenCalledTimes(21); expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(renderStaticScene).toHaveBeenCalledTimes(9);
expect(line.points.length).toEqual(5); expect(line.points.length).toEqual(5);
+3 -5
View File
@@ -246,16 +246,14 @@ export interface AppState {
openSidebar: { name: SidebarName; tab?: SidebarTabName } | null; openSidebar: { name: SidebarName; tab?: SidebarTabName } | null;
openDialog: openDialog:
| null | null
| { name: "imageExport" | "help" | "jsonExport" } | { name: "imageExport" | "help" | "jsonExport" | "mermaid" }
| { | {
name: "settings"; name: "magicSettings";
source: source:
| "tool" // when magicframe tool is selected | "tool" // when magicframe tool is selected
| "generation" // when magicframe generate button is clicked | "generation" // when magicframe generate button is clicked
| "settings"; // when AI settings dialog is explicitly invoked | "settings"; // when AI settings dialog is explicitly invoked
tab: "text-to-diagram" | "diagram-to-code"; };
}
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" };
/** /**
* Reflects user preference for whether the default sidebar should be docked. * Reflects user preference for whether the default sidebar should be docked.
* *
-4
View File
@@ -925,7 +925,3 @@ export const isMemberOf = <T extends string>(
}; };
export const cloneJSON = <T>(obj: T): T => JSON.parse(JSON.stringify(obj)); 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
View File
@@ -17,7 +17,6 @@ interface ImportMetaEnv {
// set this only if using the collaboration workflow we use on excalidraw.com // set this only if using the collaboration workflow we use on excalidraw.com
VITE_APP_PORTAL_URL: string; VITE_APP_PORTAL_URL: string;
VITE_APP_AI_BACKEND: string;
VITE_APP_FIREBASE_CONFIG: string; VITE_APP_FIREBASE_CONFIG: string;