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 && (