d97c403aab
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).
101 lines
3.4 KiB
TypeScript
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();
|
|
};
|
|
}
|