Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
347177f873 |
@@ -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!
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -104,7 +102,6 @@ import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog";
|
|||||||
import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState";
|
import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState";
|
||||||
import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm";
|
import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm";
|
||||||
import Trans from "../src/components/Trans";
|
import Trans from "../src/components/Trans";
|
||||||
import { drawingIcon } from "../src/components/icons";
|
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -776,65 +773,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
)}
|
)}
|
||||||
</OverwriteConfirmDialog>
|
</OverwriteConfirmDialog>
|
||||||
<AppFooter />
|
<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 && (
|
{isCollaborating && isOffline && (
|
||||||
<div className="collab-offline-warning">
|
<div className="collab-offline-warning">
|
||||||
{t("alerts.collabOfflineWarning")}
|
{t("alerts.collabOfflineWarning")}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
+14
-18
@@ -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 {
|
||||||
@@ -1434,6 +1435,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 +1704,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 +1719,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 +1761,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 +1771,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 +1783,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 +1875,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 +1887,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 +1905,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) {
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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 }, () => {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,97 +56,75 @@ 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> */}
|
OpenAI.
|
||||||
<TTDDialogTabs dialog="settings" tab={appState.openDialog.tab}>
|
</Paragraph>
|
||||||
{/* <TTDDialogTabTriggers>
|
<Paragraph>
|
||||||
<TTDDialogTabTrigger tab="text-to-diagram">
|
While the OpenAI API is in beta, its use is strictly limited — as such
|
||||||
<InlineIcon icon={brainIcon} /> Text to diagram
|
we require you use your own API key. You can create an{" "}
|
||||||
</TTDDialogTabTrigger>
|
<a
|
||||||
<TTDDialogTabTrigger tab="diagram-to-code">
|
href="https://platform.openai.com/login?launch"
|
||||||
<InlineIcon icon={MagicIcon} /> Wireframe to code
|
rel="noopener noreferrer"
|
||||||
</TTDDialogTabTrigger>
|
target="_blank"
|
||||||
</TTDDialogTabTriggers> */}
|
|
||||||
{/* <TTDDialogTab className="ttd-dialog-content" tab="text-to-diagram">
|
|
||||||
TODO
|
|
||||||
</TTDDialogTab> */}
|
|
||||||
<TTDDialogTab
|
|
||||||
// className="ttd-dialog-content"
|
|
||||||
tab="diagram-to-code"
|
|
||||||
>
|
>
|
||||||
<Paragraph>
|
OpenAI account
|
||||||
For the diagram-to-code feature we use{" "}
|
</a>
|
||||||
<InlineIcon icon={OpenAIIcon} />
|
, add a small credit (5 USD minimum), and{" "}
|
||||||
OpenAI.
|
<a
|
||||||
</Paragraph>
|
href="https://platform.openai.com/api-keys"
|
||||||
<Paragraph>
|
rel="noopener noreferrer"
|
||||||
While the OpenAI API is in beta, its use is strictly limited — as
|
target="_blank"
|
||||||
such we require you use your own API key. You can create an{" "}
|
>
|
||||||
<a
|
generate your own API key
|
||||||
href="https://platform.openai.com/login?launch"
|
</a>
|
||||||
rel="noopener noreferrer"
|
.
|
||||||
target="_blank"
|
</Paragraph>
|
||||||
>
|
<Paragraph>
|
||||||
OpenAI account
|
Your OpenAI key does not leave the browser, and you can also set your
|
||||||
</a>
|
own limit in your OpenAI account dashboard if needed.
|
||||||
, add a small credit (5 USD minimum), and{" "}
|
</Paragraph>
|
||||||
<a
|
<TextField
|
||||||
href="https://platform.openai.com/api-keys"
|
isRedacted
|
||||||
rel="noopener noreferrer"
|
value={keyInputValue}
|
||||||
target="_blank"
|
placeholder="Paste your API key here"
|
||||||
>
|
label="OpenAI API key"
|
||||||
generate your own API key
|
onChange={(value) => {
|
||||||
</a>
|
setKeyInputValue(value);
|
||||||
.
|
props.onChange(value.trim(), shouldPersist);
|
||||||
</Paragraph>
|
}}
|
||||||
<Paragraph>
|
selectOnRender
|
||||||
Your OpenAI key does not leave the browser, and you can also set
|
onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()}
|
||||||
your own limit in your OpenAI account dashboard if needed.
|
/>
|
||||||
</Paragraph>
|
<Paragraph>
|
||||||
<TextField
|
By default, your API token is not persisted anywhere so you'll need to
|
||||||
isRedacted
|
insert it again after reload. But, you can persist locally in your
|
||||||
value={keyInputValue}
|
browser below.
|
||||||
placeholder="Paste your API key here"
|
</Paragraph>
|
||||||
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}>
|
<CheckboxItem checked={shouldPersist} onChange={setShouldPersist}>
|
||||||
Persist API key in browser storage
|
Persist API key in browser storage
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
|
|
||||||
<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
|
||||||
className="MagicSettings__confirm"
|
className="MagicSettings__confirm"
|
||||||
size="large"
|
size="large"
|
||||||
label="Confirm"
|
label="Confirm"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
/>
|
/>
|
||||||
</TTDDialogTab>
|
|
||||||
</TTDDialogTabs>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
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: 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 {
|
&__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";
|
||||||
|
|||||||
@@ -1742,26 +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,
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
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
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
|
||||||
export const normalizeLink = (link: string) => {
|
export const normalizeLink = (link: string) => {
|
||||||
link = link.trim();
|
|
||||||
if (!link) {
|
if (!link) {
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
|
link = link.trim();
|
||||||
return sanitizeUrl(link);
|
return sanitizeUrl(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
-4
@@ -132,10 +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",
|
|
||||||
"textToDrawing": "Text to drawing"
|
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "No items added yet...",
|
"noItems": "No items added yet...",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.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)
|
## 0.17.0 (2023-11-14)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@excalidraw/excalidraw",
|
"name": "@excalidraw/excalidraw",
|
||||||
"version": "0.17.1",
|
"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);
|
||||||
|
|||||||
@@ -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] -->|Get money| B(Go shopping)
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
B --> C{Let me think}
|
B --> C{Let me think}
|
||||||
C -->|One| D[Laptop]
|
C -->|One| D[Laptop]
|
||||||
C -->|Two| E[iPhone]
|
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>"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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
@@ -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" | "text-to-drawing" };
|
|
||||||
/**
|
/**
|
||||||
* Reflects user preference for whether the default sidebar should be docked.
|
* 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 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);
|
|
||||||
};
|
|
||||||
|
|||||||
Vendored
-1
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user