diff --git a/src/components/App.tsx b/src/components/App.tsx index 2c3f7a6ef..cf68d516c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -397,6 +397,7 @@ import { COLOR_PALETTE } from "../colors"; import { ElementCanvasButton } from "./MagicButton"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; import { EditorLocalStorage } from "../data/EditorLocalStorage"; +import { TextToExcalidraw } from "./TextToExcalidraw/TextToExcalidraw"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); diff --git a/src/components/TextToExcalidraw/TextToExcalidraw.tsx b/src/components/TextToExcalidraw/TextToExcalidraw.tsx new file mode 100644 index 000000000..35cf88079 --- /dev/null +++ b/src/components/TextToExcalidraw/TextToExcalidraw.tsx @@ -0,0 +1,589 @@ +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 { + 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( + null, + ); + + const containerRef = useRef(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 ( +
+
+ 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?" + /> + +
+ +
+ + {isPanelOpen && ( +
+ {isLoading ? ( + "loading" + ) : ( +
+
+
+ + +
+ + +
+
+ +
+
+ )} +
+ )} +
+ ); +}; diff --git a/src/locales/en.json b/src/locales/en.json index 8b4a1df21..425893cd4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -251,7 +251,8 @@ "hand": "Hand (panning tool)", "extraTools": "More tools", "mermaidToExcalidraw": "Mermaid to Excalidraw", - "magicSettings": "AI settings" + "magicSettings": "AI settings", + "textToExcalidraw": "Text to Excalidraw" }, "headings": { "canvasActions": "Canvas actions", @@ -517,5 +518,10 @@ "description": "Currently only Flowcharts and Sequence Diagrams are supported. The other types will be rendered as image in Excalidraw.", "syntax": "Mermaid Syntax", "preview": "Preview" + }, + "textToExcalidraw": { + "title": "Text to Excalidraw", + "description": "Test", + "button": "Insert" } }