feat: multi-track file upload support (native images, code/text block formatting, and workspace binary uploads)
This commit is contained in:
+116
-14
@@ -3897,23 +3897,126 @@ window.__ModuleLoader__.load({
|
||||
const onButtonClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
const onFileChange = (e) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
const onFileChange = async (e) => {
|
||||
const fileList = Array.from(e.target.files || []);
|
||||
if (fileList.length === 0) return;
|
||||
e.target.value = "";
|
||||
|
||||
const textarea = document.querySelector("textarea");
|
||||
if (textarea) {
|
||||
const dt = new DataTransfer();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
dt.items.add(files[i]);
|
||||
const isZh = isZhLocale();
|
||||
|
||||
const imageFiles = [];
|
||||
const textCodeFiles = [];
|
||||
const binaryUploadFiles = [];
|
||||
|
||||
const CODE_EXT_MAP = {
|
||||
js: "javascript", jsx: "javascript", mjs: "javascript", cjs: "javascript",
|
||||
ts: "typescript", tsx: "typescript",
|
||||
py: "python",
|
||||
json: "json",
|
||||
md: "markdown", markdown: "markdown",
|
||||
html: "html", htm: "html",
|
||||
css: "css", scss: "scss", less: "less",
|
||||
sh: "bash", bash: "bash", zsh: "bash",
|
||||
yaml: "yaml", yml: "yaml",
|
||||
xml: "xml", svg: "xml",
|
||||
sql: "sql",
|
||||
c: "c", h: "c", cpp: "cpp", hpp: "cpp", cc: "cpp",
|
||||
rs: "rust",
|
||||
go: "go",
|
||||
java: "java",
|
||||
kt: "kotlin",
|
||||
rb: "ruby",
|
||||
php: "php",
|
||||
lua: "lua",
|
||||
toml: "toml", ini: "ini", conf: "ini", env: "ini",
|
||||
txt: "text", log: "text", csv: "csv"
|
||||
};
|
||||
|
||||
for (const file of fileList) {
|
||||
const ext = (file.name.split(".").pop() || "").toLowerCase();
|
||||
const isImage = file.type.startsWith("image/") || ["png", "jpg", "jpeg", "webp", "gif", "bmp"].includes(ext);
|
||||
|
||||
if (isImage) {
|
||||
imageFiles.push(file);
|
||||
} else if ((CODE_EXT_MAP[ext] || file.type.startsWith("text/")) && file.size <= 300 * 1024) {
|
||||
textCodeFiles.push({ file, ext: CODE_EXT_MAP[ext] || "text" });
|
||||
} else {
|
||||
binaryUploadFiles.push(file);
|
||||
}
|
||||
const dropEvt = new DragEvent("drop", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
dataTransfer: dt
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Process Images via native drop event to textarea
|
||||
if (imageFiles.length > 0 && textarea) {
|
||||
const dt = new DataTransfer();
|
||||
for (const f of imageFiles) dt.items.add(f);
|
||||
const dropEvt = new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer: dt });
|
||||
textarea.dispatchEvent(dropEvt);
|
||||
}
|
||||
e.target.value = "";
|
||||
|
||||
// Helper to safely insert text into textarea and trigger React/DSH state updates
|
||||
const insertIntoTextarea = (textToInsert) => {
|
||||
if (!textarea) return;
|
||||
const start = textarea.selectionStart ?? textarea.value.length;
|
||||
const end = textarea.selectionEnd ?? textarea.value.length;
|
||||
const val = textarea.value;
|
||||
const prefix = (start > 0 && !val.endsWith("\n") && !val.slice(0, start).endsWith("\n")) ? "\n" : "";
|
||||
const nextValue = val.slice(0, start) + prefix + textToInsert + val.slice(end);
|
||||
|
||||
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")?.set;
|
||||
if (setter) {
|
||||
setter.call(textarea, nextValue);
|
||||
} else {
|
||||
textarea.value = nextValue;
|
||||
}
|
||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
textarea.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
textarea.focus();
|
||||
const newPos = start + prefix.length + textToInsert.length;
|
||||
textarea.setSelectionRange(newPos, newPos);
|
||||
};
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes < 1024) return bytes + " B";
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||
};
|
||||
|
||||
// 2. Process Text/Code files by reading and formatting as code blocks
|
||||
for (const item of textCodeFiles) {
|
||||
try {
|
||||
const content = await item.file.text();
|
||||
const tag = isZh ? "附加文件" : "Attached File";
|
||||
const block = "[" + tag + ": " + item.file.name + "]\n```" + item.ext + "\n" + content + "\n```\n\n";
|
||||
insertIntoTextarea(block);
|
||||
} catch (err) {
|
||||
console.error("Failed to read text file:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Process Binary/Large files by uploading to workspace
|
||||
for (const file of binaryUploadFiles) {
|
||||
try {
|
||||
const res = await fetch("/api/model-enhancer/upload?filename=" + encodeURIComponent(file.name), {
|
||||
method: "POST",
|
||||
headers: { "X-Filename": encodeURIComponent(file.name) },
|
||||
body: file
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const tag = isZh ? "已上传文件至工作区" : "Uploaded file to workspace";
|
||||
const ref = "[" + tag + ": ./" + data.path + " (" + formatSize(file.size) + ")]\n";
|
||||
insertIntoTextarea(ref);
|
||||
} else {
|
||||
const tag = isZh ? "文件上传失败" : "File upload failed";
|
||||
insertIntoTextarea("[" + tag + ": " + file.name + "]\n");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to upload binary file:", err);
|
||||
const tag = isZh ? "文件上传出错" : "File upload error";
|
||||
insertIntoTextarea("[" + tag + ": " + file.name + " - " + (err.message || err) + "]\n");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPlusClick = (e) => {
|
||||
@@ -3931,7 +4034,6 @@ window.__ModuleLoader__.load({
|
||||
ref: fileInputRef,
|
||||
type: "file",
|
||||
multiple: true,
|
||||
accept: "image/*,.png,.jpg,.jpeg,.webp,.gif",
|
||||
style: { display: "none" },
|
||||
onChange: onFileChange
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user