mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-09 09:09:53 +02:00
Compare commits
2 Commits
are/tte
...
zsviczian-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4b1c7fd20c | ||
![]() |
606c647e3d |
@@ -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,
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
:::
|
:::
|
||||||
|
@@ -104,7 +104,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();
|
||||||
|
|
||||||
@@ -777,10 +776,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
</OverwriteConfirmDialog>
|
</OverwriteConfirmDialog>
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
<TTDDialog
|
<TTDDialog
|
||||||
onTextSubmit={async (input, type) => {
|
onTextSubmit={async (input) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${import.meta.env.VITE_APP_AI_BACKEND}/v1/ai/${type}/generate`,
|
`${
|
||||||
|
import.meta.env.VITE_APP_AI_BACKEND
|
||||||
|
}/v1/ai/text-to-diagram/generate`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -832,9 +833,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TTDDialogTrigger />
|
<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")}
|
||||||
|
@@ -3543,8 +3543,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// Input handling
|
// Input handling
|
||||||
private onKeyDown = withBatchedUpdates(
|
private onKeyDown = withBatchedUpdates(
|
||||||
(event: React.KeyboardEvent | KeyboardEvent) => {
|
(event: React.KeyboardEvent | KeyboardEvent) => {
|
||||||
|
if (this.state.resizingElement && event.shiftKey) {
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// normalize `event.key` when CapsLock is pressed #2372
|
// normalize `event.key` when CapsLock is pressed #2372
|
||||||
|
|
||||||
if (
|
if (
|
||||||
"Proxy" in window &&
|
"Proxy" in window &&
|
||||||
((!event.shiftKey && /^[A-Z]$/.test(event.key)) ||
|
((!event.shiftKey && /^[A-Z]$/.test(event.key)) ||
|
||||||
|
@@ -111,7 +111,7 @@ const MermaidToExcalidraw = ({
|
|||||||
action: () => {
|
action: () => {
|
||||||
insertToEditor({
|
insertToEditor({
|
||||||
app,
|
app,
|
||||||
data: data.current,
|
data,
|
||||||
text,
|
text,
|
||||||
shouldSaveMermaidDataToStorage: true,
|
shouldSaveMermaidDataToStorage: true,
|
||||||
});
|
});
|
||||||
|
@@ -2,20 +2,58 @@ import { Dialog } from "../Dialog";
|
|||||||
import { useApp } from "../App";
|
import { useApp } from "../App";
|
||||||
import MermaidToExcalidraw from "./MermaidToExcalidraw";
|
import MermaidToExcalidraw from "./MermaidToExcalidraw";
|
||||||
import TTDDialogTabs from "./TTDDialogTabs";
|
import TTDDialogTabs from "./TTDDialogTabs";
|
||||||
import { useEffect, useState } from "react";
|
import { ChangeEventHandler, useEffect, useRef, useState } from "react";
|
||||||
import { useUIAppState } from "../../context/ui-appState";
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
import { withInternalFallback } from "../hoc/withInternalFallback";
|
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||||
import { TTDDialogTabTriggers } from "./TTDDialogTabTriggers";
|
import { TTDDialogTabTriggers } from "./TTDDialogTabTriggers";
|
||||||
import { TTDDialogTabTrigger } from "./TTDDialogTabTrigger";
|
import { TTDDialogTabTrigger } from "./TTDDialogTabTrigger";
|
||||||
import { TTDDialogTab } from "./TTDDialogTab";
|
import { TTDDialogTab } from "./TTDDialogTab";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { CommonDialogProps, MermaidToExcalidrawLibProps } from "./common";
|
import { TTDDialogInput } from "./TTDDialogInput";
|
||||||
|
import { TTDDialogOutput } from "./TTDDialogOutput";
|
||||||
|
import { TTDDialogPanel } from "./TTDDialogPanel";
|
||||||
|
import { TTDDialogPanels } from "./TTDDialogPanels";
|
||||||
|
import {
|
||||||
|
MermaidToExcalidrawLibProps,
|
||||||
|
convertMermaidToExcalidraw,
|
||||||
|
insertToEditor,
|
||||||
|
saveMermaidDataToStorage,
|
||||||
|
} from "./common";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
|
import { BinaryFiles } from "../../types";
|
||||||
|
import { ArrowRightIcon } from "../icons";
|
||||||
|
|
||||||
import "./TTDDialog.scss";
|
import "./TTDDialog.scss";
|
||||||
import { TextToDiagram } from "./TextToDiagram";
|
import { isFiniteNumber } from "../../utils";
|
||||||
import { TextToDrawing } from "./TextToDrawing";
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { trackEvent } from "../../analytics";
|
||||||
|
|
||||||
export const TTDDialog = (props: CommonDialogProps | { __fallback: true }) => {
|
const MIN_PROMPT_LENGTH = 3;
|
||||||
|
const MAX_PROMPT_LENGTH = 1000;
|
||||||
|
|
||||||
|
const rateLimitsAtom = atom<{
|
||||||
|
rateLimit: number;
|
||||||
|
rateLimitRemaining: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
type OnTestSubmitRetValue = {
|
||||||
|
rateLimit?: number | null;
|
||||||
|
rateLimitRemaining?: number | null;
|
||||||
|
} & (
|
||||||
|
| { generatedResponse: string | undefined; error?: null | undefined }
|
||||||
|
| {
|
||||||
|
error: Error;
|
||||||
|
generatedResponse?: null | undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TTDDialog = (
|
||||||
|
props:
|
||||||
|
| {
|
||||||
|
onTextSubmit(value: string): Promise<OnTestSubmitRetValue>;
|
||||||
|
}
|
||||||
|
| { __fallback: true },
|
||||||
|
) => {
|
||||||
const appState = useUIAppState();
|
const appState = useUIAppState();
|
||||||
|
|
||||||
if (appState.openDialog?.name !== "ttd") {
|
if (appState.openDialog?.name !== "ttd") {
|
||||||
@@ -34,10 +72,118 @@ export const TTDDialogBase = withInternalFallback(
|
|||||||
tab,
|
tab,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
tab: "text-to-diagram" | "mermaid" | "text-to-drawing";
|
tab: "text-to-diagram" | "mermaid";
|
||||||
} & (CommonDialogProps | { __fallback: true })) => {
|
} & (
|
||||||
|
| {
|
||||||
|
onTextSubmit(value: string): Promise<OnTestSubmitRetValue>;
|
||||||
|
}
|
||||||
|
| { __fallback: true }
|
||||||
|
)) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
|
||||||
|
const someRandomDivRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [text, setText] = useState("");
|
||||||
|
|
||||||
|
const prompt = text.trim();
|
||||||
|
|
||||||
|
const handleTextChange: ChangeEventHandler<HTMLTextAreaElement> = (
|
||||||
|
event,
|
||||||
|
) => {
|
||||||
|
setText(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [onTextSubmitInProgess, setOnTextSubmitInProgess] = useState(false);
|
||||||
|
const [rateLimits, setRateLimits] = useAtom(rateLimitsAtom);
|
||||||
|
|
||||||
|
const onGenerate = async () => {
|
||||||
|
if (
|
||||||
|
prompt.length > MAX_PROMPT_LENGTH ||
|
||||||
|
prompt.length < MIN_PROMPT_LENGTH ||
|
||||||
|
onTextSubmitInProgess ||
|
||||||
|
rateLimits?.rateLimitRemaining === 0 ||
|
||||||
|
// means this is not a text-to-diagram dialog (needed for TS only)
|
||||||
|
"__fallback" in rest
|
||||||
|
) {
|
||||||
|
if (prompt.length < MIN_PROMPT_LENGTH) {
|
||||||
|
setError(
|
||||||
|
new Error(
|
||||||
|
`Prompt is too short (min ${MIN_PROMPT_LENGTH} characters)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (prompt.length > MAX_PROMPT_LENGTH) {
|
||||||
|
setError(
|
||||||
|
new Error(
|
||||||
|
`Prompt is too long (max ${MAX_PROMPT_LENGTH} characters)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setOnTextSubmitInProgess(true);
|
||||||
|
|
||||||
|
trackEvent("ai", "generate", "ttd");
|
||||||
|
|
||||||
|
const { generatedResponse, error, rateLimit, rateLimitRemaining } =
|
||||||
|
await rest.onTextSubmit(prompt);
|
||||||
|
|
||||||
|
if (isFiniteNumber(rateLimit) && isFiniteNumber(rateLimitRemaining)) {
|
||||||
|
setRateLimits({ rateLimit, rateLimitRemaining });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!generatedResponse) {
|
||||||
|
setError(new Error("Generation failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await convertMermaidToExcalidraw({
|
||||||
|
canvasRef: someRandomDivRef,
|
||||||
|
data,
|
||||||
|
mermaidToExcalidrawLib,
|
||||||
|
setError,
|
||||||
|
mermaidDefinition: generatedResponse,
|
||||||
|
});
|
||||||
|
trackEvent("ai", "mermaid parse success", "ttd");
|
||||||
|
saveMermaidDataToStorage(generatedResponse);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.info(
|
||||||
|
`%cTTD mermaid render errror: ${error.message}`,
|
||||||
|
"color: red",
|
||||||
|
);
|
||||||
|
console.info(
|
||||||
|
`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\nTTD mermaid definition render errror: ${error.message}`,
|
||||||
|
"color: yellow",
|
||||||
|
);
|
||||||
|
trackEvent("ai", "mermaid parse failed", "ttd");
|
||||||
|
setError(
|
||||||
|
new Error(
|
||||||
|
"Generated an invalid diagram :(. You may also try a different prompt.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
let message: string | undefined = error.message;
|
||||||
|
if (!message || message === "Failed to fetch") {
|
||||||
|
message = "Request failed";
|
||||||
|
}
|
||||||
|
setError(new Error(message));
|
||||||
|
} finally {
|
||||||
|
setOnTextSubmitInProgess(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refOnGenerate = useRef(onGenerate);
|
||||||
|
refOnGenerate.current = onGenerate;
|
||||||
|
|
||||||
const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] =
|
const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] =
|
||||||
useState<MermaidToExcalidrawLibProps>({
|
useState<MermaidToExcalidrawLibProps>({
|
||||||
loaded: false,
|
loaded: false,
|
||||||
@@ -54,6 +200,13 @@ export const TTDDialogBase = withInternalFallback(
|
|||||||
fn();
|
fn();
|
||||||
}, [mermaidToExcalidrawLib.api]);
|
}, [mermaidToExcalidrawLib.api]);
|
||||||
|
|
||||||
|
const data = useRef<{
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
files: BinaryFiles | null;
|
||||||
|
}>({ elements: [], files: null });
|
||||||
|
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
className="ttd-dialog"
|
className="ttd-dialog"
|
||||||
@@ -90,26 +243,6 @@ export const TTDDialogBase = withInternalFallback(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TTDDialogTabTrigger>
|
</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>
|
<TTDDialogTabTrigger tab="mermaid">Mermaid</TTDDialogTabTrigger>
|
||||||
</TTDDialogTabTriggers>
|
</TTDDialogTabTriggers>
|
||||||
)}
|
)}
|
||||||
@@ -121,15 +254,93 @@ export const TTDDialogBase = withInternalFallback(
|
|||||||
</TTDDialogTab>
|
</TTDDialogTab>
|
||||||
{!("__fallback" in rest) && (
|
{!("__fallback" in rest) && (
|
||||||
<TTDDialogTab className="ttd-dialog-content" tab="text-to-diagram">
|
<TTDDialogTab className="ttd-dialog-content" tab="text-to-diagram">
|
||||||
<TextToDiagram
|
<div className="ttd-dialog-desc">
|
||||||
onTextSubmit={rest.onTextSubmit}
|
Currently we use Mermaid as a middle step, so you'll get best
|
||||||
mermaidToExcalidrawLib={mermaidToExcalidrawLib}
|
results if you describe a diagram, workflow, flow chart, and
|
||||||
/>
|
similar.
|
||||||
</TTDDialogTab>
|
</div>
|
||||||
)}
|
<TTDDialogPanels>
|
||||||
{!("__fallback" in rest) && (
|
<TTDDialogPanel
|
||||||
<TTDDialogTab className="ttd-dialog-content" tab="text-to-drawing">
|
label={t("labels.prompt")}
|
||||||
<TextToDrawing onTextSubmit={rest.onTextSubmit} />
|
panelAction={{
|
||||||
|
action: onGenerate,
|
||||||
|
label: "Generate",
|
||||||
|
icon: ArrowRightIcon,
|
||||||
|
}}
|
||||||
|
onTextSubmitInProgess={onTextSubmitInProgess}
|
||||||
|
panelActionDisabled={
|
||||||
|
prompt.length > MAX_PROMPT_LENGTH ||
|
||||||
|
rateLimits?.rateLimitRemaining === 0
|
||||||
|
}
|
||||||
|
renderTopRight={() => {
|
||||||
|
if (!rateLimits) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="ttd-dialog-rate-limit"
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
marginLeft: "auto",
|
||||||
|
color:
|
||||||
|
rateLimits.rateLimitRemaining === 0
|
||||||
|
? "var(--color-danger)"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rateLimits.rateLimitRemaining} requests left today
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderBottomRight={() => {
|
||||||
|
const ratio = prompt.length / MAX_PROMPT_LENGTH;
|
||||||
|
if (ratio > 0.8) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
color:
|
||||||
|
ratio > 1 ? "var(--color-danger)" : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Length: {prompt.length}/{MAX_PROMPT_LENGTH}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TTDDialogInput
|
||||||
|
onChange={handleTextChange}
|
||||||
|
input={text}
|
||||||
|
placeholder={"Describe what you want to see..."}
|
||||||
|
onKeyboardSubmit={() => {
|
||||||
|
refOnGenerate.current();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TTDDialogPanel>
|
||||||
|
<TTDDialogPanel
|
||||||
|
label="Preview"
|
||||||
|
panelAction={{
|
||||||
|
action: () => {
|
||||||
|
console.info("Panel action clicked");
|
||||||
|
insertToEditor({ app, data });
|
||||||
|
},
|
||||||
|
label: "Insert",
|
||||||
|
icon: ArrowRightIcon,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TTDDialogOutput
|
||||||
|
canvasRef={someRandomDivRef}
|
||||||
|
error={error}
|
||||||
|
loaded={mermaidToExcalidrawLib.loaded}
|
||||||
|
/>
|
||||||
|
</TTDDialogPanel>
|
||||||
|
</TTDDialogPanels>
|
||||||
</TTDDialogTab>
|
</TTDDialogTab>
|
||||||
)}
|
)}
|
||||||
</TTDDialogTabs>
|
</TTDDialogTabs>
|
||||||
|
@@ -7,11 +7,8 @@ const TTDDialogTabs = (
|
|||||||
props: {
|
props: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
} & (
|
} & (
|
||||||
| { dialog: "ttd"; tab: "text-to-diagram" | "mermaid" | "text-to-drawing" }
|
| { dialog: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
||||||
| {
|
| { dialog: "settings"; tab: "text-to-diagram" | "diagram-to-code" }
|
||||||
dialog: "settings";
|
|
||||||
tab: "text-to-diagram" | "diagram-to-code";
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
) => {
|
) => {
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
@@ -49,7 +46,7 @@ const TTDDialogTabs = (
|
|||||||
});
|
});
|
||||||
} else if (
|
} else if (
|
||||||
props.dialog === "ttd" &&
|
props.dialog === "ttd" &&
|
||||||
isMemberOf(["text-to-diagram", "mermaid", "text-to-drawing"], tab)
|
isMemberOf(["text-to-diagram", "mermaid"], tab)
|
||||||
) {
|
) {
|
||||||
setAppState({
|
setAppState({
|
||||||
openDialog: { name: props.dialog, tab },
|
openDialog: { name: props.dialog, tab },
|
||||||
|
@@ -9,11 +9,9 @@ import { trackEvent } from "../../analytics";
|
|||||||
export const TTDDialogTrigger = ({
|
export const TTDDialogTrigger = ({
|
||||||
children,
|
children,
|
||||||
icon,
|
icon,
|
||||||
tab,
|
|
||||||
}: {
|
}: {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
tab?: "mermaid" | "text-to-diagram" | "text-to-drawing";
|
|
||||||
}) => {
|
}) => {
|
||||||
const { TTDDialogTriggerTunnel } = useTunnels();
|
const { TTDDialogTriggerTunnel } = useTunnels();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
@@ -23,9 +21,7 @@ export const TTDDialogTrigger = ({
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
trackEvent("ai", "dialog open", "ttd");
|
trackEvent("ai", "dialog open", "ttd");
|
||||||
setAppState({
|
setAppState({ openDialog: { name: "ttd", tab: "text-to-diagram" } });
|
||||||
openDialog: { name: "ttd", tab: tab ?? "text-to-diagram" },
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
icon={icon ?? brainIcon}
|
icon={icon ?? brainIcon}
|
||||||
>
|
>
|
||||||
|
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -8,9 +8,8 @@ import {
|
|||||||
import { NonDeletedExcalidrawElement } from "../../element/types";
|
import { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
import { AppClassProperties, BinaryFiles } from "../../types";
|
import { AppClassProperties, BinaryFiles } from "../../types";
|
||||||
import { canvasToBlob } from "../../data/blob";
|
import { canvasToBlob } from "../../data/blob";
|
||||||
import { atom } from "jotai";
|
|
||||||
|
|
||||||
export const resetPreview = ({
|
const resetPreview = ({
|
||||||
canvasRef,
|
canvasRef,
|
||||||
setError,
|
setError,
|
||||||
}: {
|
}: {
|
||||||
@@ -31,26 +30,6 @@ export const resetPreview = ({
|
|||||||
canvasNode.replaceChildren();
|
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 {
|
export interface MermaidToExcalidrawLibProps {
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
api: Promise<{
|
api: Promise<{
|
||||||
@@ -158,14 +137,14 @@ export const insertToEditor = ({
|
|||||||
shouldSaveMermaidDataToStorage,
|
shouldSaveMermaidDataToStorage,
|
||||||
}: {
|
}: {
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
data: {
|
data: React.MutableRefObject<{
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
};
|
}>;
|
||||||
text?: string;
|
text?: string;
|
||||||
shouldSaveMermaidDataToStorage?: boolean;
|
shouldSaveMermaidDataToStorage?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { elements: newElements, files } = data;
|
const { elements: newElements, files } = data.current;
|
||||||
|
|
||||||
if (!newElements.length) {
|
if (!newElements.length) {
|
||||||
return;
|
return;
|
||||||
@@ -183,11 +162,3 @@ export const insertToEditor = ({
|
|||||||
saveMermaidDataToStorage(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);
|
|
||||||
|
@@ -1755,13 +1755,3 @@ export const brainIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
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,
|
|
||||||
);
|
|
||||||
|
@@ -134,8 +134,7 @@
|
|||||||
"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",
|
"textToDiagram": "Text to diagram",
|
||||||
"prompt": "Prompt",
|
"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
|
||||||
|
@@ -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);
|
||||||
|
@@ -255,7 +255,7 @@ export interface AppState {
|
|||||||
| "settings"; // when AI settings dialog is explicitly invoked
|
| "settings"; // when AI settings dialog is explicitly invoked
|
||||||
tab: "text-to-diagram" | "diagram-to-code";
|
tab: "text-to-diagram" | "diagram-to-code";
|
||||||
}
|
}
|
||||||
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" | "text-to-drawing" };
|
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" };
|
||||||
/**
|
/**
|
||||||
* Reflects user preference for whether the default sidebar should be docked.
|
* Reflects user preference for whether the default sidebar should be docked.
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user