Add files via upload

This commit is contained in:
公明
2026-07-22 16:18:33 +08:00
committed by GitHub
parent fbef2afd68
commit 3f2984b7c6
5 changed files with 482 additions and 61 deletions
+387 -54
View File
@@ -6,18 +6,26 @@ let chatFilesFoldersCache = [];
let chatFilesDisplayed = [];
let chatFilesEditRelativePath = '';
let chatFilesRenameRelativePath = '';
let chatFilesTotal = 0;
let chatFilesPage = 1;
let chatFilesPageSize = 20;
let chatFilesSearchDebounceTimer = null;
const CHAT_FILES_GROUP_STORAGE_KEY = 'csai_chat_files_group_by';
const CHAT_FILES_BROWSE_PATH_KEY = 'csai_chat_files_browse_path';
const CHAT_FILES_PAGE_SIZE_STORAGE_KEY = 'csai_chat_files_page_size';
const CHAT_FILES_TREE_UPLOAD_ROOT = 'uploads';
const CHAT_FILES_TREE_REDUCTION_ROOT = 'tool_outputs';
const CHAT_FILES_TREE_ARTIFACT_ROOT = 'conversation_artifacts';
/** 按文件夹浏览模式下的当前路径(相对 chat_uploads 的段数组),如 ['2024-03-21','uuid'] */
/** 按文件夹浏览模式下的当前路径(虚拟根段数组),如 ['uploads','2024-03-21','uuid'] */
let chatFilesBrowsePath = [];
/** 非空时,下一次上传文件落到此相对路径(chat_uploads 下目录),如 2026-03-21/uuid/sub */
let chatFilesPendingUploadDir = '';
/** 文件管理页面向服务器上传进行中,避免重复选择并禁用顶栏按钮 */
let chatFilesXHRUploadBusy = false;
const CHAT_FILES_FILTER_SELECT_IDS = ['chat-files-group-by'];
const CHAT_FILES_FILTER_SELECT_IDS = ['chat-files-filter-source', 'chat-files-group-by'];
const chatFilesFilterSelectMap = {};
let chatFilesFilterSelectDocBound = false;
const CHAT_FILES_FILTER_SELECT_CARET = '<svg class="chat-files-filter-select-caret" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
@@ -194,6 +202,17 @@ function chatFilesSetBrowsePath(path) {
}
}
function chatFilesLoadPageSizeFromStorage() {
try {
const n = parseInt(localStorage.getItem(CHAT_FILES_PAGE_SIZE_STORAGE_KEY) || '', 10);
if ([10, 20, 50, 100].includes(n)) {
chatFilesPageSize = n;
}
} catch (e) {
/* ignore */
}
}
function chatFilesResolveTreeNode(root, path) {
let node = root;
let i;
@@ -217,6 +236,7 @@ function chatFilesNormalizeBrowsePathForTree(root) {
function initChatFilesPage() {
chatFilesLoadBrowsePathFromStorage();
chatFilesLoadPageSizeFromStorage();
try {
localStorage.removeItem('csai_chat_files_synthetic_dirs');
} catch (e) {
@@ -227,7 +247,7 @@ function initChatFilesPage() {
if (sel) {
try {
const v = localStorage.getItem(CHAT_FILES_GROUP_STORAGE_KEY);
if (v === 'none' || v === 'date' || v === 'conversation' || v === 'folder') {
if (v === 'none' || v === 'date' || v === 'conversation' || v === 'project' || v === 'folder') {
sel.value = v;
}
} catch (e) {
@@ -324,6 +344,8 @@ function ensureChatFilesDocClickClose() {
async function loadChatFilesPage() {
const wrap = document.getElementById('chat-files-list-wrap');
if (!wrap) return;
const pager = document.getElementById('chat-files-pagination');
if (pager) pager.hidden = true;
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>';
@@ -333,10 +355,31 @@ async function loadChatFilesPage() {
const conv = document.getElementById('chat-files-filter-conv');
const convQ = conv ? conv.value.trim() : '';
let url = '/api/chat-uploads';
const project = document.getElementById('chat-files-filter-project');
const projectQ = project ? project.value.trim() : '';
const source = document.getElementById('chat-files-filter-source');
const sourceQ = source ? source.value : 'all';
const search = document.getElementById('chat-files-filter-name');
const searchQ = search ? search.value.trim() : '';
const groupMode = chatFilesGetGroupByMode();
const params = new URLSearchParams();
if (convQ) {
url += '?conversation=' + encodeURIComponent(convQ);
params.set('conversation', convQ);
}
if (projectQ) {
params.set('project', projectQ);
}
if (sourceQ && sourceQ !== 'all') {
params.set('source', sourceQ);
}
if (searchQ) {
params.set('search', searchQ);
}
params.set('page', String(chatFilesPage));
params.set('pageSize', groupMode === 'folder' ? 'all' : String(chatFilesPageSize));
let url = '/api/chat-uploads';
const query = params.toString();
if (query) url += '?' + query;
try {
const res = await apiFetch(url);
@@ -347,6 +390,16 @@ async function loadChatFilesPage() {
const data = await res.json();
chatFilesCache = Array.isArray(data.files) ? data.files : [];
chatFilesFoldersCache = Array.isArray(data.folders) ? data.folders : [];
chatFilesTotal = Number.isFinite(Number(data.total)) ? Number(data.total) : chatFilesCache.length;
chatFilesPage = Number.isFinite(Number(data.page)) ? Math.max(1, Number(data.page)) : chatFilesPage;
if (Number.isFinite(Number(data.pageSize)) && Number(data.pageSize) > 0) {
chatFilesPageSize = Number(data.pageSize);
}
if (groupMode !== 'folder' && chatFilesTotal > 0 && chatFilesCache.length === 0 && chatFilesPage > chatFilesTotalPages()) {
chatFilesPage = chatFilesTotalPages();
loadChatFilesPage();
return;
}
renderChatFilesTable();
} catch (e) {
console.error(e);
@@ -354,24 +407,141 @@ async function loadChatFilesPage() {
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>';
renderChatFilesPagination();
}
}
async function exportChatFiles() {
const conv = document.getElementById('chat-files-filter-conv');
const project = document.getElementById('chat-files-filter-project');
const convQ = conv ? conv.value.trim() : '';
const projectQ = project ? project.value.trim() : '';
const source = document.getElementById('chat-files-filter-source');
const sourceQ = source ? source.value : 'all';
const search = document.getElementById('chat-files-filter-name');
const searchQ = search ? search.value.trim() : '';
const params = new URLSearchParams();
if (convQ) params.set('conversation', convQ);
if (projectQ) params.set('project', projectQ);
if (sourceQ && sourceQ !== 'all') params.set('source', sourceQ);
if (searchQ) params.set('search', searchQ);
const url = '/api/chat-uploads/export' + (params.toString() ? ('?' + params.toString()) : '');
try {
const res = await apiFetch(url);
if (!res.ok) {
const raw = await res.text();
let msg = raw;
try {
const j = JSON.parse(raw);
if (j && j.error) msg = j.error;
} catch (e) {
/* keep raw */
}
if (res.status === 404) {
msg = (typeof window.t === 'function') ? window.t('chatFilesPage.exportEmpty') : '没有可导出的文件';
}
throw new Error(msg || String(res.status));
}
const blob = await res.blob();
const disposition = res.headers.get('Content-Disposition') || '';
let filename = 'chat-files-export.zip';
const m = disposition.match(/filename="?([^"]+)"?/i);
if (m && m[1]) filename = m[1];
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
const ok = (typeof window.t === 'function') ? window.t('chatFilesPage.exportStarted') : '已开始导出';
chatFilesShowToast(ok);
} catch (e) {
alert((e && e.message) ? e.message : String(e));
}
}
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);
});
return Array.isArray(files) ? files : [];
}
function chatFilesResetToFirstPageAndLoad() {
chatFilesPage = 1;
loadChatFilesPage();
}
/** 仅前端按文件名筛选,不重新请求 */
function chatFilesFilterNameOnInput() {
if (!chatFilesCache.length && !chatFilesFoldersCache.length && chatFilesGetGroupByMode() !== 'folder') return;
renderChatFilesTable();
if (chatFilesSearchDebounceTimer) clearTimeout(chatFilesSearchDebounceTimer);
chatFilesSearchDebounceTimer = setTimeout(function () {
chatFilesSearchDebounceTimer = null;
chatFilesResetToFirstPageAndLoad();
}, 250);
}
function chatFilesTotalPages() {
if (chatFilesGetGroupByMode() === 'folder') return 1;
return Math.max(1, Math.ceil((chatFilesTotal || 0) / Math.max(1, chatFilesPageSize)));
}
function renderChatFilesPagination() {
const pager = document.getElementById('chat-files-pagination');
if (!pager) return;
const groupMode = chatFilesGetGroupByMode();
if (groupMode === 'folder' || chatFilesTotal <= 0) {
pager.hidden = true;
pager.innerHTML = '';
return;
}
const totalPages = chatFilesTotalPages();
if (chatFilesPage > totalPages) {
chatFilesPage = totalPages;
}
const start = (chatFilesPage - 1) * chatFilesPageSize + 1;
const end = Math.min(chatFilesTotal, chatFilesPage * chatFilesPageSize);
const info = (typeof window.t === 'function')
? window.t('chatFilesPage.paginationInfo', { start: start, end: end, total: chatFilesTotal })
: ('显示 ' + start + '-' + end + ' / ' + chatFilesTotal);
const pageText = (typeof window.t === 'function')
? window.t('chatFilesPage.paginationPage', { page: chatFilesPage, totalPages: totalPages })
: ('第 ' + chatFilesPage + ' / ' + totalPages + ' 页');
const pageSizeLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.pageSize') : '每页';
const prevLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.prevPage') : '上一页';
const nextLabel = (typeof window.t === 'function') ? window.t('chatFilesPage.nextPage') : '下一页';
const sizes = [10, 20, 50, 100].map(function (n) {
return '<option value="' + n + '"' + (n === chatFilesPageSize ? ' selected' : '') + '>' + n + '</option>';
}).join('');
pager.innerHTML = `
<div class="pagination-info">
<span>${escapeHtml(info)}</span>
<label class="pagination-page-size">${escapeHtml(pageSizeLabel)}
<select onchange="changeChatFilesPageSize(this.value)">${sizes}</select>
</label>
</div>
<div class="pagination-controls">
<button type="button" class="btn-secondary" ${chatFilesPage <= 1 ? 'disabled' : ''} onclick="changeChatFilesPage(${chatFilesPage - 1})">${escapeHtml(prevLabel)}</button>
<span class="pagination-page">${escapeHtml(pageText)}</span>
<button type="button" class="btn-secondary" ${chatFilesPage >= totalPages ? 'disabled' : ''} onclick="changeChatFilesPage(${chatFilesPage + 1})">${escapeHtml(nextLabel)}</button>
</div>`;
pager.hidden = false;
}
function changeChatFilesPage(page) {
const totalPages = chatFilesTotalPages();
const next = Math.min(totalPages, Math.max(1, parseInt(page, 10) || 1));
if (next === chatFilesPage) return;
chatFilesPage = next;
loadChatFilesPage();
}
function changeChatFilesPageSize(value) {
const n = parseInt(value, 10);
if (![10, 20, 50, 100].includes(n)) return;
chatFilesPageSize = n;
chatFilesPage = 1;
try {
localStorage.setItem(CHAT_FILES_PAGE_SIZE_STORAGE_KEY, String(n));
} catch (e) {
/* ignore */
}
loadChatFilesPage();
}
function formatChatFileBytes(n) {
@@ -477,7 +647,7 @@ function chatFilesAlertMessage(raw) {
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;
if (v === 'date' || v === 'conversation' || v === 'project' || v === 'folder') return v;
return 'none';
}
@@ -490,7 +660,7 @@ function chatFilesGroupByChange() {
/* ignore */
}
}
renderChatFilesTable();
chatFilesResetToFirstPageAndLoad();
}
function chatFilesCompareDateKeysDesc(a, b) {
@@ -507,7 +677,7 @@ function chatFilesTreeMakeNode() {
}
function chatFilesTreeInsertFile(root, f, idx) {
const rp = String(f.relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
const rp = chatFilesTreePathForFile(f);
if (!rp) return;
const parts = rp.split('/').filter(function (p) {
return p.length > 0;
@@ -530,6 +700,21 @@ function chatFilesBuildTree(files) {
return root;
}
function chatFilesTreePathForFile(f) {
const source = f && f.source;
let rp = String((f && f.relativePath) || '').replace(/\\/g, '/').replace(/^\/+/, '');
if (!rp) return '';
if (source === 'reduction') {
rp = rp.replace(/^__reduction__\//, '');
return CHAT_FILES_TREE_REDUCTION_ROOT + '/' + rp;
}
if (source === 'conversation_artifact') {
rp = rp.replace(/^__conversation_artifact__\//, '');
return CHAT_FILES_TREE_ARTIFACT_ROOT + '/' + rp;
}
return CHAT_FILES_TREE_UPLOAD_ROOT + '/' + rp;
}
/** 将后端返回的目录相对路径(如 a/b/c)并入树,便于展示空文件夹 */
function chatFilesTreeInsertFolderPath(root, relSlash) {
const rp = String(relSlash || '').replace(/\\/g, '/').replace(/^\/+/, '');
@@ -549,18 +734,94 @@ function chatFilesTreeInsertFolderPath(root, relSlash) {
function chatFilesMergeFoldersIntoTree(root, folderPaths) {
if (!Array.isArray(folderPaths)) return;
if (!chatFilesShouldMergeUploadFolders()) return;
let i;
for (i = 0; i < folderPaths.length; i++) {
chatFilesTreeInsertFolderPath(root, folderPaths[i]);
chatFilesTreeInsertFolderPath(root, CHAT_FILES_TREE_UPLOAD_ROOT + '/' + folderPaths[i]);
}
}
function chatFilesTreeRootMerged() {
const root = chatFilesBuildTree(chatFilesDisplayed);
chatFilesMergeFoldersIntoTree(root, chatFilesFoldersCache);
chatFilesEnsureSourceRoots(root);
return root;
}
function chatFilesCurrentSourceFilter() {
const el = document.getElementById('chat-files-filter-source');
return el ? (el.value || 'all') : 'all';
}
function chatFilesCurrentSearchQuery() {
const el = document.getElementById('chat-files-filter-name');
return el ? String(el.value || '').trim() : '';
}
function chatFilesShouldMergeUploadFolders() {
const source = chatFilesCurrentSourceFilter();
return (source === 'all' || source === 'upload') && !chatFilesCurrentSearchQuery();
}
function chatFilesEnsureSourceRoots(root) {
const source = chatFilesCurrentSourceFilter();
const roots = [];
if (source === 'all' || source === 'upload') roots.push(CHAT_FILES_TREE_UPLOAD_ROOT);
if (source === 'all' || source === 'reduction') roots.push(CHAT_FILES_TREE_REDUCTION_ROOT);
if (source === 'all' || source === 'conversation_artifact') roots.push(CHAT_FILES_TREE_ARTIFACT_ROOT);
roots.forEach(function (name) {
if (!root.dirs[name]) root.dirs[name] = chatFilesTreeMakeNode();
});
}
function chatFilesIsInternalSource(f) {
const source = f && f.source;
return source === 'reduction' || source === 'conversation_artifact';
}
function chatFilesSourceLabel(source) {
if (source === 'reduction') {
return (typeof window.t === 'function') ? window.t('chatFilesPage.sourceReduction') : '工具输出';
}
if (source === 'conversation_artifact') {
return (typeof window.t === 'function') ? window.t('chatFilesPage.sourceConversationArtifact') : '会话产物';
}
return (typeof window.t === 'function') ? window.t('chatFilesPage.sourceUpload') : '对话附件';
}
function chatFilesBrowseCanMutateCurrentPath() {
return chatFilesBrowsePath[0] === CHAT_FILES_TREE_UPLOAD_ROOT;
}
function chatFilesBrowseCanUploadToPath(path) {
return Array.isArray(path) && path[0] === CHAT_FILES_TREE_UPLOAD_ROOT;
}
function chatFilesBrowseCanDeleteFolderPath(path) {
return Array.isArray(path) && path[0] === CHAT_FILES_TREE_UPLOAD_ROOT && path.length > 1;
}
function chatFilesTreeDisplayName(name) {
if (name === CHAT_FILES_TREE_UPLOAD_ROOT) {
return (typeof window.t === 'function') ? window.t('chatFilesPage.treeUploadsRoot') : '对话附件';
}
if (name === CHAT_FILES_TREE_REDUCTION_ROOT) {
return (typeof window.t === 'function') ? window.t('chatFilesPage.treeReductionRoot') : '工具输出';
}
if (name === CHAT_FILES_TREE_ARTIFACT_ROOT) {
return (typeof window.t === 'function') ? window.t('chatFilesPage.treeArtifactsRoot') : '会话产物';
}
return name;
}
function chatFilesUploadRelativeDirFromBrowsePath(path) {
const parts = Array.isArray(path) ? path.slice() : [];
if (parts[0] === CHAT_FILES_TREE_UPLOAD_ROOT) {
parts.shift();
}
return parts.join('/');
}
function chatFilesTreeNodeMaxMod(node) {
let m = 0;
let i;
@@ -587,7 +848,14 @@ function chatFilesTreeSortDirKeys(node, keys) {
function chatFilesBuildGroups(files, mode) {
const map = new Map();
files.forEach(function (f, idx) {
const key = mode === 'date' ? (f.date || '—') : (f.conversationId || '—');
let key;
if (mode === 'date') {
key = f.date || '—';
} else if (mode === 'project') {
key = f.projectId || '—';
} else {
key = f.conversationId || '—';
}
if (!map.has(key)) {
map.set(key, { key: key, items: [] });
}
@@ -623,12 +891,20 @@ function chatFilesBuildGroups(files, mode) {
return groups;
}
/** 分组标题:会话 ID 过长时缩短展示,完整值放在 title */
function chatFilesGroupHeadingConversation(key) {
/** 分组标题: ID 缩短展示,完整值放在 title */
function chatFilesGroupHeadingID(key, emptyLabel) {
const c = key == null ? '' : String(key);
if (c === '' || c === '—') {
return { text: '—', title: '' };
return { text: emptyLabel || '—', title: '' };
}
if (c.length > 36) {
return { text: c.slice(0, 8) + '…' + c.slice(-6), title: c };
}
return { text: c, title: c };
}
function chatFilesGroupHeadingConversation(key) {
const c = key == null ? '' : String(key);
if (typeof window.t === 'function') {
if (c === '_manual') {
return { text: window.t('chatFilesPage.convManual'), title: '_manual' };
@@ -637,10 +913,12 @@ function chatFilesGroupHeadingConversation(key) {
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 };
return chatFilesGroupHeadingID(key);
}
function chatFilesGroupHeadingProject(key) {
const empty = (typeof window.t === 'function') ? window.t('chatFilesPage.projectUnbound') : '未绑定项目';
return chatFilesGroupHeadingID(key, empty);
}
function renderChatFilesTable() {
@@ -658,6 +936,7 @@ function renderChatFilesTable() {
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(wrap);
}
renderChatFilesPagination();
return;
}
@@ -683,6 +962,8 @@ function renderChatFilesTable() {
const rp = f.relativePath || '';
const pathForTitle = (f.absolutePath && String(f.absolutePath).trim()) ? String(f.absolutePath).trim() : rp;
const nameEsc = escapeHtml(f.name || '');
const isInternal = chatFilesIsInternalSource(f);
const sourceBadge = isInternal ? '<span class="chat-files-source-badge">' + escapeHtml(chatFilesSourceLabel(f.source)) + '</span>' : '';
const conv = f.conversationId || '';
const convEsc = escapeHtml(conv);
const dt = f.modifiedUnix ? new Date(f.modifiedUnix * 1000).toLocaleString() : '—';
@@ -700,13 +981,17 @@ function renderChatFilesTable() {
if (canOpenChat) {
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesConversationIdx(${idx});">${tOpenChat}</button>`);
}
if (!bin) {
if (isInternal) {
menuParts.push(`<div class="chat-files-dropdown-item is-disabled">${escapeHtml(chatFilesSourceLabel(f.source))}</div>`);
} else 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>`);
if (!isInternal) {
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() : '';
@@ -728,7 +1013,7 @@ function renderChatFilesTable() {
<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 class="chat-files-cell-name" title="${escapeHtml(pathForTitle)}">${nameEsc}${sourceBadge}</td>
<td>${formatChatFileBytes(f.size || 0)}</td>
<td>${escapeHtml(dt)}</td>
<td class="chat-files-actions">
@@ -774,11 +1059,11 @@ function renderChatFilesTable() {
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 tRoot = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.browseRoot') : '文件');
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 tCopyFolder = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.copyFolderPathTitle') : '复制目录路径');
const tEnter = escapeHtml((typeof window.t === 'function') ? window.t('chatFilesPage.enterFolderTitle') : '进入');
let breadcrumbHtml = '<nav class="chat-files-breadcrumb" aria-label="breadcrumb">';
@@ -789,16 +1074,20 @@ function renderChatFilesTable() {
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>';
breadcrumbHtml += '<span class="chat-files-breadcrumb-current">' + escapeHtml(chatFilesTreeDisplayName(seg)) + '</span>';
} else {
breadcrumbHtml += '<button type="button" class="chat-files-breadcrumb-link" onclick="chatFilesNavigateBreadcrumb(' + bi + ')">' + escapeHtml(seg) + '</button>';
breadcrumbHtml += '<button type="button" class="chat-files-breadcrumb-link" onclick="chatFilesNavigateBreadcrumb(' + bi + ')">' + escapeHtml(chatFilesTreeDisplayName(seg)) + '</button>';
}
}
breadcrumbHtml += '</nav>';
const canMutateCurrentPath = chatFilesBrowseCanMutateCurrentPath();
const upDisabled = chatFilesBrowsePath.length === 0 ? ' disabled' : '';
const mkdirButton = canMutateCurrentPath
? '<button type="button" class="btn-secondary chat-files-mkdir-btn" onclick="openChatFilesMkdirModal()">' + tMkdir + '</button>'
: '';
const toolbarHtml = '<div class="chat-files-browse-toolbar">' + breadcrumbHtml +
'<button type="button" class="btn-secondary chat-files-mkdir-btn" onclick="openChatFilesMkdirModal()">' + tMkdir + '</button>' +
mkdirButton +
'<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>';
@@ -808,19 +1097,28 @@ function renderChatFilesTable() {
function rowHtmlBrowseFolder(name) {
const nameAttr = encodeURIComponent(String(name));
const relToFolder = chatFilesBrowsePath.concat([name]).join('/');
const folderPath = chatFilesBrowsePath.concat([name]);
const relToFolder = folderPath.join('/');
const uploadDirAttr = encodeURIComponent(relToFolder);
const canUploadFolder = chatFilesBrowseCanUploadToPath(folderPath);
const canDeleteFolder = chatFilesBrowseCanDeleteFolderPath(folderPath);
const uploadBtn = canUploadFolder
? `<button type="button" class="btn-icon" title="${tUploadToFolder}" data-upload-dir="${uploadDirAttr}" onclick="chatFilesUploadToFolderClick(event, this)">${svgUploadToFolder}</button>`
: '';
const deleteBtn = canDeleteFolder
? `<button type="button" class="btn-icon btn-danger" title="${tDeleteFolder}" data-chat-folder-name="${nameAttr}" onclick="chatFilesDeleteFolderFromBtn(event, this)">${svgTrash}</button>`
: '';
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>
<span class="chat-files-tree-name-inner">${svgFolder}<span class="chat-files-tree-name-text">${escapeHtml(chatFilesTreeDisplayName(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>
${uploadBtn}
<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>
${deleteBtn}
</div>
</td>
</tr>`;
@@ -829,6 +1127,8 @@ function renderChatFilesTable() {
function rowHtmlTreeFile(f, idx) {
const pathForTitle = (f.absolutePath && String(f.absolutePath).trim()) ? String(f.absolutePath).trim() : (f.relativePath || '');
const nameEsc = escapeHtml(f.name || '');
const isInternal = chatFilesIsInternalSource(f);
const sourceBadge = isInternal ? '<span class="chat-files-source-badge">' + escapeHtml(chatFilesSourceLabel(f.source)) + '</span>' : '';
const dt = f.modifiedUnix ? new Date(f.modifiedUnix * 1000).toLocaleString() : '—';
const conv = f.conversationId || '';
const canOpenChat = conv && conv !== '_manual' && conv !== '_new';
@@ -845,18 +1145,22 @@ function renderChatFilesTable() {
if (canOpenChat) {
menuParts.push(`<button type="button" class="chat-files-dropdown-item" onclick="chatFilesCloseAllMenus(); openChatFilesConversationIdx(${idx});">${tOpenChat}</button>`);
}
if (!bin) {
if (isInternal) {
menuParts.push(`<div class="chat-files-dropdown-item is-disabled">${escapeHtml(chatFilesSourceLabel(f.source))}</div>`);
} else 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>`);
if (!isInternal) {
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>
<span class="chat-files-tree-name-inner">${svgFile}<span class="chat-files-tree-name-text">${nameEsc}${sourceBadge}</span></span>
</td>
<td>${formatChatFileBytes(f.size || 0)}</td>
<td>${escapeHtml(dt)}</td>
@@ -899,6 +1203,10 @@ function renderChatFilesTable() {
let summaryTitleAttr = '';
if (groupMode === 'date') {
summaryMain = escapeHtml(String(g.key));
} else if (groupMode === 'project') {
const h = chatFilesGroupHeadingProject(g.key);
summaryMain = escapeHtml(h.text);
summaryTitleAttr = h.title ? ' title="' + escapeHtml(h.title) + '"' : '';
} else {
const h = chatFilesGroupHeadingConversation(g.key);
summaryMain = escapeHtml(h.text);
@@ -926,9 +1234,13 @@ function renderChatFilesTable() {
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');
renderChatFilesPagination();
}
window.chatFilesGroupByChange = chatFilesGroupByChange;
window.changeChatFilesPage = changeChatFilesPage;
window.changeChatFilesPageSize = changeChatFilesPageSize;
window.chatFilesResetToFirstPageAndLoad = chatFilesResetToFirstPageAndLoad;
function chatFilesNavigateInto(name) {
const root = chatFilesTreeRootMerged();
@@ -989,7 +1301,8 @@ function chatFilesCopyFolderPathFromBtn(ev, btn) {
async function deleteChatFolderFromBrowse(folderName) {
const segs = chatFilesBrowsePath.concat([folderName]);
const rel = segs.join('/');
if (!chatFilesBrowseCanDeleteFolderPath(segs)) return;
const rel = chatFilesUploadRelativeDirFromBrowsePath(segs);
const q = (typeof window.t === 'function') ? window.t('chatFilesPage.confirmDeleteFolder') : '确定删除该文件夹及其中的全部文件?';
if (!confirm(q)) return;
try {
@@ -1038,8 +1351,7 @@ function chatFilesDeleteFolderFromBtn(ev, btn) {
async function copyChatFolderPathFromBrowse(folderName) {
const segs = chatFilesBrowsePath.concat([folderName]);
const rel = segs.join('/');
const text = rel ? ('chat_uploads/' + rel.replace(/^\/+/, '')) : 'chat_uploads';
const text = chatFilesClipboardFolderPath(segs);
const ok = await chatFilesCopyText(text);
if (ok) {
const msg = (typeof window.t === 'function') ? window.t('chatFilesPage.folderPathCopied') : '目录路径已复制';
@@ -1050,6 +1362,22 @@ async function copyChatFolderPathFromBrowse(folderName) {
}
}
function chatFilesClipboardFolderPath(segs) {
const parts = Array.isArray(segs) ? segs.slice() : [];
if (!parts.length) return '';
const root = parts.shift();
if (root === CHAT_FILES_TREE_UPLOAD_ROOT) {
return ['chat_uploads'].concat(parts).join('/');
}
if (root === CHAT_FILES_TREE_REDUCTION_ROOT) {
return ['tool_outputs'].concat(parts).join('/');
}
if (root === CHAT_FILES_TREE_ARTIFACT_ROOT) {
return ['conversation_artifacts'].concat(parts).join('/');
}
return [root].concat(parts).map(chatFilesTreeDisplayName).join('/');
}
window.chatFilesNavigateInto = chatFilesNavigateInto;
window.chatFilesNavigateBreadcrumb = chatFilesNavigateBreadcrumb;
window.chatFilesNavigateUp = chatFilesNavigateUp;
@@ -1060,6 +1388,7 @@ window.chatFilesCopyFolderPathFromBtn = chatFilesCopyFolderPathFromBtn;
window.chatFilesDeleteFolderFromBtn = chatFilesDeleteFolderFromBtn;
window.chatFilesOpenUploadPicker = chatFilesOpenUploadPicker;
window.chatFilesUploadToFolderClick = chatFilesUploadToFolderClick;
window.exportChatFiles = exportChatFiles;
window.openChatFilesMkdirModal = openChatFilesMkdirModal;
window.closeChatFilesMkdirModal = closeChatFilesMkdirModal;
window.submitChatFilesMkdir = submitChatFilesMkdir;
@@ -1254,11 +1583,12 @@ async function submitChatFilesRename() {
function openChatFilesMkdirModal() {
if (chatFilesGetGroupByMode() !== 'folder') return;
if (!chatFilesBrowseCanMutateCurrentPath()) 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';
const p = chatFilesBrowsePath.map(chatFilesTreeDisplayName).join('/');
if (hint) hint.textContent = p || chatFilesTreeDisplayName(CHAT_FILES_TREE_UPLOAD_ROOT);
if (input) input.value = '';
if (modal) openAppModal(modal);
if (modal && typeof window.applyTranslations === 'function') {
@@ -1289,7 +1619,7 @@ async function submitChatFilesMkdir() {
alert(msg);
return;
}
const parent = chatFilesBrowsePath.join('/');
const parent = chatFilesUploadRelativeDirFromBrowsePath(chatFilesBrowsePath);
try {
const res = await apiFetch('/api/chat-uploads/mkdir', {
method: 'POST',
@@ -1355,7 +1685,8 @@ function chatFilesSetUploadBusy(busy) {
function chatFilesOpenUploadPicker() {
if (chatFilesXHRUploadBusy) return;
if (chatFilesGetGroupByMode() === 'folder') {
chatFilesPendingUploadDir = chatFilesBrowsePath.join('/');
if (!chatFilesBrowseCanMutateCurrentPath()) return;
chatFilesPendingUploadDir = chatFilesUploadRelativeDirFromBrowsePath(chatFilesBrowsePath);
} else {
chatFilesPendingUploadDir = '';
}
@@ -1370,6 +1701,7 @@ function chatFilesUploadToFolderClick(ev, btn) {
if (!raw) return;
try {
chatFilesPendingUploadDir = decodeURIComponent(raw);
chatFilesPendingUploadDir = chatFilesUploadRelativeDirFromBrowsePath(chatFilesPendingUploadDir.split('/').filter(Boolean));
} catch (e) {
chatFilesPendingUploadDir = '';
return;
@@ -1385,8 +1717,9 @@ function chatFilesResolveUploadTarget() {
return { relativeDir: pendingDir };
}
if (chatFilesGetGroupByMode() === 'folder') {
const dir = chatFilesBrowsePath.join('/');
return dir ? { relativeDir: dir } : {};
if (!chatFilesBrowseCanMutateCurrentPath()) return {};
const relDir = chatFilesUploadRelativeDirFromBrowsePath(chatFilesBrowsePath);
return relDir ? { relativeDir: relDir } : {};
}
const conv = document.getElementById('chat-files-filter-conv');
if (conv && conv.value.trim()) {