+
+
-
选择目标目录
-
浏览到目标目录,然后点击底部按钮确认
-
-
+
+ 选入到
+
+
+
勾选要带入的文件 / 目录(可跨目录,选择跨切换保留);底部按钮把它们复制或移动到此处。
+
+
-
-
+ 已选 0 项
+
+
+
@@ -675,8 +686,6 @@ const state = {
taskId: null,
taskMeta: null,
filesPath: "",
- // 文件多选状态:rel 路径集合;路径切换 / 复制移动成功后清空,refresh 保留(仅剔除已不存在的)
- selectedFiles: new Set(),
evtSrc: null,
streaming: false, // 当前是否在流式中;true 时显示 stop 按钮
// task list 分页 + 筛选
@@ -1068,7 +1077,6 @@ async function selectTask(tid) {
// 不强绑定 — 用户可点 crumb 回上层看 user_root 其他目录
const wdName = meta.working_dir ? meta.working_dir.split("/").filter(Boolean).pop() : "";
state.filesPath = wdName || "";
- state.selectedFiles.clear(); // 切 task 跨 view 重置选中
await loadFiles();
} catch (e) {
if (e.status === 401) { logout(); return; }
@@ -1441,116 +1449,110 @@ $("btn-upload").onclick = () => $("upload-input").click();
$("chat-upload").onclick = () => $("upload-input").click();
$("upload-input").addEventListener("change", uploadSelected);
-// 顶栏全选 checkbox:三态 — 全未选 → 全选;部分/全选 → 清空
-$("files-selall").onchange = () => {
- const rels = Object.keys(state.entriesByRel || {});
- if (!rels.length) return;
- const allSelected = rels.every((r) => state.selectedFiles.has(r));
- if (allSelected) rels.forEach((r) => state.selectedFiles.delete(r));
- else rels.forEach((r) => state.selectedFiles.add(r));
- // 不需全量重渲染(行已在 DOM),逐行反映 selection
- document.querySelectorAll("#file-list .file-row").forEach((row) => {
- const sel = state.selectedFiles.has(row.dataset.rel);
- row.classList.toggle("selected", sel);
- const cb = row.querySelector(".row-cb");
- if (cb) cb.checked = sel;
- });
- syncBulkBar();
-};
+// ───── 选入 modal(勾源 → 复制 / 移动到主区当前目录)─────
+// 设计:目的地永远是主区 state.filesPath。弹框内浏览的 path 跟主区独立 — 用户从 A 翻到 B
+// 勾几个,再翻到 C 接着勾,跨目录 selection 用 Set
全程保留;切换浏览路径不清空。
+const srcPicker = { path: "", selected: new Set() };
-// 多选 toolbar
-$("btn-bulk-copy").onclick = () => openDirPicker("copy");
-$("btn-bulk-move").onclick = () => openDirPicker("move");
-$("btn-bulk-clear").onclick = () => {
- state.selectedFiles.clear();
- document.querySelectorAll("#file-list .file-row").forEach((row) => {
- row.classList.remove("selected");
- const cb = row.querySelector(".row-cb");
- if (cb) cb.checked = false;
- });
- syncBulkBar();
-};
-
-// ───── 目录选择 modal(复制 / 移动 目标)─────
-const dirPicker = { mode: null, sources: [], path: "" };
-
-async function openDirPicker(mode) {
- const sources = [...state.selectedFiles];
- if (!sources.length) return;
- dirPicker.mode = mode;
- dirPicker.sources = sources;
- dirPicker.path = "";
- const isCopy = mode === "copy";
- $("dp-title").textContent = isCopy ? "复制到…" : "移动到…";
- $("dp-confirm").textContent = isCopy ? "在此处复制" : "在此处移动";
- $("dp-confirm").className = isCopy ? "primary" : "primary"; // 都用 primary;若想区分色再改
- $("dp-hint").textContent =
- (isCopy ? "将 " : "将 ") + sources.length + " 项" +
- (isCopy ? "复制" : "移动") +
- "到下面浏览到的目录;源目录灰禁不可选(不能放到自己里)";
- $("dir-picker-modal").classList.add("show");
- await loadDirPicker();
+async function openSrcPicker() {
+ srcPicker.path = "";
+ srcPicker.selected.clear();
+ const destLabel = state.filesPath ? "我的 / " + state.filesPath : "我的 (根目录)";
+ $("sp-dest").textContent = destLabel;
+ $("sp-dest").title = destLabel;
+ syncSrcCount();
+ $("src-picker-modal").classList.add("show");
+ await loadSrcPicker();
}
-function closeDirPicker() {
- $("dir-picker-modal").classList.remove("show");
- dirPicker.mode = null;
- dirPicker.sources = [];
- dirPicker.path = "";
+function closeSrcPicker() {
+ $("src-picker-modal").classList.remove("show");
+ srcPicker.path = "";
+ srcPicker.selected.clear();
}
-async function loadDirPicker() {
+async function loadSrcPicker() {
try {
- const qs = dirPicker.path ? "?path=" + encodeURIComponent(dirPicker.path) : "";
+ const qs = srcPicker.path ? "?path=" + encodeURIComponent(srcPicker.path) : "";
const data = await api("GET", "/v1/files" + qs);
- renderDirPicker(data);
+ renderSrcPicker(data);
} catch (e) {
if (e.status === 401) { logout(); return; }
- $("dp-list").innerHTML = `${escapeHtml(e.message)}
`;
+ $("sp-list").innerHTML = `${escapeHtml(e.message)}
`;
}
}
-function renderDirPicker(data) {
- // crumbs(可点回上层)
+function renderSrcPicker(data) {
const cr = data.crumbs.map((c, i) => {
const label = i === 0 ? "我的" : c.label;
const isLast = i === data.crumbs.length - 1;
if (isLast) return `${escapeHtml(label)}`;
return `${escapeHtml(label)} /`;
}).join(" ");
- $("dp-crumbs").innerHTML = cr || `/`;
- $("dp-crumbs").querySelectorAll("a").forEach((a) => {
- a.onclick = (e) => { e.preventDefault(); dirPicker.path = a.dataset.rel; loadDirPicker(); };
+ $("sp-crumbs").innerHTML = cr || `/`;
+ $("sp-crumbs").querySelectorAll("a").forEach((a) => {
+ a.onclick = (e) => { e.preventDefault(); srcPicker.path = a.dataset.rel; loadSrcPicker(); };
});
- // 只列目录;源 dir 本身灰禁(server 也会拒,UI 提前拦更友好)
- const dirs = ((data.entries || []).filter((e) => e.is_dir));
- const srcSet = new Set(dirPicker.sources);
- if (!dirs.length) {
- $("dp-list").innerHTML = `(无子目录)
`;
- } else {
- $("dp-list").innerHTML = dirs.map((e) => {
- const isSrc = srcSet.has(e.rel);
- const cls = "dir-row" + (isSrc ? " disabled" : "");
- const title = isSrc ? "源目录,不能放进自己" : "进入";
- return `${escapeHtml(e.name)}
`;
- }).join("");
- $("dp-list").querySelectorAll(".dir-row:not(.disabled)").forEach((row) => {
- row.onclick = () => { dirPicker.path = row.dataset.rel; loadDirPicker(); };
- });
+ const entries = data.entries || [];
+ if (!data.exists) {
+ $("sp-list").innerHTML = `(目录尚未创建)
`;
+ return;
}
+ if (!entries.length) {
+ $("sp-list").innerHTML = `(空目录)
`;
+ return;
+ }
+ // 闸:当前浏览路径 == 主区目的地 → 同目录内勾选无意义(同名 409),全行 disabled
+ const destPath = state.filesPath || "";
+ const sameAsDest = srcPicker.path === destPath;
+ $("sp-list").innerHTML = entries.map((e) => {
+ const cls = e.is_dir ? "ico-dir" : "ico-file";
+ const checked = srcPicker.selected.has(e.rel) ? " checked" : "";
+ const disabled = sameAsDest ? " disabled" : "";
+ return `
+
+
+ ${escapeHtml(e.name)}
+ ${humanSize(e.size)}
+
+ `;
+ }).join("");
+ $("sp-list").querySelectorAll(".sp-name").forEach((el) => {
+ el.onclick = () => {
+ if (el.dataset.isdir === "true") {
+ srcPicker.path = el.dataset.rel;
+ loadSrcPicker();
+ }
+ };
+ });
+ $("sp-list").querySelectorAll(".sp-cb").forEach((cb) => {
+ cb.onchange = () => {
+ const rel = cb.dataset.rel;
+ if (cb.checked) srcPicker.selected.add(rel);
+ else srcPicker.selected.delete(rel);
+ syncSrcCount();
+ };
+ });
}
-async function confirmDirPicker() {
- if (!dirPicker.mode) return;
- const endpoint = dirPicker.mode === "copy" ? "/v1/files/copy" : "/v1/files/move";
- const verb = dirPicker.mode === "copy" ? "复制" : "移动";
+function syncSrcCount() {
+ const n = srcPicker.selected.size;
+ $("sp-count").textContent = String(n);
+ $("sp-copy").disabled = n === 0;
+ $("sp-move").disabled = n === 0;
+}
+
+async function doSrcTransfer(mode) {
+ const sources = [...srcPicker.selected];
+ if (!sources.length) return;
+ const endpoint = mode === "copy" ? "/v1/files/copy" : "/v1/files/move";
+ const verb = mode === "copy" ? "复制" : "移动";
try {
- const res = await api("POST", endpoint, {
- paths: dirPicker.sources,
- dest_dir: dirPicker.path,
+ await api("POST", endpoint, {
+ paths: sources,
+ dest_dir: state.filesPath || "",
});
- closeDirPicker();
- state.selectedFiles.clear();
+ closeSrcPicker();
await loadFiles();
await loadFolderSuggestions();
} catch (e) {
@@ -1559,11 +1561,47 @@ async function confirmDirPicker() {
}
}
-$("dp-cancel").onclick = closeDirPicker;
-$("dp-confirm").onclick = confirmDirPicker;
-$("dir-picker-modal").addEventListener("click", (e) => {
- // 点遮罩(card 外)关闭
- if (e.target.id === "dir-picker-modal") closeDirPicker();
+$("btn-src-pick").onclick = openSrcPicker;
+$("sp-cancel").onclick = closeSrcPicker;
+$("sp-copy").onclick = () => doSrcTransfer("copy");
+$("sp-move").onclick = () => doSrcTransfer("move");
+$("src-picker-modal").addEventListener("click", (e) => {
+ if (e.target.id === "src-picker-modal") closeSrcPicker();
+});
+
+// ───── 拖拽上传到主区(目的地 = state.filesPath)─────
+// 用 enter/leave 计数避免子元素冒泡时 overlay 闪烁。
+let _dragDepth = 0;
+function _hasFiles(ev) {
+ const t = ev.dataTransfer;
+ if (!t) return false;
+ if (t.types && [...t.types].includes("Files")) return true;
+ return false;
+}
+$("pane-right").addEventListener("dragenter", (e) => {
+ if (!_hasFiles(e)) return;
+ e.preventDefault();
+ _dragDepth++;
+ $("file-droparea").classList.add("show");
+});
+$("pane-right").addEventListener("dragover", (e) => {
+ if (!_hasFiles(e)) return;
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "copy";
+});
+$("pane-right").addEventListener("dragleave", (e) => {
+ if (!_hasFiles(e)) return;
+ _dragDepth = Math.max(0, _dragDepth - 1);
+ if (_dragDepth === 0) $("file-droparea").classList.remove("show");
+});
+$("pane-right").addEventListener("drop", async (e) => {
+ if (!_hasFiles(e)) return;
+ e.preventDefault();
+ _dragDepth = 0;
+ $("file-droparea").classList.remove("show");
+ const files = Array.from(e.dataTransfer.files || []);
+ if (!files.length) return;
+ await uploadFiles(files);
});
// 工具调用返回时,右侧文件可能有新增/修改 — debounce 500ms 刷新,避免每次 tool_result 都 hit API
@@ -1585,10 +1623,9 @@ async function loadFiles() {
}
}
-// 切换文件面板浏览路径:重置多选(选中是 per-view 概念),然后加载
+// 切换文件面板浏览路径
function navFiles(newPath) {
state.filesPath = newPath || "";
- state.selectedFiles.clear();
loadFiles();
}
@@ -1614,28 +1651,19 @@ function renderFiles(data) {
if (!data.exists) {
$("file-list").innerHTML = `(目录尚未创建)
`;
state.entriesByRel = {};
- syncBulkBar();
return;
}
if (!data.entries.length) {
$("file-list").innerHTML = `(空目录)
`;
state.entriesByRel = {};
- syncBulkBar();
return;
}
state.entriesByRel = {};
for (const e of data.entries) state.entriesByRel[e.rel] = e;
- // refresh 后剔除已不在当前视图的选中项(避免幽灵选中跨视图残留)
- for (const r of [...state.selectedFiles]) {
- if (!(r in state.entriesByRel)) state.selectedFiles.delete(r);
- }
$("file-list").innerHTML = data.entries.map((e) => {
const cls = e.is_dir ? "ico-dir" : "ico-file";
- const sel = state.selectedFiles.has(e.rel);
- const rowCls = "file-row" + (sel ? " selected" : "");
return `
-
-
+
${escapeHtml(e.name)}
@@ -1660,35 +1688,6 @@ function renderFiles(data) {
showMenu(btn, fileMenuItems(e));
};
});
- $("file-list").querySelectorAll(".row-cb").forEach((cb) => {
- cb.onclick = (ev) => ev.stopPropagation(); // 防 row 命中导航
- cb.onchange = (ev) => {
- const rel = cb.dataset.rel;
- if (cb.checked) state.selectedFiles.add(rel);
- else state.selectedFiles.delete(rel);
- const row = cb.closest(".file-row");
- if (row) row.classList.toggle("selected", cb.checked);
- syncBulkBar();
- };
- });
- syncBulkBar();
-}
-
-// 同步多选 toolbar 显隐 + 计数 + 顶栏全选 checkbox(全选 / 半选 / 未选三态)
-function syncBulkBar() {
- const n = state.selectedFiles.size;
- $("files-bulk-count").textContent = String(n);
- $("files-bulkbar").classList.toggle("show", n > 0);
- const rels = Object.keys(state.entriesByRel || {});
- const cb = $("files-selall");
- if (!rels.length) {
- cb.checked = false; cb.indeterminate = false; cb.disabled = true;
- } else {
- cb.disabled = false;
- const selN = rels.reduce((acc, r) => acc + (state.selectedFiles.has(r) ? 1 : 0), 0);
- cb.checked = selN === rels.length;
- cb.indeterminate = selN > 0 && selN < rels.length;
- }
}
function fileMenuItems(e) {
@@ -1996,15 +1995,13 @@ $("file-preview-modal").addEventListener("click", (e) => {
});
document.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
- // 多模态共存:优先关靠前栈顶 — 目录选择(z 95)→ 文件预览(z 90)→ 新任务(z 80)
- if ($("dir-picker-modal").classList.contains("show")) { closeDirPicker(); return; }
+ // 多模态共存:优先关靠前栈顶 — 选入(z 95)→ 文件预览(z 90)→ 新任务(z 80)
+ if ($("src-picker-modal").classList.contains("show")) { closeSrcPicker(); return; }
if ($("file-preview-modal").classList.contains("show")) { closeFilePreview(); return; }
});
-async function uploadSelected() {
- const inp = $("upload-input");
- const files = Array.from(inp.files || []);
- if (!files.length) return;
+async function uploadFiles(files) {
+ if (!files || !files.length) return;
const fd = new FormData();
fd.append("path", state.filesPath || "");
for (const f of files) fd.append("files", f);
@@ -2021,6 +2018,14 @@ async function uploadSelected() {
await loadFiles();
} catch (e) {
alert("上传失败:" + e.message);
+ }
+}
+
+async function uploadSelected() {
+ const inp = $("upload-input");
+ const files = Array.from(inp.files || []);
+ try {
+ await uploadFiles(files);
} finally {
inp.value = ""; // 允许重新选同名文件
}