Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1608,7 +1608,12 @@ const CHAT_MARKDOWN_COMPONENTS = {
fenceTitle={fenceTitle}
theme={resolvedTheme}
isStreaming={isStreaming && !isClosedCodeFence(node, text, codeBlock.code)}
onExpand={(src) => expandMedia({ images: [{ src, name: "Mermaid diagram" }], index: 0 })}
onExpand={(src) =>
expandMedia({
images: [{ src, name: "Mermaid diagram", mimeType: "image/svg+xml" }],
index: 0,
})
}
>
{highlightedCode}
</MarkdownMermaidCodeBlock>
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/components/chat/ExpandedImageDialog.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,43 @@ const snapShotSource: SnapShotSource = {
windowTitle: "Downloads",
};

const D2_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1174 920"><rect width="1174" height="920"/></svg>';

describe("ExpandedImageDialog", () => {
it("fits a markdown SVG without intrinsic size to the viewer", async () => {
const src = URL.createObjectURL(new Blob([D2_SVG], { type: "image/svg+xml" }));
using _src = { [Symbol.dispose]: () => URL.revokeObjectURL(src) };
await using screen = await render(
<ExpandedImageDialog
preview={{
images: [
{
src,
name: "Pipeline",
actionsSource: {
kind: "image",
name: "Pipeline",
src,
reference: { kind: "file", path: "/work/pipeline.svg" },
},
},
],
index: 0,
}}
onClose={vi.fn()}
/>,
// The dialog renders no links and reads no route.
{ router: false },
);

const viewer = screen.getByRole("region", { name: "Pipeline, zoomable image" });
const availableWidth = window.innerWidth * 0.92 - (window.innerWidth >= 640 ? 96 : 0);
await vi.waitFor(() =>
expect(viewer.element().getBoundingClientRect().width).toBeCloseTo(availableWidth, 0),
);
});

it("Escape closes a popup opened inside the dialog before the dialog", async () => {
const onClose = vi.fn();
await using screen = await render(
Expand Down
12 changes: 11 additions & 1 deletion apps/web/src/components/chat/ExpandedImageDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { Atom } from "effect/reactivity";
import { appAtomRegistry } from "~/rpc/atomRegistry";
import { expandedImageKey } from "./ExpandedImagePreview";
import { registerThreadOverlay } from "./threadOverlayRetention";
import { mediaReferenceFileName } from "@t3tools/client-runtime/media-reference";
import { mediaMimeType } from "@t3tools/shared/mediaPreview";

interface ExpandedImageDialogProps {
preview: ExpandedImagePreview;
Expand All @@ -42,6 +44,14 @@ function ExpandedMediaFailure({ children }: { children: ReactNode }) {
);
}

function itemMimeType(item: ExpandedImageItem): string | undefined {
if (item.mimeType) return item.mimeType;
const reference = item.actionsSource?.reference;
return (
mediaMimeType((reference && mediaReferenceFileName(reference)) || item.src || "") ?? undefined
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
);
}

function ExpandedVideo({ item }: { readonly item: ExpandedImageItem }) {
const asset = item.actionsSource?.asset;
const assetUrl = useAssetUrlState(asset?.environmentId ?? null, asset?.resource ?? null);
Expand Down Expand Up @@ -222,7 +232,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({
key={`${index}:${item.src}`}
src={item.src}
name={item.name}
mimeType={item.mimeType}
mimeType={itemMimeType(item)}
onError={() => setFailedImageSrc(item.src)}
/>
)}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/chat/ZoomableImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ export function ZoomableImage({
const suppressClickRef = useRef(false);
const [dragging, setDragging] = useState(false);
const maxHeight = Math.max(1, Math.min(viewerSize.height * 0.86, viewerSize.height - 160));
// A vector scales without loss, and an SVG without width and height reports a tiny default size.
const fit = Math.min(
1,
mimeType === "image/svg+xml" ? Infinity : 1,
(viewerSize.width * 0.92 - (viewerSize.width >= 640 ? 96 : 0)) / (naturalSize.width || 1),
maxHeight / (naturalSize.height || 1),
);
Expand Down
Loading