Files
CyberStrikeAI/web/static/js/chat-files.js
2026-03-21 22:38:48 +08:00

1268 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 对话附件chat_uploads文件管理
let chatFilesCache = [];
let chatFilesDisplayed = [];
let chatFilesEditRelativePath = '';
let chatFilesRenameRelativePath = '';
const CHAT_FILES_GROUP_STORAGE_KEY = 'csai_chat_files_group_by';
const CHAT_FILES_BROWSE_PATH_KEY = 'csai_chat_files_browse_path';
/** 按文件夹浏览模式下的当前路径(相对 chat_uploads 的段数组),如 ['2024-03-21','uuid'] */
let chatFilesBrowsePath = [];
/** 非空时下一次上传文件落到此相对路径chat_uploads 下目录),如 2026-03-21/uuid/sub */
let chatFilesPendingUploadDir = '';
/** 仅前端记录的「空目录」键 parentPath'' 表示 chat_uploads 根)-> 子目录名列表,与树合并以便 mkdir 后可见 */
const CHAT_FILES_SYNTHETIC_DIRS_KEY = 'csai_chat_files_synthetic_dirs';
let chatFilesSyntheticEmptyDirs = {};
function chatFilesLoadSyntheticDirsFromStorage() {
try {
const raw = localStorage.getItem(CHAT_FILES_SYNTHETIC_DIRS_KEY);
if (!raw) return;
const o = JSON.parse(raw);
if (o && typeof o === 'object') {
chatFilesSyntheticEmptyDirs = o;
}
} catch (e) {
chatFilesSyntheticEmptyDirs = {};
}
}
function chatFilesRegisterSyntheticEmptyDir(parentSegments, name) {
const p = parentSegments.join('/');
if (!chatFilesSyntheticEmptyDirs[p]) {
chatFilesSyntheticEmptyDirs[p] = [];
}
const arr = chatFilesSyntheticEmptyDirs[p];
if (arr.indexOf(name) === -1) {
arr.push(name);
}
try {
localStorage.setItem(CHAT_FILES_SYNTHETIC_DIRS_KEY, JSON.stringify(chatFilesSyntheticEmptyDirs));
} catch (e) {
/* ignore */
}
}
function chatFilesRemoveSyntheticDirSubtree(relPathUnderRoot) {
const rel = String(relPathUnderRoot || '').replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
if (!rel) return;
const parts = rel.split('/').filter(function (x) {
return x.length > 0;
});
if (parts.length === 0) return;
const leaf = parts[parts.length - 1];
const parentKey = parts.slice(0, -1).join('/');
const arr = chatFilesSyntheticEmptyDirs[parentKey];
if (arr) {
const ix = arr.indexOf(leaf);
if (ix >= 0) arr.splice(ix, 1);
if (arr.length === 0) delete chatFilesSyntheticEmptyDirs[parentKey];
}
const prefix = rel + '/';
let k;
for (k in chatFilesSyntheticEmptyDirs) {
if (!Object.prototype.hasOwnProperty.call(chatFilesSyntheticEmptyDirs, k)) continue;
if (k === rel || k.indexOf(prefix) === 0) {
delete chatFilesSyntheticEmptyDirs[k];
}
}
try {
localStorage.setItem(CHAT_FILES_SYNTHETIC_DIRS_KEY, JSON.stringify(chatFilesSyntheticEmptyDirs));
} catch (e) {
/* ignore */
}
}
function chatFilesMergeSyntheticDirsIntoTree(root) {
function ensurePath(node, segments) {
let n = node;
let i;
for (i = 0; i < segments.length; i++) {
const s = segments[i];
if (!n.dirs[s]) n.dirs[s] = chatFilesTreeMakeNode();
n = n.dirs[s];
}
return n;
}
let k;
for (k in chatFilesSyntheticEmptyDirs) {
if (!Object.prototype.hasOwnProperty.call(chatFilesSyntheticEmptyDirs, k)) continue;
const names = chatFilesSyntheticEmptyDirs[k];
if (!Array.isArray(names)) continue;
const segs = k ? k.split('/').filter(function (x) {
return x.length > 0;
}) : [];
const node = ensurePath(root, segs);
let ni;
for (ni = 0; ni < names.length; ni++) {
const nm = names[ni];
if (!nm || typeof nm !== 'string') continue;
if (!node.dirs[nm]) node.dirs[nm] = chatFilesTreeMakeNode();
}
}
}
function chatFilesLoadBrowsePathFromStorage() {
try {
const raw = localStorage.getItem(CHAT_FILES_BROWSE_PATH_KEY);
if (!raw) {
chatFilesBrowsePath = [];
return;
}
const p = JSON.parse(raw);
if (Array.isArray(p) && p.every(function (x) {
return typeof x === 'string';
})) {
chatFilesBrowsePath = p;
}
} catch (e) {
chatFilesBrowsePath = [];
}
}
function chatFilesSetBrowsePath(path) {
chatFilesBrowsePath = path.slice();
try {
localStorage.setItem(CHAT_FILES_BROWSE_PATH_KEY, JSON.stringify(chatFilesBrowsePath));
} catch (e) {
/* ignore */
}
}
function chatFilesResolveTreeNode(root, path) {
let node = root;
let i;
for (i = 0; i < path.length; i++) {
const seg = path[i];
if (!node.dirs[seg]) return null;
node = node.dirs[seg];
}
return node;
}
function chatFilesNormalizeBrowsePathForTree(root) {
let path = chatFilesBrowsePath.slice();
while (path.length > 0 && !chatFilesResolveTreeNode(root, path)) {
path.pop();
}
if (path.length !== chatFilesBrowsePath.length) {
chatFilesSetBrowsePath(path);
}
}
function initChatFilesPage() {
chatFilesLoadBrowsePathFromStorage();
chatFilesLoadSyntheticDirsFromStorage();
ensureChatFilesDocClickClose();
const sel = document.getElementById('chat-files-group-by');
if (sel) {
try {
const v = localStorage.getItem(CHAT_FILES_GROUP_STORAGE_KEY);
if (v === 'none' || v === 'date' || v === 'conversation' || v === 'folder') {
sel.value = v;
}
} catch (e) {
/* ignore */
}
}
loadChatFilesPage();
}
function chatFilesCloseAllMenus() {
document.querySelectorAll('.chat-files-dropdown').forEach((el) => {
el.hidden = true;
el.style.position = '';
el.style.left = '';
el.style.top = '';
el.style.right = '';
el.style.minWidth = '';
el.style.zIndex = '';
el.classList.remove('chat-files-dropdown-fixed');
});
}
/**
* 「更多」菜单使用 fixed 定位,避免表格外层 overflow 把菜单裁成一条细线。
*/
function chatFilesToggleMoreMenu(ev, idx) {
if (ev) ev.stopPropagation();
const menu = document.getElementById('chat-files-menu-' + idx);
const btn = ev && ev.currentTarget;
if (!menu) return;
const opening = menu.hidden;
chatFilesCloseAllMenus();
if (!opening) return;
menu.hidden = false;
menu.classList.add('chat-files-dropdown-fixed');
if (!btn || typeof btn.getBoundingClientRect !== 'function') return;
requestAnimationFrame(() => {
const r = btn.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const margin = 8;
const minW = 220;
menu.style.boxSizing = 'border-box';
menu.style.position = 'fixed';
menu.style.zIndex = '5000';
menu.style.minWidth = minW + 'px';
menu.style.right = 'auto';
const w = Math.max(minW, menu.offsetWidth || minW);
let left = r.right - w;
if (left < margin) left = margin;
if (left + w > vw - margin) left = Math.max(margin, vw - margin - w);
menu.style.left = left + 'px';
const gap = 6;
let top = r.bottom + gap;
const estH = menu.offsetHeight || 120;
if (top + estH > vh - margin && r.top - gap - estH >= margin) {
top = r.top - gap - estH;
}
menu.style.top = top + 'px';
});
}
window.chatFilesCloseAllMenus = chatFilesCloseAllMenus;
window.chatFilesToggleMoreMenu = chatFilesToggleMoreMenu;
function ensureChatFilesDocClickClose() {
if (window.__chatFilesDocClose) return;
window.__chatFilesDocClose = true;
document.addEventListener('click', function (ev) {
if (ev.target.closest && ev.target.closest('.chat-files-dropdown-wrap')) return;
chatFilesCloseAllMenus();
});
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape') chatFilesCloseAllMenus();
});
window.addEventListener(
'scroll',
function () {
chatFilesCloseAllMenus();
},
true
);
window.addEventListener('resize', function () {
chatFilesCloseAllMenus();
});
}
async function loadChatFilesPage() {
const wrap = document.getElementById('chat-files-list-wrap');
if (!wrap) return;
wrap.classList.remove('chat-files-table-wrap--grouped');
wrap.classList.remove('chat-files-table-wrap--tree');
wrap.innerHTML = '<div class="loading-spinner" data-i18n="common.loading">加载中…</div>';
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(wrap);
}
const conv = document.getElementById('chat-files-filter-conv');
const convQ = conv ? conv.value.trim() : '';
let url = '/api/chat-uploads';
if (convQ) {
url += '?conversation=' + encodeURIComponent(convQ);
}
try {
const res = await apiFetch(url);
if (!res.ok) {
const t = await res.text();
throw new Error(t || res.status);
}
const data = await res.json();
chatFilesCache = Array.isArray(data.files) ? data.files : [];
renderChatFilesTable();
} catch (e) {
console.error(e);
wrap.classList.remove('chat-files-table-wrap--grouped');
wrap.classList.remove('chat-files-table-wrap--tree');
const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.errorLoad') : '加载失败';
wrap.innerHTML = '<div class="error-message">' + escapeHtml(msg + ': ' + (e.message || String(e))) + '</div>';
}
}
function chatFilesNameFilter(files) {
const el = document.getElementById('chat-files-filter-name');
const q = el ? el.value.trim().toLowerCase() : '';
if (!q) return files;
return files.filter(function (f) {
const name = (f.name || '').toLowerCase();
const sub = (f.subPath || '').toLowerCase();
return name.includes(q) || sub.includes(q);
});
}
/** 仅前端按文件名筛选,不重新请求 */
function chatFilesFilterNameOnInput() {
if (!chatFilesCache.length) return;
renderChatFilesTable();
}
function formatChatFileBytes(n) {
if (n < 1024) return n + ' B';
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
return (n / (1024 * 1024)).toFixed(1) + ' MB';
}
function chatFilesShowToast(message) {
const el = document.createElement('div');
el.className = 'chat-files-toast';
el.setAttribute('role', 'status');
el.textContent = message;
document.body.appendChild(el);
requestAnimationFrame(() => el.classList.add('chat-files-toast-visible'));
setTimeout(() => {
el.classList.remove('chat-files-toast-visible');
setTimeout(() => el.remove(), 300);
}, 2200);
}
async function chatFilesCopyText(text) {
try {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) {
/* fall through */
}
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
} catch (e2) {
return false;
}
}
async function copyChatFilePathIdx(idx) {
const f = chatFilesDisplayed[idx];
if (!f) return;
const text = (f.absolutePath && String(f.absolutePath).trim())
? String(f.absolutePath).trim()
: ('chat_uploads/' + String(f.relativePath || '').replace(/^\/+/, ''));
const ok = await chatFilesCopyText(text);
if (ok) {
const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.pathCopied') : '路径已复制,可粘贴到对话中引用';
chatFilesShowToast(msg);
} else {
const fail = (typeof window.t === 'function') ? window.t('common.copyFailed') : '复制失败';
alert(fail);
}
}
/** 常见二进制扩展名:此类文件无法在纯文本编辑器中打开 */
const CHAT_FILES_BINARY_EXT = new Set([
'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'tif', 'tiff', 'heic', 'heif', 'svgz',
'pdf', 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'zst',
'mp3', 'm4a', 'wav', 'ogg', 'flac', 'aac',
'mp4', 'avi', 'mkv', 'mov', 'wmv', 'webm', 'm4v',
'exe', 'dll', 'so', 'dylib', 'bin', 'app', 'dmg', 'pkg',
'woff', 'woff2', 'ttf', 'otf', 'eot',
'sqlite', 'db', 'sqlite3',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods',
'class', 'jar', 'war', 'apk', 'ipa',
'iso', 'img'
]);
function chatFileIsBinaryByName(fileName) {
if (!fileName || typeof fileName !== 'string') return false;
const i = fileName.lastIndexOf('.');
if (i < 0 || i === fileName.length - 1) return false;
const ext = fileName.slice(i + 1).toLowerCase();
return CHAT_FILES_BINARY_EXT.has(ext);
}
function chatFilesEditBlockedHint() {
return (typeof window.t === 'function')
? window.t('chatFilesPage.editBinaryHint')
: '图片、压缩包等二进制文件无法在此以文本方式编辑,请使用「下载」。';
}
function chatFilesAlertMessage(raw) {
const s = (raw == null) ? '' : String(raw).trim();
const lower = s.toLowerCase();
if (lower.includes('binary file not editable') || lower.includes('binary')) {
return chatFilesEditBlockedHint();
}
if (lower.includes('file too large') || lower.includes('entity too large') || lower.includes('413')) {
return (typeof window.t === 'function') ? window.t('chatFilesPage.editTooLarge') : '文件过大,无法在此编辑。';
}
return s || ((typeof window.t === 'function') ? window.t('chatFilesPage.errorGeneric') : '操作失败');
}
function chatFilesGetGroupByMode() {
const sel = document.getElementById('chat-files-group-by');
const v = sel ? sel.value : 'none';
if (v === 'date' || v === 'conversation' || v === 'folder') return v;
return 'none';
}
function chatFilesGroupByChange() {
const sel = document.getElementById('chat-files-group-by');
if (sel) {
try {
localStorage.setItem(CHAT_FILES_GROUP_STORAGE_KEY, sel.value);
} catch (e) {
/* ignore */
}
}
renderChatFilesTable();
}
function chatFilesCompareDateKeysDesc(a, b) {
const as = String(a);
const bs = String(b);
if (as === '—' && bs !== '—') return 1;
if (bs === '—' && as !== '—') return -1;
return bs.localeCompare(as);
}
/** 目录树节点dirs[段名] -> 子节点files: { idx, name }[] */
function chatFilesTreeMakeNode() {
return { dirs: {}, files: [] };
}
function chatFilesTreeInsertFile(root, f, idx) {
const rp = String(f.relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
if (!rp) return;
const parts = rp.split('/').filter(function (p) {
return p.length > 0;
});
if (parts.length < 2) return;
let node = root;
for (let i = 0; i < parts.length - 1; i++) {
const seg = parts[i];
if (!node.dirs[seg]) node.dirs[seg] = chatFilesTreeMakeNode();
node = node.dirs[seg];
}
node.files.push({ idx: idx, name: parts[parts.length - 1] });
}
function chatFilesBuildTree(files) {
const root = chatFilesTreeMakeNode();
files.forEach(function (f, idx) {
chatFilesTreeInsertFile(root, f, idx);
});
return root;
}
function chatFilesTreeRootMerged() {
const root = chatFilesBuildTree(chatFilesDisplayed);
chatFilesMergeSyntheticDirsIntoTree(root);
return root;
}
function chatFilesTreeNodeMaxMod(node) {
let m = 0;
let i;
for (i = 0; i < node.files.length; i++) {
const f = chatFilesDisplayed[node.files[i].idx];
m = Math.max(m, (f && f.modifiedUnix) || 0);
}
const keys = Object.keys(node.dirs);
for (i = 0; i < keys.length; i++) {
m = Math.max(m, chatFilesTreeNodeMaxMod(node.dirs[keys[i]]));
}
return m;
}
function chatFilesTreeSortDirKeys(node, keys) {
return keys.slice().sort(function (a, b) {
const ma = chatFilesTreeNodeMaxMod(node.dirs[a]);
const mb = chatFilesTreeNodeMaxMod(node.dirs[b]);
if (mb !== ma) return mb - ma;
return String(a).localeCompare(String(b));
});
}
function chatFilesBuildGroups(files, mode) {
const map = new Map();
files.forEach(function (f, idx) {
const key = mode === 'date' ? (f.date || '—') : (f.conversationId || '—');
if (!map.has(key)) {
map.set(key, { key: key, items: [] });
}
map.get(key).items.push({ idx: idx, f: f });
});
const groups = Array.from(map.values());
groups.forEach(function (g) {
g.items.sort(function (a, b) {
return (b.f.modifiedUnix || 0) - (a.f.modifiedUnix || 0);
});
});
if (mode === 'date') {
groups.sort(function (a, b) {
return chatFilesCompareDateKeysDesc(a.key, b.key);
});
} else {
groups.sort(function (a, b) {
const ma = Math.max.apply(
null,
a.items.map(function (x) {
return x.f.modifiedUnix || 0;
})
);
const mb = Math.max.apply(
null,
b.items.map(function (x) {
return x.f.modifiedUnix || 0;
})
);
return mb - ma;
});
}
return groups;
}
/** 分组标题:会话 ID 过长时缩短展示,完整值放在 title */
function chatFilesGroupHeadingConversation(key) {
const c = key == null ? '' : String(key);
if (c === '' || c === '—') {
return { text: '—', title: '' };
}
if (typeof window.t === 'function') {
if (c === '_manual') {
return { text: window.t('chatFilesPage.convManual'), title: '_manual' };
}
if (c === '_new') {
return { text: window.t('chatFilesPage.convNew'), title: '_new' };
}
}
if (c.length > 36) {
return { text: c.slice(0, 8) + '…' + c.slice(-6), title: c };
}
return { text: c, title: c };
}
function renderChatFilesTable() {
const wrap = document.getElementById('chat-files-list-wrap');
if (!wrap) return;
chatFilesDisplayed = chatFilesNameFilter(chatFilesCache);
const emptyMsg = (typeof window.t === 'function') ? window.t('chatFilesPage.empty') : '暂无文件';
if (!chatFilesDisplayed.length) {
wrap.classList.remove('chat-files-table-wrap--grouped');
wrap.classList.remove('chat-files-table-wrap--tree');
wrap.innerHTML = '<div class="empty-state" data-i18n="chatFilesPage.empty">' + escapeHtml(emptyMsg) + '</div>';
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(wrap);
}
return;
}
const thDate = (typeof window.t === 'function') ? window.t('chatFilesPage.colDate') : '日期';
const thConv = (typeof window.t === 'function') ? window.t('chatFilesPage.colConversation') : '会话';
const thSubPath = (typeof window.t === 'function') ? window.t('chatFilesPage.colSubPath') : '子路径';
const thName = (typeof window.t === 'function') ? window.t('chatFilesPage.colName') : '文件名';
const thSize = (typeof window.t === 'function') ? window.t('chatFilesPage.colSize') : '大小';
const thModified = (typeof window.t === 'function') ? window.t('chatFilesPage.colModified') : '修改时间';
const thActions = (typeof window.t === 'function') ? window.t('chatFilesPage.colActions') : '操作';
const svgCopy = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
const svgDownload = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
const svgMore = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>';
const svgFolder = '<svg class="chat-files-tree-icon" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
const svgFile = '<svg class="chat-files-tree-file-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
const tCopyTitle = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.copyPathTitle') : '复制服务器上的绝对路径,可粘贴到对话中引用');
const tDlTitle = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.download') : '下载');
const tMoreTitle = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.moreActions') : '更多操作');
function rowHtml(f, idx) {
const rp = f.relativePath || '';
const pathForTitle = (f.absolutePath && String(f.absolutePath).trim()) ? String(f.absolutePath).trim() : rp;
const nameEsc = escapeHtml(f.name || '');
const conv = f.conversationId || '';
const convEsc = escapeHtml(conv);
const dt = f.modifiedUnix ? new Date(f.modifiedUnix * 1000).toLocaleString() : '—';
const canOpenChat = conv && conv !== '_manual' && conv !== '_new';
const bin = chatFileIsBinaryByName(f.name);
const editHint = escapeHtml(chatFilesEditBlockedHint());
const editUnavailable = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.editUnavailable')) : '不可编辑';
const tEdit = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.edit')) : '编辑';
const tOpenChat = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.openChat')) : '打开对话';
const tRename = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.rename')) : '重命名';
const tDelete = (typeof window.t === 'function') ? escapeHtml(window.t('common.delete')) : '删除';
const menuParts = [];
if (canOpenChat) {
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesConversationIdx(${idx});">${tOpenChat}</button>`);
}
if (!bin) {
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesEditIdx(${idx});">${tEdit}</button>`);
} else {
menuParts.push(`<div class="chat-files-dropdown-item is-disabled" title="${editHint}">${editUnavailable}</div>`);
}
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesRenameIdx(${idx});">${tRename}</button>`);
menuParts.push(`<button type="button" class="chat-files-dropdown-item is-danger" onclick="chatFilesCloseAllMenus(); deleteChatFileIdx(${idx});">${tDelete}</button>`);
const menuHtml = menuParts.join('');
const subRaw = (f.subPath && String(f.subPath).trim()) ? String(f.subPath).trim() : '';
const rootLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.folderRoot') : '(根目录)';
let subCellInner;
if (subRaw) {
const segs = subRaw.split('/').filter(function (s) {
return s.length > 0;
});
subCellInner = '<span class="chat-files-path-breadcrumb">' + segs.map(function (seg, i) {
return (i > 0 ? '<span class="chat-files-path-sep"></span>' : '') +
'<span class="chat-files-path-crumb">' + escapeHtml(seg) + '</span>';
}).join('') + '</span>';
} else {
subCellInner = '<span class="chat-files-path-root">' + escapeHtml(rootLabel) + '</span>';
}
return `<tr>
<td>${escapeHtml(f.date || '—')}</td>
<td class="chat-files-cell-conv"><code title="${convEsc}">${convEsc}</code></td>
<td class="chat-files-cell-subpath" title="${escapeHtml(subRaw || '')}">${subCellInner}</td>
<td class="chat-files-cell-name" title="${escapeHtml(pathForTitle)}">${nameEsc}</td>
<td>${formatChatFileBytes(f.size || 0)}</td>
<td>${escapeHtml(dt)}</td>
<td class="chat-files-actions">
<div class="chat-files-action-bar">
<button type="button" class="btn-icon" title="${tCopyTitle}" onclick="copyChatFilePathIdx(${idx})">${svgCopy}</button>
<button type="button" class="btn-icon" title="${tDlTitle}" onclick="downloadChatFileIdx(${idx})">${svgDownload}</button>
<div class="chat-files-dropdown-wrap">
<button type="button" class="btn-icon" title="${tMoreTitle}" aria-haspopup="true" onclick="chatFilesToggleMoreMenu(event, ${idx})">${svgMore}</button>
<div class="chat-files-dropdown" id="chat-files-menu-${idx}" hidden>${menuHtml}</div>
</div>
</div>
</td>
</tr>`;
}
const theadHtml = `<thead><tr>
<th>${escapeHtml(thDate)}</th>
<th>${escapeHtml(thConv)}</th>
<th>${escapeHtml(thSubPath)}</th>
<th>${escapeHtml(thName)}</th>
<th>${escapeHtml(thSize)}</th>
<th>${escapeHtml(thModified)}</th>
<th>${escapeHtml(thActions)}</th>
</tr></thead>`;
const theadCompact = `<thead><tr>
<th>${escapeHtml(thName)}</th>
<th>${escapeHtml(thSize)}</th>
<th>${escapeHtml(thModified)}</th>
<th>${escapeHtml(thActions)}</th>
</tr></thead>`;
const groupMode = chatFilesGetGroupByMode();
let innerHtml;
if (groupMode === 'folder') {
const root = chatFilesTreeRootMerged();
chatFilesNormalizeBrowsePathForTree(root);
const node = chatFilesResolveTreeNode(root, chatFilesBrowsePath);
const current = node || root;
const dirKeys = chatFilesTreeSortDirKeys(current, Object.keys(current.dirs));
current.files.sort(function (a, b) {
return (chatFilesDisplayed[b.idx].modifiedUnix || 0) - (chatFilesDisplayed[a.idx].modifiedUnix || 0);
});
const tRoot = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseRoot') : 'chat_uploads');
const tUp = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseUp') : '上级');
const tMkdir = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.newFolderButton') : '新建文件夹');
const tEmpty = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.folderEmpty') : '此文件夹为空');
const tCopyFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.copyFolderPathTitle') : '复制 chat_uploads 下相对路径');
const tEnter = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.enterFolderTitle') : '进入');
let breadcrumbHtml = '<nav class="chat-files-breadcrumb" aria-label="breadcrumb">';
breadcrumbHtml += '<button type="button" class="chat-files-breadcrumb-link" onclick="chatFilesNavigateBreadcrumb(-1)">' + tRoot + '</button>';
let bi;
for (bi = 0; bi < chatFilesBrowsePath.length; bi++) {
const seg = chatFilesBrowsePath[bi];
const isLast = bi === chatFilesBrowsePath.length - 1;
breadcrumbHtml += '<span class="chat-files-breadcrumb-sep">/</span>';
if (isLast) {
breadcrumbHtml += '<span class="chat-files-breadcrumb-current">' + escapeHtml(seg) + '</span>';
} else {
breadcrumbHtml += '<button type="button" class="chat-files-breadcrumb-link" onclick="chatFilesNavigateBreadcrumb(' + bi + ')">' + escapeHtml(seg) + '</button>';
}
}
breadcrumbHtml += '</nav>';
const upDisabled = chatFilesBrowsePath.length === 0 ? ' disabled' : '';
const toolbarHtml = '<div class="chat-files-browse-toolbar">' + breadcrumbHtml +
'<button type="button" class="btn-secondary chat-files-mkdir-btn" onclick="openChatFilesMkdirModal()">' + tMkdir + '</button>' +
'<button type="button" class="btn-secondary chat-files-browse-up"' + upDisabled + ' onclick="chatFilesNavigateUp()">' + tUp + '</button></div>';
const svgTrash = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
const svgUploadToFolder = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>';
const tDeleteFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.deleteFolderTitle') : '删除文件夹');
const tUploadToFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.uploadToFolderTitle') : '上传到此文件夹');
function rowHtmlBrowseFolder(name) {
const nameAttr = encodeURIComponent(String(name));
const relToFolder = chatFilesBrowsePath.concat([name]).join('/');
const uploadDirAttr = encodeURIComponent(relToFolder);
return `<tr class="chat-files-tr-folder chat-files-tr-folder--nav" role="button" tabindex="0" data-chat-folder-name="${nameAttr}" onclick="chatFilesOnFolderRowClick(event)" onkeydown="chatFilesOnFolderRowKeydown(event)">
<td class="chat-files-tree-name-cell chat-files-tree-name-cell--folder" title="${tEnter}">
<span class="chat-files-tree-name-inner">${svgFolder}<span class="chat-files-tree-name-text">${escapeHtml(name)}</span></span>
</td>
<td class="chat-files-tree-muted">—</td>
<td class="chat-files-tree-muted">—</td>
<td class="chat-files-actions" data-chat-files-stop="true" onclick="event.stopPropagation()">
<div class="chat-files-action-bar">
<button type="button" class="btn-icon" title="${tUploadToFolder}" data-upload-dir="${uploadDirAttr}" onclick="chatFilesUploadToFolderClick(event, this)">${svgUploadToFolder}</button>
<button type="button" class="btn-icon" title="${tCopyFolder}" data-chat-folder-name="${nameAttr}" onclick="chatFilesCopyFolderPathFromBtn(event, this)">${svgCopy}</button>
<button type="button" class="btn-icon btn-danger" title="${tDeleteFolder}" data-chat-folder-name="${nameAttr}" onclick="chatFilesDeleteFolderFromBtn(event, this)">${svgTrash}</button>
</div>
</td>
</tr>`;
}
function rowHtmlTreeFile(f, idx) {
const pathForTitle = (f.absolutePath && String(f.absolutePath).trim()) ? String(f.absolutePath).trim() : (f.relativePath || '');
const nameEsc = escapeHtml(f.name || '');
const dt = f.modifiedUnix ? new Date(f.modifiedUnix * 1000).toLocaleString() : '—';
const conv = f.conversationId || '';
const canOpenChat = conv && conv !== '_manual' && conv !== '_new';
const bin = chatFileIsBinaryByName(f.name);
const editHint = escapeHtml(chatFilesEditBlockedHint());
const editUnavailable = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.editUnavailable')) : '不可编辑';
const tEdit = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.edit')) : '编辑';
const tOpenChat = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.openChat')) : '打开对话';
const tRename = (typeof window.t === 'function') ? escapeHtml(window.t('chatFilesPage.rename')) : '重命名';
const tDelete = (typeof window.t === 'function') ? escapeHtml(window.t('common.delete')) : '删除';
const menuParts = [];
if (canOpenChat) {
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesConversationIdx(${idx});">${tOpenChat}</button>`);
}
if (!bin) {
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesEditIdx(${idx});">${tEdit}</button>`);
} else {
menuParts.push(`<div class="chat-files-dropdown-item is-disabled" title="${editHint}">${editUnavailable}</div>`);
}
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesRenameIdx(${idx});">${tRename}</button>`);
menuParts.push(`<button type="button" class="chat-files-dropdown-item is-danger" onclick="chatFilesCloseAllMenus(); deleteChatFileIdx(${idx});">${tDelete}</button>`);
const menuHtml = menuParts.join('');
return `<tr class="chat-files-tr-file">
<td class="chat-files-tree-name-cell" title="${escapeHtml(pathForTitle)}">
<span class="chat-files-tree-name-inner">${svgFile}<span class="chat-files-tree-name-text">${nameEsc}</span></span>
</td>
<td>${formatChatFileBytes(f.size || 0)}</td>
<td>${escapeHtml(dt)}</td>
<td class="chat-files-actions">
<div class="chat-files-action-bar">
<button type="button" class="btn-icon" title="${tCopyTitle}" onclick="copyChatFilePathIdx(${idx})">${svgCopy}</button>
<button type="button" class="btn-icon" title="${tDlTitle}" onclick="downloadChatFileIdx(${idx})">${svgDownload}</button>
<div class="chat-files-dropdown-wrap">
<button type="button" class="btn-icon" title="${tMoreTitle}" aria-haspopup="true" onclick="chatFilesToggleMoreMenu(event, ${idx})">${svgMore}</button>
<div class="chat-files-dropdown" id="chat-files-menu-${idx}" hidden>${menuHtml}</div>
</div>
</div>
</td>
</tr>`;
}
const folderRows = dirKeys.map(rowHtmlBrowseFolder).join('');
const fileRows = current.files.map(function (item) {
return rowHtmlTreeFile(chatFilesDisplayed[item.idx], item.idx);
}).join('');
let bodyRows = folderRows + fileRows;
if (!bodyRows) {
bodyRows = '<tr class="chat-files-tr-empty"><td colspan="4" class="chat-files-folder-empty">' + tEmpty + '</td></tr>';
}
innerHtml = '<div class="chat-files-browse-wrap">' + toolbarHtml + '<table class="chat-files-table chat-files-table--tree-flat">' + theadCompact + '<tbody>' + bodyRows + '</tbody></table></div>';
} else if (groupMode === 'none') {
const rows = chatFilesDisplayed.map(function (f, idx) {
return rowHtml(f, idx);
}).join('');
innerHtml = `<table class="chat-files-table">${theadHtml}<tbody>${rows}</tbody></table>`;
} else {
const groups = chatFilesBuildGroups(chatFilesDisplayed, groupMode);
const blocks = groups.map(function (g) {
const rows = g.items.map(function (item) {
return rowHtml(item.f, item.idx);
}).join('');
let summaryMain;
let summaryTitleAttr = '';
if (groupMode === 'date') {
summaryMain = escapeHtml(String(g.key));
} else {
const h = chatFilesGroupHeadingConversation(g.key);
summaryMain = escapeHtml(h.text);
summaryTitleAttr = h.title ? ' title="' + escapeHtml(h.title) + '"' : '';
}
const n = g.items.length;
const countLabel = (typeof window.t === 'function')
? escapeHtml(window.t('chatFilesPage.groupCount', { count: n }))
: escapeHtml(String(n));
return `<details class="chat-files-group" open>
<summary class="chat-files-group-summary"${summaryTitleAttr}>
<span class="chat-files-group-title">${summaryMain}</span>
<span class="chat-files-group-count">${countLabel}</span>
</summary>
<div class="chat-files-group-body">
<table class="chat-files-table">${theadHtml}<tbody>${rows}</tbody></table>
</div>
</details>`;
}).join('');
innerHtml = `<div class="chat-files-grouped">${blocks}</div>`;
}
ensureChatFilesDocClickClose();
wrap.innerHTML = innerHtml;
wrap.classList.toggle('chat-files-table-wrap--grouped', groupMode !== 'none' && groupMode !== 'folder');
wrap.classList.toggle('chat-files-table-wrap--tree', groupMode === 'folder');
}
window.chatFilesGroupByChange = chatFilesGroupByChange;
function chatFilesNavigateInto(name) {
const root = chatFilesTreeRootMerged();
chatFilesNormalizeBrowsePathForTree(root);
const next = chatFilesBrowsePath.concat([name]);
if (!chatFilesResolveTreeNode(root, next)) return;
chatFilesSetBrowsePath(next);
renderChatFilesTable();
}
function chatFilesNavigateBreadcrumb(level) {
const root = chatFilesTreeRootMerged();
chatFilesNormalizeBrowsePathForTree(root);
if (level < 0) {
chatFilesSetBrowsePath([]);
} else {
chatFilesSetBrowsePath(chatFilesBrowsePath.slice(0, level + 1));
}
renderChatFilesTable();
}
function chatFilesNavigateUp() {
if (chatFilesBrowsePath.length === 0) return;
chatFilesSetBrowsePath(chatFilesBrowsePath.slice(0, -1));
renderChatFilesTable();
}
function chatFilesFolderNameFromRow(el) {
if (!el || !el.getAttribute) return '';
try {
return decodeURIComponent(String(el.getAttribute('data-chat-folder-name') || ''));
} catch (e) {
return '';
}
}
function chatFilesOnFolderRowClick(ev) {
if (ev.target.closest && ev.target.closest('[data-chat-files-stop]')) return;
const name = chatFilesFolderNameFromRow(ev.currentTarget);
if (!name) return;
chatFilesNavigateInto(name);
}
function chatFilesOnFolderRowKeydown(ev) {
if (ev.key !== 'Enter' && ev.key !== ' ') return;
ev.preventDefault();
const name = chatFilesFolderNameFromRow(ev.currentTarget);
if (!name) return;
chatFilesNavigateInto(name);
}
function chatFilesCopyFolderPathFromBtn(ev, btn) {
if (ev) ev.stopPropagation();
const name = chatFilesFolderNameFromRow(btn);
if (!name) return;
copyChatFolderPathFromBrowse(name);
}
async function deleteChatFolderFromBrowse(folderName) {
const segs = chatFilesBrowsePath.concat([folderName]);
const rel = segs.join('/');
const q = (typeof window.t === 'function') ? window.t('chatFilesPage.confirmDeleteFolder') : '确定删除该文件夹及其中的全部文件?';
if (!confirm(q)) return;
try {
const res = await apiFetch('/api/chat-uploads', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: rel })
});
if (!res.ok) {
throw new Error(await res.text());
}
chatFilesRemoveSyntheticDirSubtree(rel);
loadChatFilesPage();
} catch (e) {
alert((e && e.message) ? e.message : String(e));
}
}
function chatFilesDeleteFolderFromBtn(ev, btn) {
if (ev) ev.stopPropagation();
const name = chatFilesFolderNameFromRow(btn);
if (!name) return;
deleteChatFolderFromBrowse(name);
}
async function copyChatFolderPathFromBrowse(folderName) {
const segs = chatFilesBrowsePath.concat([folderName]);
const rel = segs.join('/');
const text = rel ? ('chat_uploads/' + rel.replace(/^\/+/, '')) : 'chat_uploads';
const ok = await chatFilesCopyText(text);
if (ok) {
const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.folderPathCopied') : '目录路径已复制';
chatFilesShowToast(msg);
} else {
const fail = (typeof window.t === 'function') ? window.t('common.copyFailed') : '复制失败';
alert(fail);
}
}
window.chatFilesNavigateInto = chatFilesNavigateInto;
window.chatFilesNavigateBreadcrumb = chatFilesNavigateBreadcrumb;
window.chatFilesNavigateUp = chatFilesNavigateUp;
window.chatFilesOnFolderRowClick = chatFilesOnFolderRowClick;
window.copyChatFolderPathFromBrowse = copyChatFolderPathFromBrowse;
window.chatFilesOnFolderRowKeydown = chatFilesOnFolderRowKeydown;
window.chatFilesCopyFolderPathFromBtn = chatFilesCopyFolderPathFromBtn;
window.chatFilesDeleteFolderFromBtn = chatFilesDeleteFolderFromBtn;
window.chatFilesOpenUploadPicker = chatFilesOpenUploadPicker;
window.chatFilesUploadToFolderClick = chatFilesUploadToFolderClick;
window.openChatFilesMkdirModal = openChatFilesMkdirModal;
window.closeChatFilesMkdirModal = closeChatFilesMkdirModal;
window.submitChatFilesMkdir = submitChatFilesMkdir;
function openChatFilesConversationIdx(idx) {
const f = chatFilesDisplayed[idx];
if (!f || !f.conversationId) return;
openChatFilesConversation(f.conversationId);
}
function downloadChatFileIdx(idx) {
const f = chatFilesDisplayed[idx];
if (!f) return;
downloadChatFile(f.relativePath, f.name);
}
function openChatFilesEditIdx(idx) {
const f = chatFilesDisplayed[idx];
if (!f) return;
if (chatFileIsBinaryByName(f.name)) {
alert(chatFilesEditBlockedHint());
return;
}
openChatFilesEdit(f.relativePath);
}
function openChatFilesRenameIdx(idx) {
const f = chatFilesDisplayed[idx];
if (!f) return;
openChatFilesRename(f.relativePath, f.name);
}
function deleteChatFileIdx(idx) {
const f = chatFilesDisplayed[idx];
if (!f) return;
deleteChatFile(f.relativePath);
}
function openChatFilesConversation(conversationId) {
if (!conversationId) return;
window.location.hash = 'chat?conversation=' + encodeURIComponent(conversationId);
if (typeof switchPage === 'function') {
switchPage('chat');
}
setTimeout(() => {
if (typeof loadConversation === 'function') {
loadConversation(conversationId);
}
}, 400);
}
async function downloadChatFile(relativePath, filename) {
try {
const url = '/api/chat-uploads/download?path=' + encodeURIComponent(relativePath);
const res = await apiFetch(url);
if (!res.ok) {
throw new Error(await res.text());
}
const blob = await res.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename || 'download';
a.click();
URL.revokeObjectURL(a.href);
} catch (e) {
alert((e && e.message) ? e.message : String(e));
}
}
async function deleteChatFile(relativePath) {
const q = (typeof window.t === 'function') ? window.t('chatFilesPage.confirmDelete') : '确定删除该文件?';
if (!confirm(q)) return;
try {
const res = await apiFetch('/api/chat-uploads', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: relativePath })
});
if (!res.ok) {
throw new Error(await res.text());
}
loadChatFilesPage();
} catch (e) {
alert((e && e.message) ? e.message : String(e));
}
}
async function openChatFilesEdit(relativePath) {
chatFilesEditRelativePath = relativePath;
const pathEl = document.getElementById('chat-files-edit-path');
const ta = document.getElementById('chat-files-edit-textarea');
const modal = document.getElementById('chat-files-edit-modal');
if (pathEl) pathEl.textContent = relativePath;
if (ta) ta.value = '';
if (modal) modal.style.display = 'block';
try {
const res = await apiFetch('/api/chat-uploads/content?path=' + encodeURIComponent(relativePath));
if (!res.ok) {
let errText = '';
try {
const err = await res.json();
errText = err.error || JSON.stringify(err);
} catch (e2) {
errText = await res.text();
}
throw new Error(errText || res.status);
}
const data = await res.json();
if (ta) ta.value = data.content != null ? String(data.content) : '';
} catch (e) {
if (modal) modal.style.display = 'none';
alert(chatFilesAlertMessage(e && e.message));
}
}
function closeChatFilesEditModal() {
const modal = document.getElementById('chat-files-edit-modal');
if (modal) modal.style.display = 'none';
chatFilesEditRelativePath = '';
}
async function saveChatFilesEdit() {
const ta = document.getElementById('chat-files-edit-textarea');
if (!ta || !chatFilesEditRelativePath) return;
try {
const res = await apiFetch('/api/chat-uploads/content', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: chatFilesEditRelativePath, content: ta.value })
});
if (!res.ok) {
throw new Error(await res.text());
}
closeChatFilesEditModal();
loadChatFilesPage();
} catch (e) {
alert(chatFilesAlertMessage(e && e.message));
}
}
function openChatFilesRename(relativePath, currentName) {
chatFilesRenameRelativePath = relativePath;
const input = document.getElementById('chat-files-rename-input');
const modal = document.getElementById('chat-files-rename-modal');
if (input) input.value = currentName || '';
if (modal) modal.style.display = 'block';
setTimeout(() => { if (input) input.focus(); }, 100);
}
function closeChatFilesRenameModal() {
const modal = document.getElementById('chat-files-rename-modal');
if (modal) modal.style.display = 'none';
chatFilesRenameRelativePath = '';
}
async function submitChatFilesRename() {
const input = document.getElementById('chat-files-rename-input');
const newName = input ? input.value.trim() : '';
if (!newName || !chatFilesRenameRelativePath) {
closeChatFilesRenameModal();
return;
}
try {
const res = await apiFetch('/api/chat-uploads/rename', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: chatFilesRenameRelativePath, newName: newName })
});
if (!res.ok) {
throw new Error(await res.text());
}
closeChatFilesRenameModal();
loadChatFilesPage();
} catch (e) {
alert((e && e.message) ? e.message : String(e));
}
}
function openChatFilesMkdirModal() {
if (chatFilesGetGroupByMode() !== 'folder') return;
const hint = document.getElementById('chat-files-mkdir-parent-hint');
const input = document.getElementById('chat-files-mkdir-input');
const modal = document.getElementById('chat-files-mkdir-modal');
const p = chatFilesBrowsePath.join('/');
if (hint) hint.textContent = p ? ('chat_uploads/' + p) : 'chat_uploads';
if (input) input.value = '';
if (modal) modal.style.display = 'block';
if (modal && typeof window.applyTranslations === 'function') {
window.applyTranslations(modal);
}
setTimeout(() => {
if (input) input.focus();
}, 100);
}
function closeChatFilesMkdirModal() {
const modal = document.getElementById('chat-files-mkdir-modal');
if (modal) modal.style.display = 'none';
const input = document.getElementById('chat-files-mkdir-input');
if (input) input.value = '';
}
async function submitChatFilesMkdir() {
const input = document.getElementById('chat-files-mkdir-input');
const name = input ? String(input.value).trim() : '';
if (!name) {
closeChatFilesMkdirModal();
return;
}
if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
const msg = (typeof window.t === 'function')
? window.t('chatFilesPage.mkdirInvalidName')
: '名称无效';
alert(msg);
return;
}
const parent = chatFilesBrowsePath.join('/');
try {
const res = await apiFetch('/api/chat-uploads/mkdir', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ parent: parent, name: name })
});
if (!res.ok) {
let errText = '';
try {
const j = await res.json();
errText = j.error || JSON.stringify(j);
} catch (e2) {
errText = await res.text();
}
if (res.status === 409) {
const msg = (typeof window.t === 'function')
? window.t('chatFilesPage.mkdirExists')
: errText;
alert(msg);
return;
}
throw new Error(errText || String(res.status));
}
chatFilesRegisterSyntheticEmptyDir(chatFilesBrowsePath.slice(), name);
closeChatFilesMkdirModal();
loadChatFilesPage();
const okMsg = (typeof window.t === 'function')
? window.t('chatFilesPage.mkdirOk')
: '文件夹已创建';
chatFilesShowToast(okMsg);
} catch (e) {
alert((e && e.message) ? e.message : String(e));
}
}
function chatFilesOpenUploadPicker() {
if (chatFilesGetGroupByMode() === 'folder') {
chatFilesPendingUploadDir = chatFilesBrowsePath.join('/');
} else {
chatFilesPendingUploadDir = '';
}
const inp = document.getElementById('chat-files-upload-input');
if (inp) inp.click();
}
function chatFilesUploadToFolderClick(ev, btn) {
if (ev) ev.stopPropagation();
const raw = btn.getAttribute('data-upload-dir');
if (!raw) return;
try {
chatFilesPendingUploadDir = decodeURIComponent(raw);
} catch (e) {
chatFilesPendingUploadDir = '';
return;
}
const inp = document.getElementById('chat-files-upload-input');
if (inp) inp.click();
}
async function onChatFilesUploadPick(ev) {
const input = ev.target;
const file = input && input.files && input.files[0];
if (!file) return;
const form = new FormData();
form.append('file', file);
const pendingDir = chatFilesPendingUploadDir;
chatFilesPendingUploadDir = '';
if (pendingDir) {
form.append('relativeDir', pendingDir);
} else {
const conv = document.getElementById('chat-files-filter-conv');
if (conv && conv.value.trim()) {
form.append('conversationId', conv.value.trim());
}
}
try {
const res = await apiFetch('/api/chat-uploads', { method: 'POST', body: form });
if (!res.ok) {
throw new Error(await res.text());
}
const data = await res.json().catch(() => ({}));
loadChatFilesPage();
if (data && data.ok) {
const msg = (typeof window.t === 'function')
? window.t('chatFilesPage.uploadOkHint')
: '上传成功。在列表中点击「复制路径」即可粘贴到对话中引用。';
chatFilesShowToast(msg);
}
} catch (e) {
alert((e && e.message) ? e.message : String(e));
} finally {
input.value = '';
}
}
// 语言切换后重新渲染列表:表头与「更多」菜单由 JS 拼接,无 data-i18n需用当前语言的 t() 再生成一遍
document.addEventListener('languagechange', function () {
if (typeof window.currentPage !== 'function') return;
if (window.currentPage() !== 'chat-files') return;
if (typeof renderChatFilesTable === 'function') {
renderChatFilesTable();
}
});