Files
AgentHub/client/src/components/HtmlFrame.tsx
T
nntrivi2001 d97c403aab fix(changes): a huge line no longer cuts the diff short; preview pages up to 64 MB
A page that inlines a video as base64 has rows of megabytes. Its patch was read
at the 200 KB per-file cap, so the first such row used the whole budget and
every hunk after it was lost ("Diff cut short"). The patch is now read up to
16 M characters, rows over 4,000 characters are cut to 1,000 with an `elided`
count the diff shows, and only then is the cap applied. Hunk actions re-read
git's own diff by header and are unchanged.

The HTML preview refused a page over 10 MB and showed only the file icon; a
self-contained 18 MB slide deck now opens (limit 64 MB).
2026-10-30 10:28:53 +07:00

101 lines
3.4 KiB
TypeScript

/**
* @file HtmlFrame.tsx
* @description An HTML file rendered as a page, the one way every file view
* shows one (a Chat file preview, the Changes panel's Preview, the File
* Explorer's viewer): its relative CSS, scripts and images pulled in through
* the caller's guarded read (`lib/htmlInline.ts`), then run in the chat
* artifact's sandbox — `<iframe srcdoc>` with scripts but never the
* dashboard's origin (no `allow-same-origin`). Given no `html`, it reads the
* file itself through the same `fetchFile`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { FileText, Loader2 } from "lucide-react";
import { artifactDocument, frameSandbox } from "../lib/artifactView";
import { blobText, inlineHtml, type FetchFile } from "../lib/htmlInline";
/** A page read by `fetchFile` is shown up to this size. A self-contained
* page (a slide deck with its video and images inlined as base64) is easily
* past 10 MB; the iframe renders far larger documents than this. */
const HTML_MAX = 64 * 1024 * 1024;
export function HtmlFrame({
path,
html,
fetchFile,
title,
className = "",
testId = "html-frame",
}: {
/** The file's path relative to the place `fetchFile` reads from. */
path: string;
/** The page's text; read through `fetchFile` when absent. */
html?: string;
/** Reads the page's relative files; none = the page as it is. */
fetchFile?: FetchFile | null;
title: string;
className?: string;
testId?: string;
}) {
const [doc, setDoc] = useState<string | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
let live = true;
// The page shown stays until its successor is ready (a version step, a
// re-read): no flash of the spinner between two renders of one file.
setFailed(false);
(async () => {
let text = html;
if (text === undefined) {
if (!fetchFile) throw new Error("nothing to read the page with");
const b = await fetchFile(path);
if (b.size > HTML_MAX) throw new Error("too big");
text = await blobText(b);
}
const page = text;
return fetchFile ? inlineHtml(page, path, fetchFile).catch(() => page) : page;
})().then(
(d) => live && setDoc(d),
() => live && setFailed(true)
);
return () => {
live = false;
};
}, [html, path, fetchFile]);
if (failed) {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-2 p-6 text-center text-sm text-fg-muted">
<FileText className="h-8 w-8" />
<p className="break-all font-mono text-xs">{path}</p>
</div>
);
}
if (doc === null) {
return (
<div className="flex flex-1 items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-fg-muted" />
</div>
);
}
return (
<iframe
data-testid={testId}
title={title}
srcDoc={artifactDocument("html", doc, false) ?? ""}
sandbox={frameSandbox("html")}
referrerPolicy="no-referrer"
className={`min-h-0 w-full flex-1 border-0 bg-white ${className}`}
/>
);
}
/** A `FetchFile` over a download URL builder (a workspace's or a home's). */
export function fetchVia(url: (relPath: string) => string): FetchFile {
return async (rel) => {
const r = await fetch(url(rel), { credentials: "same-origin" });
if (!r.ok) throw new Error(String(r.status));
return r.blob();
};
}