diff --git a/src/components/artifacts/TextRenderer.tsx b/src/components/artifacts/TextRenderer.tsx index 6b9ba818..1454783e 100644 --- a/src/components/artifacts/TextRenderer.tsx +++ b/src/components/artifacts/TextRenderer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { ArtifactMarkdownV3 } from "@/types"; import "@blocknote/core/fonts/inter.css"; import { @@ -13,11 +13,45 @@ import { CopyText } from "./components/CopyText"; import { getArtifactContent } from "@/contexts/utils"; import { useGraphContext } from "@/contexts/GraphContext"; import React from "react"; +import { TooltipIconButton } from "../ui/assistant-ui/tooltip-icon-button"; +import { Eye, EyeOff } from "lucide-react"; +import { motion } from "framer-motion"; +import { Textarea } from "../ui/textarea"; const cleanText = (text: string) => { return text.replaceAll("\\\n", "\n"); }; +function ViewRawText({ + isRawView, + setIsRawView, +}: { + isRawView: boolean; + setIsRawView: Dispatch>; +}) { + return ( + + setIsRawView((p) => !p)} + > + {isRawView ? ( + + ) : ( + + )} + + + ); +} + export interface TextRendererProps { isEditing: boolean; isHovering: boolean; @@ -37,6 +71,8 @@ export function TextRendererComponent(props: TextRendererProps) { setUpdateRenderedArtifactRequired, } = graphData; + const [rawMarkdown, setRawMarkdown] = useState(""); + const [isRawView, setIsRawView] = useState(false); const [manuallyUpdatingArtifact, setManuallyUpdatingArtifact] = useState(false); @@ -118,6 +154,24 @@ export function TextRendererComponent(props: TextRendererProps) { } }, [artifact, updateRenderedArtifactRequired]); + useEffect(() => { + if (isRawView) { + editor.blocksToMarkdownLossy(editor.document).then(setRawMarkdown); + } else if (!isRawView && rawMarkdown) { + try { + (async () => { + setManuallyUpdatingArtifact(true); + const markdownAsBlocks = + await editor.tryParseMarkdownToBlocks(rawMarkdown); + editor.replaceBlocks(editor.document, markdownAsBlocks); + setManuallyUpdatingArtifact(false); + })(); + } catch (_) { + setManuallyUpdatingArtifact(false); + } + } + }, [isRawView, editor]); + const isComposition = useRef(false); const onChange = async () => { @@ -159,48 +213,94 @@ export function TextRendererComponent(props: TextRendererProps) { }); }; + const onChangeRawMarkdown = (e: React.ChangeEvent) => { + const newRawMarkdown = e.target.value; + setRawMarkdown(newRawMarkdown); + setArtifact((prev) => { + if (!prev) { + return { + currentIndex: 1, + contents: [ + { + index: 1, + fullMarkdown: newRawMarkdown, + title: "Untitled", + type: "text", + }, + ], + }; + } else { + return { + ...prev, + contents: prev.contents.map((c) => { + if (c.index === prev.currentIndex) { + return { + ...c, + fullMarkdown: newRawMarkdown, + }; + } + return c; + }), + }; + } + }); + }; + return (
{props.isHovering && artifact && ( -
+
+
)} - - (isComposition.current = true)} - onCompositionEndCapture={() => (isComposition.current = false)} - onChange={onChange} - editable={!isStreaming || props.isEditing || !manuallyUpdatingArtifact} - editor={editor} - className={isStreaming && !firstTokenReceived ? "pulse-text" : ""} - > - - getDefaultReactSlashMenuItems(editor).filter( - (z) => z.group !== "Media" - ) - } - triggerCharacter={"/"} + {isRawView ? ( +