Files
excalidraw/packages/excalidraw/components/TTDDialog/TextToDiagram.tsx
T
Tamas Landdwelle a0b98a944f feat: TextToDiagram v2 (#10530)
* feat: introducing TextToDiagram v2 feature

* fix: eslint issue

* debug mermaid bundle size

* tests: covering the utils

* fix: import mock chunks dynamically to shrink the bundle size

* fix: removing replay feature

* fix: removing unused prop

* fix: bumping workbox cache limit

* snapshots + yarn.lock

* bump mermaid-to-excalidraw@2 and split into its own chunk

* bump node@20

* css tweaks

* move files around & rewrite stream chunk schema

* random naming & file structure refactor + some tweaks

* fix preview theme

* support custom warning renderer

* label and css fix

* fix and rwrite 429 handling

* fix label

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2026-01-15 19:15:41 +01:00

245 lines
6.8 KiB
TypeScript

import { useRef } from "react";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import { useAtom, useAtomValue } from "../../editor-jotai";
import { useApp, useExcalidrawSetAppState } from "../App";
import { useChatAgent } from "./Chat";
import {
convertMermaidToExcalidraw,
insertToEditor,
saveMermaidDataToStorage,
} from "./common";
import { errorAtom, chatHistoryAtom, showPreviewAtom } from "./TTDContext";
import { useTTDChatStorage } from "./useTTDChatStorage";
import { useMermaidRenderer } from "./hooks/useMermaidRenderer";
import { useTextGeneration } from "./hooks/useTextGeneration";
import { useChatManagement } from "./hooks/useChatManagement";
import { TTDChatPanel } from "./Chat/TTDChatPanel";
import { TTDPreviewPanel } from "./TTDPreviewPanel";
import { getLastAssistantMessage } from "./utils/chat";
import type { BinaryFiles } from "../../types";
import type { MermaidToExcalidrawLibProps, TChat, TTTDDialog } from "./types";
const TextToDiagramContent = ({
mermaidToExcalidrawLib,
onTextSubmit,
renderWarning,
}: {
mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
onTextSubmit: (
props: TTTDDialog.OnTextSubmitProps,
) => Promise<TTTDDialog.OnTextSubmitRetValue>;
renderWarning?: TTTDDialog.renderWarning;
}) => {
const app = useApp();
const setAppState = useExcalidrawSetAppState();
const canvasRef = useRef<HTMLDivElement | null>(null);
const [error, setError] = useAtom(errorAtom);
const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom);
const showPreview = useAtomValue(showPreviewAtom);
const { savedChats } = useTTDChatStorage();
const lastAssistantMessage = getLastAssistantMessage(chatHistory);
const { setLastRetryAttempt } = useChatAgent();
const { data } = useMermaidRenderer({
canvasRef,
mermaidToExcalidrawLib,
});
const { onGenerate, handleAbort } = useTextGeneration({
onTextSubmit,
});
const {
isMenuOpen,
onRestoreChat,
handleDeleteChat,
handleNewChat,
handleMenuToggle,
handleMenuClose,
} = useChatManagement();
const onViewAsMermaid = () => {
if (typeof lastAssistantMessage?.content === "string") {
saveMermaidDataToStorage(lastAssistantMessage.content);
setAppState({
openDialog: { name: "ttd", tab: "mermaid" },
});
}
};
const handleMermaidTabClick = (message: TChat.ChatMessage) => {
const mermaidContent = message.content || "";
if (mermaidContent) {
saveMermaidDataToStorage(mermaidContent);
setAppState({
openDialog: { name: "ttd", tab: "mermaid" },
});
}
};
const handleInsertMessage = async (message: TChat.ChatMessage) => {
const mermaidContent = message.content || "";
if (!mermaidContent.trim() || !mermaidToExcalidrawLib.loaded) {
return;
}
const tempDataRef = {
current: {
elements: [] as readonly NonDeletedExcalidrawElement[],
files: null as BinaryFiles | null,
},
};
const result = await convertMermaidToExcalidraw({
canvasRef,
data: tempDataRef,
mermaidToExcalidrawLib,
setError,
mermaidDefinition: mermaidContent,
theme: app.state.theme,
});
if (result.success) {
insertToEditor({
app,
data: tempDataRef,
text: mermaidContent,
shouldSaveMermaidDataToStorage: true,
});
}
};
const handleAiRepairClick = async (message: TChat.ChatMessage) => {
const mermaidContent = message.content || "";
const errorMessage = message.error || "";
if (!mermaidContent) {
return;
}
const repairPrompt = `Fix the error in this Mermaid diagram. The diagram is:\n\n\`\`\`mermaid\n${mermaidContent}\n\`\`\`\n\nThe exception/error is: ${errorMessage}\n\nPlease fix the Mermaid syntax and regenerate a valid diagram.`;
await onGenerate(repairPrompt, true);
};
const handleRetry = async (message: TChat.ChatMessage) => {
const messageIndex = chatHistory.messages.findIndex(
(msg) => msg.id === message.id,
);
if (messageIndex > 0) {
const previousMessage = chatHistory.messages[messageIndex - 1];
if (previousMessage.type === "user" && previousMessage.content) {
setLastRetryAttempt();
await onGenerate(previousMessage.content, true);
}
}
};
const handleInsertToEditor = () => {
insertToEditor({ app, data });
};
const handleDeleteMessage = (messageId: string) => {
const assistantMessageIndex = chatHistory.messages.findIndex(
(msg) => msg.id === messageId && msg.type === "assistant",
);
const remainingMessages = chatHistory.messages.slice(
0,
assistantMessageIndex - 1,
);
setChatHistory({
...chatHistory,
messages: remainingMessages,
});
};
const handlePromptChange = (newPrompt: string) => {
setChatHistory((prev) => ({
...prev,
currentPrompt: newPrompt,
}));
};
return (
<div
className={`ttd-dialog-layout ${
showPreview
? "ttd-dialog-layout--split"
: "ttd-dialog-layout--chat-only"
}`}
>
<TTDChatPanel
chatId={chatHistory.id}
messages={chatHistory.messages}
currentPrompt={chatHistory.currentPrompt}
onPromptChange={handlePromptChange}
onSendMessage={onGenerate}
isGenerating={lastAssistantMessage?.isGenerating ?? false}
generatedResponse={lastAssistantMessage?.content}
isMenuOpen={isMenuOpen}
onMenuToggle={handleMenuToggle}
onMenuClose={handleMenuClose}
onNewChat={handleNewChat}
onRestoreChat={onRestoreChat}
onDeleteChat={handleDeleteChat}
savedChats={savedChats}
activeSessionId={chatHistory.id}
onAbort={handleAbort}
onMermaidTabClick={handleMermaidTabClick}
onAiRepairClick={handleAiRepairClick}
onDeleteMessage={handleDeleteMessage}
onInsertMessage={handleInsertMessage}
onRetry={handleRetry}
onViewAsMermaid={onViewAsMermaid}
renderWarning={renderWarning}
/>
{showPreview && (
<TTDPreviewPanel
canvasRef={canvasRef}
hideErrorDetails={lastAssistantMessage?.errorType === "parse"}
error={error}
loaded={mermaidToExcalidrawLib.loaded}
onInsert={handleInsertToEditor}
/>
)}
</div>
);
};
export const TextToDiagram = ({
mermaidToExcalidrawLib,
onTextSubmit,
renderWarning,
}: {
mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
onTextSubmit(
props: TTTDDialog.OnTextSubmitProps,
): Promise<TTTDDialog.OnTextSubmitRetValue>;
renderWarning?: TTTDDialog.renderWarning;
}) => {
return (
<TextToDiagramContent
mermaidToExcalidrawLib={mermaidToExcalidrawLib}
onTextSubmit={onTextSubmit}
renderWarning={renderWarning}
/>
);
};
export default TextToDiagram;