diff --git a/apps/web/src/components/artifacts/ArtifactRenderer.tsx b/apps/web/src/components/artifacts/ArtifactRenderer.tsx index 7a2caf4a..993f8c9b 100644 --- a/apps/web/src/components/artifacts/ArtifactRenderer.tsx +++ b/apps/web/src/components/artifacts/ArtifactRenderer.tsx @@ -318,6 +318,9 @@ function ArtifactRendererComponent(props: ArtifactRendererProps) { artifactUpdateFailed={artifactUpdateFailed} chatCollapsed={props.chatCollapsed} setChatCollapsed={props.setChatCollapsed} + artifact={artifact} + setSelectedBlocks={setSelectedBlocks} + isStreaming={isStreaming} />
void; + setSelectedBlocks: (blocks: TextHighlight | undefined) => void; + isStreaming?: boolean; +} + +export function ArtifactVersionsMenu({ + artifact, + currentArtifactContent, + setSelectedArtifact, + setSelectedBlocks, + isStreaming = false, +}: ArtifactVersionsMenuProps) { + const [isOpen, setIsOpen] = useState(false); + + const handleVersionSelect = (index: number) => { + if (!isStreaming && index !== currentArtifactContent.index) { + + setSelectedBlocks(undefined); + setSelectedArtifact(index); + } + setIsOpen(false); + }; + + const getVersionTypeIcon = (content: ArtifactCodeV3 | ArtifactMarkdownV3) => { + return content.type === "code" ? ( + + ) : ( + + ); + }; + + const getVersionLabel = (content: ArtifactCodeV3 | ArtifactMarkdownV3) => { + const baseTitle = content.title || `${content.type === "code" ? "Code" : "Text"} v${content.index}`; + if (content.type === "code" && content.language && content.language !== "other") { + return `${baseTitle} (${content.language})`; + } + return baseTitle; + }; + + const getVersionPreview = (content: ArtifactCodeV3 | ArtifactMarkdownV3) => { + if (content.type === "code") { + const preview = content.code.slice(0, 60).replace(/\n/g, " "); + return preview.length < content.code.length ? `${preview}...` : preview; + } else { + const preview = content.fullMarkdown.slice(0, 60).replace(/\n/g, " "); + return preview.length < content.fullMarkdown.length ? `${preview}...` : preview; + } + }; + + // Sort versions by index + const sortedVersions = [...artifact.contents].sort((a, b) => a.index - b.index); + + return ( + + + + {isOpen ? ( + + ) : ( + + )} + + + + + + Artifact Versions ({artifact.contents.length}) + + + + {sortedVersions.map((version) => { + const isCurrentVersion = version.index === currentArtifactContent.index; + + return ( + handleVersionSelect(version.index)} + disabled={isStreaming} + > +
+ {getVersionTypeIcon(version)} + + {getVersionLabel(version)} + + {isCurrentVersion && ( + + Current + + )} +
+ + {/* version preview */} +
+ {getVersionPreview(version)} +
+ + {/* version number */} +
+ Version {version.index} +
+
+ ); + })} + + {artifact.contents.length === 0 && ( + + No versions available + + )} +
+ + ); +} diff --git a/apps/web/src/components/artifacts/header/index.tsx b/apps/web/src/components/artifacts/header/index.tsx index 2fae7e1e..2c1aaaf0 100644 --- a/apps/web/src/components/artifacts/header/index.tsx +++ b/apps/web/src/components/artifacts/header/index.tsx @@ -1,7 +1,8 @@ import { ReflectionsDialog } from "../../reflections-dialog/ReflectionsDialog"; import { ArtifactTitle } from "./artifact-title"; import { NavigateArtifactHistory } from "./navigate-artifact-history"; -import { ArtifactCodeV3, ArtifactMarkdownV3 } from "@opencanvas/shared/types"; +import { ArtifactVersionsMenu } from "./artifact-versions-menu"; +import { ArtifactCodeV3, ArtifactMarkdownV3, ArtifactV3, TextHighlight } from "@opencanvas/shared/types"; import { Assistant } from "@langchain/langgraph-sdk"; import { PanelRightClose } from "lucide-react"; import { TooltipIconButton } from "@/components/ui/assistant-ui/tooltip-icon-button"; @@ -17,12 +18,24 @@ interface ArtifactHeaderProps { artifactUpdateFailed: boolean; chatCollapsed: boolean; setChatCollapsed: (c: boolean) => void; + artifact: ArtifactV3; + setSelectedBlocks: (blocks: TextHighlight | undefined) => void; + isStreaming?: boolean; } export function ArtifactHeader(props: ArtifactHeaderProps) { return (
+ {/* Burger Menu for Artifact Versions */} + + {props.chatCollapsed && (