import { Dialog } from "../Dialog"; import { useApp, useExcalidrawSetAppState } from "../App"; import MermaidToExcalidraw from "./MermaidToExcalidraw"; import TTDDialogTabs from "./TTDDialogTabs"; import { ChangeEventHandler, useEffect, useRef, useState } from "react"; import { useUIAppState } from "../../context/ui-appState"; import { withInternalFallback } from "../hoc/withInternalFallback"; import { TTDDialogTabTriggers } from "./TTDDialogTabTriggers"; import { TTDDialogTabTrigger } from "./TTDDialogTabTrigger"; import { TTDDialogTab } from "./TTDDialogTab"; import { t } from "../../i18n"; import { TTDDialogInput } from "./TTDDialogInput"; import { TTDDialogOutput } from "./TTDDialogOutput"; import { TTDDialogPanel } from "./TTDDialogPanel"; import { TTDDialogPanels } from "./TTDDialogPanels"; import { MermaidToExcalidrawLibProps, convertMermaidToExcalidraw, insertToEditor, saveMermaidDataToStorage, } from "./common"; import { NonDeletedExcalidrawElement } from "../../element/types"; import { BinaryFiles } from "../../types"; import { ArrowRightIcon } from "../icons"; import "./TTDDialog.scss"; import { isFiniteNumber } from "../../utils"; import { atom, useAtom } from "jotai"; import { trackEvent } from "../../analytics"; import { InlineIcon } from "../InlineIcon"; import { TTDDialogSubmitShortcut } from "./TTDDialogSubmitShortcut"; const MIN_PROMPT_LENGTH = 3; const MAX_PROMPT_LENGTH = 1000; const rateLimitsAtom = atom<{ rateLimit: number; rateLimitRemaining: number; } | null>(null); const ttdGenerationAtom = atom<{ generatedResponse: string | null; prompt: string | null; } | 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; } | { __fallback: true }, ) => { const appState = useUIAppState(); if (appState.openDialog?.name !== "ttd") { return null; } return ; }; /** * Text to diagram (TTD) dialog */ export const TTDDialogBase = withInternalFallback( "TTDDialogBase", ({ tab, ...rest }: { tab: "text-to-diagram" | "mermaid"; } & ( | { onTextSubmit(value: string): Promise; } | { __fallback: true } )) => { const app = useApp(); const setAppState = useExcalidrawSetAppState(); const someRandomDivRef = useRef(null); const [ttdGeneration, setTtdGeneration] = useAtom(ttdGenerationAtom); const [text, setText] = useState(ttdGeneration?.prompt ?? ""); const prompt = text.trim(); const handleTextChange: ChangeEventHandler = ( event, ) => { setText(event.target.value); setTtdGeneration((s) => ({ generatedResponse: s?.generatedResponse ?? null, prompt: 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 (typeof generatedResponse === "string") { setTtdGeneration((s) => ({ generatedResponse, prompt: s?.prompt ?? null, })); } 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"); } catch (error: any) { console.info( `%cTTD mermaid render errror: ${error.message}`, "color: red", ); console.info( `>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\nTTD mermaid definition render errror: ${error.message}`, "color: yellow", ); trackEvent("ai", "mermaid parse failed", "ttd"); setError( new Error( "Generated an invalid diagram :(. You may also try a different prompt.", ), ); } } catch (error: any) { let message: string | undefined = error.message; if (!message || message === "Failed to fetch") { message = "Request failed"; } setError(new Error(message)); } finally { setOnTextSubmitInProgess(false); } }; const refOnGenerate = useRef(onGenerate); refOnGenerate.current = onGenerate; const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState({ loaded: false, api: import("@excalidraw/mermaid-to-excalidraw"), }); useEffect(() => { const fn = async () => { await mermaidToExcalidrawLib.api; setMermaidToExcalidrawLib((prev) => ({ ...prev, loaded: true })); }; fn(); }, [mermaidToExcalidrawLib.api]); const data = useRef<{ elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles | null; }>({ elements: [], files: null }); const [error, setError] = useState(null); return ( { app.setOpenDialog(null); }} size={1200} title={false} {...rest} autofocus={false} > {"__fallback" in rest && rest.__fallback ? (

{t("mermaid.title")}

) : (
{t("labels.textToDiagram")}
AI Beta
Mermaid
)} {!("__fallback" in rest) && (
Currently we use Mermaid as a middle step, so you'll get best results if you describe a diagram, workflow, flow chart, and similar.
MAX_PROMPT_LENGTH || rateLimits?.rateLimitRemaining === 0 } renderTopRight={() => { if (!rateLimits) { return null; } return (
{rateLimits.rateLimitRemaining} requests left today
); }} renderSubmitShortcut={() => } renderBottomRight={() => { if (typeof ttdGeneration?.generatedResponse === "string") { return (
{ if ( typeof ttdGeneration?.generatedResponse === "string" ) { saveMermaidDataToStorage( ttdGeneration.generatedResponse, ); setAppState({ openDialog: { name: "ttd", tab: "mermaid" }, }); } }} > View as Mermaid
); } const ratio = prompt.length / MAX_PROMPT_LENGTH; if (ratio > 0.8) { return (
1 ? "var(--color-danger)" : undefined, }} > Length: {prompt.length}/{MAX_PROMPT_LENGTH}
); } return null; }} > { refOnGenerate.current(); }} />
{ console.info("Panel action clicked"); insertToEditor({ app, data }); }, label: "Insert", icon: ArrowRightIcon, }} >
)}
); }, );