/**
* Drag-and-drop ingestion: turn a folder of MLC-compiled model artifacts into
* populated Cache Storage entries, with no network involved at any point.
*
* Everything is validated before the first byte is written, so a folder that is
* missing a shard fails immediately instead of after copying 2 GB.
*/
import { ERROR, EngineError } from "./errors.js";
import { CACHE_CONFIG, CACHE_MODEL, CACHE_WASM, SOURCE, baseUrlFor, toModelType } from "./model-store.js";
/** WebLLM asks for this name; older MLC exports ship `ndarray-cache.json`. */
export const TENSOR_MANIFEST = "tensor-cache.json";
export const LEGACY_TENSOR_MANIFEST = "ndarray-cache.json";
export const CHAT_CONFIG = "mlc-chat-config.json";
export const CONTENT_TYPES = {
json: "application/json",
wasm: "application/wasm",
bin: "application/octet-stream",
};
/**
* Walk a DataTransfer from a drop event into flat `{ path, file }` entries.
* Uses the entries API so dropping a *folder* works, not just a file selection.
*/
export async function filesFromDataTransfer(dataTransfer) {
// `Array.from` throughout, for the reason `filesFromInput` gives: a
// DataTransferItemList and a FileList are array-like, and only sometimes
// iterable. Spreading them threw from inside here, three frames from the drop
// handler the caller actually wrote.
const roots = Array.from(dataTransfer.items)
.filter((item) => item.kind === "file")
.map((item) => (item.webkitGetAsEntry ? item.webkitGetAsEntry() : null));
if (roots.some((entry) => entry === null)) {
// No entries API: fall back to the flat file list (a folder drop yields nothing).
return Array.from(dataTransfer.files, (file) => ({
path: file.webkitRelativePath || file.name,
file,
}));
}
const out = [];
await Promise.all(roots.filter(Boolean).map((entry) => walkEntry(entry, "", out)));
return out;
}
async function walkEntry(entry, prefix, out) {
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isFile) {
out.push({ path, file: await new Promise((res, rej) => entry.file(res, rej)) });
return;
}
const reader = entry.createReader();
// readEntries() returns at most ~100 entries per call; drain it.
for (;;) {
const batch = await new Promise((res, rej) => reader.readEntries(res, rej));
if (batch.length === 0) break;
await Promise.all(batch.map((child) => walkEntry(child, path, out)));
}
}
/**
* Turns `` output into the same `{ path, file }` shape.
*
* `Array.from`, not spread: a real `FileList` is iterable, but plenty of things
* that behave like one are only array-like, and spreading those fails with
* "fileList is not iterable" — an error that names none of the three places it
* could have come from. Array.from accepts both.
*/
export function filesFromInput(fileList) {
return Array.from(fileList, (file) => ({
path: file.webkitRelativePath || file.name,
file,
}));
}
/**
* @param {Array<{path: string, file: File}>} entries
* @param {object} opts
* @param {import("./model-store.js").ModelStore} opts.store where the registry entry lands
* @param {string} [opts.modelId] overrides the id inferred from the folder name
* @param {"llm"|"embedding"|"vlm"} [opts.modelType] declare a vision model, or WebLLM
* rejects every image sent to it
* @param {(p: {phase: string, done: number, total: number, label: string}) => void} [opts.onProgress]
* @returns {Promise