From d6cf3c1ee2449ced03648a04a4558077bae85c61 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 10:12:50 +0200 Subject: [PATCH 1/3] Reuse file Markdown styling for artifacts Generated-By: PostHog Code Task-Id: ac25b9a6-5db4-4592-ad52-be66ce043db0 --- .../components/CodeEditorPanel.tsx | 15 +++---- .../components/MarkdownDocumentPreview.tsx | 19 ++++++++ .../sessions/components/ArtifactPreview.tsx | 43 ++++++++++++++++++- .../components/artifactPreviewDocument.ts | 21 --------- 4 files changed, 65 insertions(+), 33 deletions(-) create mode 100644 packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx diff --git a/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx b/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx index 841af0ea91..026282774b 100644 --- a/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx +++ b/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx @@ -17,8 +17,6 @@ import type { Task } from "@posthog/shared/domain-types"; import { Box, Flex, IconButton, Text } from "@radix-ui/themes"; import { useCallback, useMemo, useState } from "react"; import type { Components } from "react-markdown"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; import { PanelMessage } from "../../../primitives/PanelMessage"; import { SafeImagePreview } from "../../../primitives/SafeImagePreview"; import { Tooltip } from "../../../primitives/Tooltip"; @@ -38,6 +36,7 @@ import { import { useFileEnrichment } from "../hooks/useFileEnrichment"; import { CodeMirrorEditor } from "./CodeMirrorEditor"; import { EnrichmentPopover } from "./EnrichmentPopover"; +import { MarkdownDocumentPreview } from "./MarkdownDocumentPreview"; import { SelectionCommentOverlay, useSelectionComposer, @@ -363,14 +362,10 @@ export function CodeEditorPanel({ {sourceView} ) : renderableKind === "markdown" ? ( - - - {fileContent} - - + ) : ( diff --git a/packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx b/packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx new file mode 100644 index 0000000000..b3769d2809 --- /dev/null +++ b/packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx @@ -0,0 +1,19 @@ +import type { Components } from "react-markdown"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +export function MarkdownDocumentPreview({ + content, + components, +}: { + content: string; + components?: Components; +}) { + return ( +
+ + {content} + +
+ ); +} diff --git a/packages/ui/src/features/sessions/components/ArtifactPreview.tsx b/packages/ui/src/features/sessions/components/ArtifactPreview.tsx index c13bb02d41..36e07c4103 100644 --- a/packages/ui/src/features/sessions/components/ArtifactPreview.tsx +++ b/packages/ui/src/features/sessions/components/ArtifactPreview.tsx @@ -10,11 +10,21 @@ import { useAuthStateValue, } from "@posthog/ui/features/auth/store"; import { AUTH_SCOPED_QUERY_META } from "@posthog/ui/features/auth/useCurrentUser"; +import { Flex } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { useEffect, useMemo, useState } from "react"; import { ZoomableImage } from "../../../primitives/SafeImagePreview"; +import { CodeMirrorEditor } from "../../code-editor/components/CodeMirrorEditor"; +import { DocumentPreviewHeader } from "../../code-editor/components/DocumentPreviewHeader"; +import { MarkdownDocumentPreview } from "../../code-editor/components/MarkdownDocumentPreview"; import { artifactPreviewBlob } from "./artifactPreviewDocument"; +const MARKDOWN_EXTENSIONS = new Set(["md", "mdx", "markdown"]); + +function extension(filename: string): string { + return filename.split(".").pop()?.toLowerCase() ?? ""; +} + function ArtifactPreviewError() { return (
@@ -50,6 +60,7 @@ export function ArtifactPreview({ name: string; }) { const sessionService = useService(SESSION_SERVICE); + const [showRendered, setShowRendered] = useState(true); const authIdentity = useAuthStateValue(getAuthIdentity); const { data, isLoading, isError } = useQuery({ queryKey: ["artifactPreview", authIdentity, taskId, runId, artifactId], @@ -62,7 +73,11 @@ export function ArtifactPreview({ if (!url) throw new Error("Artifact is unavailable"); const response = await fetch(url); if (!response.ok) throw new Error("Artifact preview failed"); - return artifactPreviewBlob(await response.blob(), name); + const blob = await response.blob(); + if (MARKDOWN_EXTENSIONS.has(extension(name))) { + return blob.text(); + } + return artifactPreviewBlob(blob, name); }, enabled: authIdentity !== null, staleTime: Infinity, @@ -70,7 +85,7 @@ export function ArtifactPreview({ meta: AUTH_SCOPED_QUERY_META, }); const previewUrl = useMemo( - () => (data ? URL.createObjectURL(data) : null), + () => (data instanceof Blob ? URL.createObjectURL(data) : null), [data], ); @@ -87,6 +102,30 @@ export function ArtifactPreview({
); } + if (typeof data === "string") { + return ( + + setShowRendered((rendered) => !rendered)} + /> + {showRendered ? ( +
+ null }} + /> +
+ ) : ( +
+ +
+ )} +
+ ); + } if (isError || !previewUrl) { return ; } diff --git a/packages/ui/src/features/sessions/components/artifactPreviewDocument.ts b/packages/ui/src/features/sessions/components/artifactPreviewDocument.ts index 77ee9a74f8..05d0f18c17 100644 --- a/packages/ui/src/features/sessions/components/artifactPreviewDocument.ts +++ b/packages/ui/src/features/sessions/components/artifactPreviewDocument.ts @@ -1,12 +1,6 @@ import { getImageMimeType, isAllowedImageMimeType } from "@posthog/shared"; -import { createElement } from "react"; -import { renderToStaticMarkup } from "react-dom/server"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; import { applyCspToHtml } from "../../mcp-apps/utils/mcp-app-csp"; -const MARKDOWN_EXTENSIONS = new Set(["md", "mdx", "markdown"]); - function extension(filename: string): string { return filename.split(".").pop()?.toLowerCase() ?? ""; } @@ -14,16 +8,6 @@ function extension(filename: string): string { export function artifactHtmlDocument(html: string): string { return applyCspToHtml(html); } - -export function markdownDocument(markdown: string): string { - const content = renderToStaticMarkup( - createElement(ReactMarkdown, { remarkPlugins: [remarkGfm] }, markdown), - ); - return artifactHtmlDocument(`${content}`); -} - export async function artifactPreviewBlob( blob: Blob, filename: string, @@ -36,11 +20,6 @@ export async function artifactPreviewBlob( if (isAllowedImageMimeType(imageMimeType)) { return new Blob([blob], { type: imageMimeType }); } - if (MARKDOWN_EXTENSIONS.has(extension(filename))) { - return new Blob([markdownDocument(await blob.text())], { - type: "text/html", - }); - } if (["html", "htm"].includes(extension(filename))) { return new Blob([artifactHtmlDocument(await blob.text())], { type: "text/html", From 467cc83738b4282ef8b9d8e3697ab0d19a02c22b Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Wed, 29 Jul 2026 10:12:52 +0200 Subject: [PATCH 2/3] Center artifact previews and restore source toggle Generated-By: PostHog Code Task-Id: ac25b9a6-5db4-4592-ad52-be66ce043db0 --- .../components/CodeEditorPanel.tsx | 58 +++-------------- .../components/DocumentPreviewHeader.tsx | 64 +++++++++++++++++++ .../components/MarkdownDocumentPreview.tsx | 2 +- .../components/ArtifactPreview.test.tsx | 44 +++++++++---- 4 files changed, 104 insertions(+), 64 deletions(-) create mode 100644 packages/ui/src/features/code-editor/components/DocumentPreviewHeader.tsx diff --git a/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx b/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx index 026282774b..08600db06d 100644 --- a/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx +++ b/packages/ui/src/features/code-editor/components/CodeEditorPanel.tsx @@ -1,4 +1,3 @@ -import { Check, Code, Copy, Eye } from "@phosphor-icons/react"; import { getRenderableKind } from "@posthog/core/code-editor/fileKind"; import { collapseFileState, @@ -14,8 +13,8 @@ import { parseImageDataUrl, } from "@posthog/shared"; import type { Task } from "@posthog/shared/domain-types"; -import { Box, Flex, IconButton, Text } from "@radix-ui/themes"; -import { useCallback, useMemo, useState } from "react"; +import { Box, Flex } from "@radix-ui/themes"; +import { useCallback, useMemo } from "react"; import type { Components } from "react-markdown"; import { PanelMessage } from "../../../primitives/PanelMessage"; import { SafeImagePreview } from "../../../primitives/SafeImagePreview"; @@ -35,6 +34,7 @@ import { } from "../hooks/useFileContent"; import { useFileEnrichment } from "../hooks/useFileEnrichment"; import { CodeMirrorEditor } from "./CodeMirrorEditor"; +import { DocumentPreviewHeader } from "./DocumentPreviewHeader"; import { EnrichmentPopover } from "./EnrichmentPopover"; import { MarkdownDocumentPreview } from "./MarkdownDocumentPreview"; import { @@ -118,7 +118,6 @@ export function CodeEditorPanel({ const toggleKind = useFilePreviewStore((s) => s.toggleKind); const openFileInSplit = usePanelLayoutStore((s) => s.openFileInSplit); const expandToFile = useFileTreeStore((s) => s.expandToFile); - const [copied, setCopied] = useState(false); const composer = useSelectionComposer(); const handleAddSelectionToChat = useCallback( @@ -305,11 +304,6 @@ export function CodeEditorPanel({ ); if (isRenderable) { - const handleCopySource = () => { - navigator.clipboard.writeText(fileContent); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; const handleToggleRendered = () => { if (renderableKind) { toggleKind(renderableKind); @@ -318,46 +312,12 @@ export function CodeEditorPanel({ return ( - - - {filePath} - - - - - {showRendered ? : } - - - - - {copied ? : } - - - - + {!showRendered ? ( {sourceView} ) : renderableKind === "markdown" ? ( diff --git a/packages/ui/src/features/code-editor/components/DocumentPreviewHeader.tsx b/packages/ui/src/features/code-editor/components/DocumentPreviewHeader.tsx new file mode 100644 index 0000000000..8a336f8914 --- /dev/null +++ b/packages/ui/src/features/code-editor/components/DocumentPreviewHeader.tsx @@ -0,0 +1,64 @@ +import { Check, Code, Copy, Eye } from "@phosphor-icons/react"; +import { Flex, IconButton, Text } from "@radix-ui/themes"; +import { useState } from "react"; +import { Tooltip } from "../../../primitives/Tooltip"; + +export function DocumentPreviewHeader({ + label, + content, + showRendered, + onToggleRendered, +}: { + label: string; + content: string; + showRendered: boolean; + onToggleRendered: () => void; +}) { + const [copied, setCopied] = useState(false); + + const handleCopySource = () => { + navigator.clipboard.writeText(content); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + + {label} + + + + + {showRendered ? : } + + + + + {copied ? : } + + + + + ); +} diff --git a/packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx b/packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx index b3769d2809..99cd4ecab9 100644 --- a/packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx +++ b/packages/ui/src/features/code-editor/components/MarkdownDocumentPreview.tsx @@ -10,7 +10,7 @@ export function MarkdownDocumentPreview({ components?: Components; }) { return ( -
+
{content} diff --git a/packages/ui/src/features/sessions/components/ArtifactPreview.test.tsx b/packages/ui/src/features/sessions/components/ArtifactPreview.test.tsx index 4946346cf3..50df52d2f6 100644 --- a/packages/ui/src/features/sessions/components/ArtifactPreview.test.tsx +++ b/packages/ui/src/features/sessions/components/ArtifactPreview.test.tsx @@ -1,11 +1,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { ArtifactPreview } from "./ArtifactPreview"; -import { - artifactHtmlDocument, - artifactPreviewBlob, - markdownDocument, -} from "./artifactPreviewDocument"; +import { artifactHtmlDocument, artifactPreviewBlob } from "./artifactPreviewDocument"; const previewBlob = new Blob(["

Artifact content

"], { type: "text/html", @@ -34,6 +30,12 @@ vi.mock("@tanstack/react-query", () => ({ useQuery, })); +vi.mock("../../code-editor/components/CodeMirrorEditor", () => ({ + CodeMirrorEditor: ({ content }: { content: string }) => ( +
{content}
+ ), +})); + describe("ArtifactPreview", () => { beforeEach(() => { auth.identity = "auth-1"; @@ -215,17 +217,31 @@ describe("ArtifactPreview", () => { ).toBeInTheDocument(); }); - it("renders GFM Markdown while escaping embedded HTML", () => { - const document = markdownDocument( - "# Report\n\n**Ready**\n\n| Name | Value |\n| --- | --- |\n| Cost | 12 |\n\n", + it("renders Markdown artifacts with the file preview styling", () => { + useQuery.mockReturnValue({ + data: "# Report\n\n**Ready**\n\n| Name | Value |\n| --- | --- |\n| Cost | 12 |", + isLoading: false, + isError: false, + }); + + const { container } = render( + , ); - expect(document).toContain("

Report

"); - expect(document).toContain("Ready"); - expect(document).toContain(""); - expect(document).toContain("<script>"); - expect(document).not.toContain("