feat: add text-to-drawing
parent
0958241589
commit
530e92189f
@ -0,0 +1,228 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,248 @@
|
|||||||
|
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,589 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { t } from "../../i18n";
|
|
||||||
import { useApp } from "../App";
|
|
||||||
import { Dialog } from "../Dialog";
|
|
||||||
import { TextField } from "../TextField";
|
|
||||||
import Trans from "../Trans";
|
|
||||||
import {
|
|
||||||
CloseIcon,
|
|
||||||
RedoIcon,
|
|
||||||
ZoomInIcon,
|
|
||||||
ZoomOutIcon,
|
|
||||||
playerPlayIcon,
|
|
||||||
playerStopFilledIcon,
|
|
||||||
} from "../icons";
|
|
||||||
import { NonDeletedExcalidrawElement } from "../../element/types";
|
|
||||||
import { convertToExcalidrawElements } from "../../data/transform";
|
|
||||||
import { exportToCanvas } from "../../packages/utils";
|
|
||||||
import { DEFAULT_EXPORT_PADDING } from "../../constants";
|
|
||||||
import { canvasToBlob } from "../../data/blob";
|
|
||||||
|
|
||||||
const testResponse = `{
|
|
||||||
"error": false,
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"type": "ellipse",
|
|
||||||
"x": 200,
|
|
||||||
"y": 200,
|
|
||||||
"width": 100,
|
|
||||||
"height": 100,
|
|
||||||
"strokeColor": "transparent",
|
|
||||||
"backgroundColor": "yellow",
|
|
||||||
"strokeWidth": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 300,
|
|
||||||
"y": 250,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -70,
|
|
||||||
"height": 0,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 293.30127018922195,
|
|
||||||
"y": 275,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
60.62177826491069,
|
|
||||||
35
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -60.62177826491069,
|
|
||||||
"height": -35,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 275,
|
|
||||||
"y": 293.30127018922195,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35,
|
|
||||||
60.62177826491069
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -35,
|
|
||||||
"height": -60.62177826491069,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 250,
|
|
||||||
"y": 300,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
70
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": 0,
|
|
||||||
"height": -70,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 225,
|
|
||||||
"y": 293.30127018922195,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-34.99999999999997,
|
|
||||||
60.62177826491069
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -34.99999999999997,
|
|
||||||
"height": -60.62177826491069,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 206.69872981077805,
|
|
||||||
"y": 275,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-60.62177826491069,
|
|
||||||
35
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -60.62177826491069,
|
|
||||||
"height": -35,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 200,
|
|
||||||
"y": 250,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-70,
|
|
||||||
2.842170943040401e-14
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -70,
|
|
||||||
"height": -2.842170943040401e-14,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 206.69872981077805,
|
|
||||||
"y": 225,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-60.62177826491069,
|
|
||||||
-34.99999999999997
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -60.62177826491069,
|
|
||||||
"height": -34.99999999999997,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 224.99999999999997,
|
|
||||||
"y": 206.69872981077808,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-35.00000000000003,
|
|
||||||
-60.62177826491069
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -35.00000000000003,
|
|
||||||
"height": -60.62177826491069,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 250,
|
|
||||||
"y": 200,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-2.842170943040401e-14,
|
|
||||||
-70
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -2.842170943040401e-14,
|
|
||||||
"height": -70,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 275,
|
|
||||||
"y": 206.69872981077808,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35,
|
|
||||||
-60.621778264910716
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -35,
|
|
||||||
"height": -60.621778264910716,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "line",
|
|
||||||
"x": 293.3012701892219,
|
|
||||||
"y": 224.99999999999997,
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
60.621778264910745,
|
|
||||||
-35.00000000000003
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"width": -60.621778264910745,
|
|
||||||
"height": -35.00000000000003,
|
|
||||||
"strokeColor": "yellow",
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"strokeWidth": 5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`;
|
|
||||||
|
|
||||||
async function fetchData(
|
|
||||||
prompt: string,
|
|
||||||
): Promise<readonly NonDeletedExcalidrawElement[]> {
|
|
||||||
const response = await fetch(
|
|
||||||
`http://localhost:3015/v1/ai/text-to-excalidraw/generate`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ prompt }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
alert("Oops!");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertToExcalidrawElements(result.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TextToExcalidraw = () => {
|
|
||||||
const app = useApp();
|
|
||||||
|
|
||||||
const [prompt, setPrompt] = useState("");
|
|
||||||
const [isPanelOpen, setPanelOpen] = useState(false);
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
const [data, setData] = useState<
|
|
||||||
readonly NonDeletedExcalidrawElement[] | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const [previewCanvas, setPreviewCanvas] = useState<HTMLCanvasElement | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const onClose = () => {
|
|
||||||
app.setOpenDialog(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
|
||||||
setPanelOpen(true);
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const elements = await fetchData(prompt);
|
|
||||||
|
|
||||||
setData(elements);
|
|
||||||
|
|
||||||
const canvas = await exportToCanvas({
|
|
||||||
elements,
|
|
||||||
files: {},
|
|
||||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
|
||||||
});
|
|
||||||
|
|
||||||
await canvasToBlob(canvas);
|
|
||||||
|
|
||||||
setPreviewCanvas(canvas);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onInsert = async () => {
|
|
||||||
if (data) {
|
|
||||||
app.addElementsFromPasteOrLibrary({
|
|
||||||
elements: data,
|
|
||||||
files: {},
|
|
||||||
position: "center",
|
|
||||||
fitToContent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (containerRef.current && previewCanvas) {
|
|
||||||
containerRef.current.replaceChildren(previewCanvas);
|
|
||||||
}
|
|
||||||
}, [previewCanvas]);
|
|
||||||
|
|
||||||
// exportToCanvas([], {}, {}, {});
|
|
||||||
// exportToSvg([], {exportBackground}, {}, {})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "6.5rem",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "0.75rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="Island"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
gap: "0.75rem",
|
|
||||||
alignItems: "center",
|
|
||||||
height: 48,
|
|
||||||
padding: "0.5rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
value={prompt}
|
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
|
||||||
type="text"
|
|
||||||
style={{
|
|
||||||
flexGrow: 1,
|
|
||||||
height: "100%",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
border: 0,
|
|
||||||
outline: "none",
|
|
||||||
}}
|
|
||||||
placeholder="How can I help you today?"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
height: "100%",
|
|
||||||
border: "none",
|
|
||||||
background: "white",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
padding: 0,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{ width: "1.25rem", height: "1.25rem", color: "#1B1B1F" }}
|
|
||||||
>
|
|
||||||
{CloseIcon}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
style={{ background: "#D6D6D6", width: 1, height: "1.5rem" }}
|
|
||||||
></div>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
height: "100%",
|
|
||||||
border: "none",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
padding: 0,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#6965DB",
|
|
||||||
borderRadius: "0.5rem",
|
|
||||||
}}
|
|
||||||
onClick={onSubmit}
|
|
||||||
>
|
|
||||||
<div style={{ width: "1.25rem", height: "1.25rem", color: "white" }}>
|
|
||||||
{isLoading ? playerStopFilledIcon : playerPlayIcon}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isPanelOpen && (
|
|
||||||
<div
|
|
||||||
className="Island"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
height: 400,
|
|
||||||
boxSizing: "border-box",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
"loading"
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
borderTop: "1px solid #F0EFFF",
|
|
||||||
padding: "0.75rem",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: "0.75rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
width: 32,
|
|
||||||
height: "100%",
|
|
||||||
border: "none",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
padding: 0,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#F5F5F9",
|
|
||||||
borderRadius: "0.5rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "1.25rem",
|
|
||||||
height: "1.25rem",
|
|
||||||
color: "#1B1B1F",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{RedoIcon}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div style={{ width: 32, height: "100%", display: "flex" }}>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
width: 32,
|
|
||||||
height: "100%",
|
|
||||||
border: "none",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
padding: 0,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#F5F5F9",
|
|
||||||
borderRadius: "0.5rem 0 0 0.5rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "1.25rem",
|
|
||||||
height: "1.25rem",
|
|
||||||
color: "#1B1B1F",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ZoomOutIcon}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
width: 32,
|
|
||||||
height: "100%",
|
|
||||||
border: "none",
|
|
||||||
aspectRatio: "1/1",
|
|
||||||
padding: 0,
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#F5F5F9",
|
|
||||||
borderRadius: "0 0.5rem 0.5rem 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "1.25rem",
|
|
||||||
height: "1.25rem",
|
|
||||||
color: "#1B1B1F",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ZoomInIcon}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div style={{ flexGrow: 1 }}></div>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
height: "100%",
|
|
||||||
border: "none",
|
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "#6965DB",
|
|
||||||
borderRadius: "0.5rem",
|
|
||||||
color: "white",
|
|
||||||
}}
|
|
||||||
onClick={onInsert}
|
|
||||||
>
|
|
||||||
Insert into scene >
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
Loading…
Reference in New Issue