Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

138 lines
4.3 KiB
TypeScript

/**
* Maps a chat attachment to the preview renderer it should use.
*
* The drawer uses the returned ``kind`` to dynamically import the matching
* renderer (see ``./previewers/*.tsx``). Office binaries (DOCX/XLSX/PPTX)
* cannot be rendered natively in the browser, so we fall back to the
* extractor's plain-text output when present, or to a download-only
* affordance otherwise.
*/
import { langForFilename } from "@/lib/code-languages";
import { extOf } from "@/lib/doc-attachments";
export type PreviewKind =
| "pdf"
| "image"
| "svg"
| "markdown"
| "code"
| "text"
| "docx"
| "xlsx"
| "office-text"
| "fallback";
export interface FilePreviewSource {
/** Display name; also used to derive the file extension. */
filename: string;
/** MIME type when known (image/png, application/pdf, …). */
mimeType?: string;
/** Backend classification — "image" or anything else. Useful for
* attachments where the filename has no extension but the MIME is set. */
type?: string;
/** Public URL served by /api/attachments. Preferred over base64. */
url?: string;
/** Inline base64 payload — only present for pending (un-sent) attachments
* or messages that pre-date the storage rollout. */
base64?: string;
/** Plain-text rendering of office docs, populated by the backend. */
extractedText?: string;
/** Optional endpoint that returns extracted plain text on demand. */
extractedTextUrl?: string;
/** Original byte size, used for empty-state copy. */
size?: number;
/** Stable id; lets the drawer build a stable React key. */
id?: string;
}
// OOXML formats with a faithful browser renderer (docx-preview / exceljs).
const DOCX_EXTS = new Set([".docx", ".docm"]);
const XLSX_EXTS = new Set([".xlsx", ".xlsm"]);
// Office binaries with no reliable browser renderer (PowerPoint, and the
// legacy pre-OOXML formats) — fall back to the extractor's plain text.
const OFFICE_BINARY_EXTS = new Set([".pptx", ".ppt", ".doc", ".xls"]);
const MARKDOWN_EXTS = new Set([".md", ".markdown", ".rst", ".asciidoc"]);
const PLAIN_TEXT_EXTS = new Set([
".txt",
".text",
".log",
".csv",
".tsv",
".env",
".conf",
]);
const RASTER_IMAGE_EXTS = new Set([
".png",
".jpg",
".jpeg",
".gif",
".webp",
".bmp",
".tif",
".tiff",
".avif",
]);
/** Heuristic: does *source* refer to an image we can render via <img>? */
function isImage(source: FilePreviewSource, ext: string): boolean {
if (RASTER_IMAGE_EXTS.has(ext)) return true;
if (
source.mimeType?.startsWith("image/") &&
source.mimeType !== "image/svg+xml"
)
return true;
if (source.type === "image") return true;
return false;
}
export function previewKindFor(source: FilePreviewSource): PreviewKind {
const ext = extOf(source.filename || "");
const mime = source.mimeType || "";
if (ext === ".pdf" || mime === "application/pdf") return "pdf";
if (ext === ".svg" || mime === "image/svg+xml") return "svg";
if (isImage(source, ext)) return "image";
if (MARKDOWN_EXTS.has(ext) || mime === "text/markdown") return "markdown";
if (
DOCX_EXTS.has(ext) ||
mime ===
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
return "docx";
if (
XLSX_EXTS.has(ext) ||
mime === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
return "xlsx";
if (OFFICE_BINARY_EXTS.has(ext)) return "office-text";
// Catches both extension-based mappings (.js, .ts, .go, .vue, .lua, …)
// and special filenames without extensions (Dockerfile, Makefile, …).
if (langForFilename(source.filename || "")) return "code";
if (PLAIN_TEXT_EXTS.has(ext) || mime.startsWith("text/")) return "text";
return "fallback";
}
/**
* Build the in-browser-loadable URL for the preview, falling back to a
* data URL when the original file lives only as base64 in memory (the
* pending-attachment case in the composer).
*
* Returns ``null`` when neither is available; renderers should then show a
* "preview not available" affordance.
*/
export function resolveSourceUrl(
source: FilePreviewSource,
apiUrl: (path: string) => string,
): string | null {
if (source.url) {
return source.url.startsWith("http") || source.url.startsWith("blob:")
? source.url
: apiUrl(source.url);
}
if (source.base64 && source.mimeType) {
return `data:${source.mimeType};base64,${source.base64}`;
}
return null;
}