From 3f2984b7c651acfae422ac4485a71fd968bc27e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:18:33 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 30 +++ web/static/i18n/en-US.json | 25 +- web/static/i18n/zh-CN.json | 25 +- web/static/js/chat-files.js | 441 +++++++++++++++++++++++++++++++----- web/templates/index.html | 22 +- 5 files changed, 482 insertions(+), 61 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 67df23b0..70e46d42 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -29956,6 +29956,20 @@ html[data-theme="dark"] #info-collect-syntax-panel.is-generated-target { margin-bottom: 16px; } +.chat-files-source-badge { + display: inline-flex; + align-items: center; + margin-left: 8px; + padding: 2px 6px; + border: 1px solid rgba(99, 102, 241, 0.25); + border-radius: 6px; + background: rgba(99, 102, 241, 0.08); + color: #4f46e5; + font-size: 0.72rem; + line-height: 1; + vertical-align: middle; +} + .chat-files-filter-native-select { position: absolute; width: 1px; @@ -30104,6 +30118,16 @@ html[data-theme="dark"] #info-collect-syntax-panel.is-generated-target { border-radius: 8px; } +.chat-files-pagination { + margin-top: 12px; + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.chat-files-pagination[hidden] { + display: none; +} + .chat-files-table-wrap.drag-over { background: rgba(0, 102, 255, 0.06); outline: 2px dashed rgba(0, 102, 255, 0.35); @@ -34269,6 +34293,12 @@ html[data-theme="dark"] .chat-files-cell-conv code { border: 1px solid var(--border-color); } +html[data-theme="dark"] .chat-files-source-badge { + border-color: rgba(129, 140, 248, 0.35); + background: rgba(129, 140, 248, 0.12); + color: #c7d2fe; +} + html[data-theme="dark"] code, html[data-theme="dark"] pre, html[data-theme="dark"] .code-block, diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 3d019400..15b1d3f7 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2015,6 +2015,7 @@ "webshellExec": "Execute WebShell command", "webshellFileOp": "WebShell file operation", "listChatUploads": "List uploads", + "exportChatUploads": "Export uploads", "uploadChatFile": "Upload file", "deleteChatUpload": "Delete upload", "downloadChatUpload": "Download upload", @@ -2348,20 +2349,40 @@ "chatFilesPage": { "title": "File Management", "intro": "Files uploaded in chat appear here. Drag files into the list below, or click Upload to pick files (multiple allowed). Click “Copy path” to copy the server absolute path and paste it into a conversation so the model can reference the file.", + "export": "Export Current Results", + "exportStarted": "Started exporting current files", + "exportEmpty": "No files to export", "upload": "Upload", "conversationFilter": "Conversation ID", "conversationPlaceholder": "Leave empty for all", + "projectFilter": "Project ID", + "projectPlaceholder": "Leave empty for all", "searchName": "File name", "searchNamePlaceholder": "Filter by file name", + "sourceFilter": "Source", + "sourceAll": "All", + "sourceUpload": "Chat uploads", + "sourceReduction": "Tool outputs", + "sourceConversationArtifact": "Conversation artifacts", "groupBy": "Group by", "groupNone": "None (flat list)", "groupByDate": "By date", "groupByConversation": "By conversation", + "groupByProject": "By project", "groupByFolder": "By folder (path navigation)", - "browseRoot": "chat_uploads", + "projectUnbound": "Unbound project", + "paginationInfo": "Showing {{start}}-{{end}} of {{total}} files", + "paginationPage": "Page {{page}} of {{totalPages}}", + "pageSize": "Per page", + "prevPage": "Previous", + "nextPage": "Next", + "browseRoot": "Files", + "treeUploadsRoot": "Chat uploads", + "treeReductionRoot": "Tool outputs", + "treeArtifactsRoot": "Conversation artifacts", "browseUp": "Up", "enterFolderTitle": "Open folder", - "copyFolderPathTitle": "Copy relative path under chat_uploads/…", + "copyFolderPathTitle": "Copy folder path", "folderPathCopied": "Folder path copied — paste into chat if needed", "folderEmpty": "This folder is empty", "confirmDeleteFolder": "Delete this folder and everything inside it? This cannot be undone.", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index ea83e556..8ea7c1f1 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2003,6 +2003,7 @@ "webshellExec": "执行WebShell命令", "webshellFileOp": "WebShell文件操作", "listChatUploads": "列出附件", + "exportChatUploads": "导出附件", "uploadChatFile": "上传附件", "deleteChatUpload": "删除附件", "downloadChatUpload": "下载附件", @@ -2336,20 +2337,40 @@ "chatFilesPage": { "title": "文件管理", "intro": "管理在对话中上传的文件。可将文件拖拽到下方列表区域,或点击「上传文件」选择文件(支持多选)。需要让 AI 引用某文件时,在列表中点击「复制路径」,到对话里粘贴即可(路径为服务器上的绝对路径,与对话附件保存位置一致)。", + "export": "导出当前结果", + "exportStarted": "已开始导出当前文件", + "exportEmpty": "没有可导出的文件", "upload": "上传文件", "conversationFilter": "会话 ID", "conversationPlaceholder": "留空表示全部", + "projectFilter": "项目 ID", + "projectPlaceholder": "留空表示全部", "searchName": "文件名", "searchNamePlaceholder": "筛选文件名", + "sourceFilter": "来源", + "sourceAll": "全部", + "sourceUpload": "对话附件", + "sourceReduction": "工具输出", + "sourceConversationArtifact": "会话产物", "groupBy": "分组方式", "groupNone": "不分组(平铺)", "groupByDate": "按日期", "groupByConversation": "按会话", + "groupByProject": "按项目", "groupByFolder": "按文件夹(路径浏览)", - "browseRoot": "chat_uploads", + "projectUnbound": "未绑定项目", + "paginationInfo": "显示 {{start}}-{{end}} / {{total}} 个文件", + "paginationPage": "第 {{page}} / {{totalPages}} 页", + "pageSize": "每页", + "prevPage": "上一页", + "nextPage": "下一页", + "browseRoot": "文件", + "treeUploadsRoot": "对话附件", + "treeReductionRoot": "工具输出", + "treeArtifactsRoot": "会话产物", "browseUp": "上级", "enterFolderTitle": "进入此文件夹", - "copyFolderPathTitle": "复制该目录的相对路径(chat_uploads/…)", + "copyFolderPathTitle": "复制目录路径", "folderPathCopied": "目录路径已复制,可粘贴到对话中", "folderEmpty": "此文件夹为空", "confirmDeleteFolder": "确定删除该文件夹及其中的全部文件?此操作不可恢复。", diff --git a/web/static/js/chat-files.js b/web/static/js/chat-files.js index edb0eff6..03084dab 100644 --- a/web/static/js/chat-files.js +++ b/web/static/js/chat-files.js @@ -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 = ''; @@ -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 = '
加载中…
'; @@ -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 = '
' + escapeHtml(msg + ': ' + (e.message || String(e))) + '
'; + 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 ''; + }).join(''); + pager.innerHTML = ` +
+ ${escapeHtml(info)} + +
+
+ + ${escapeHtml(pageText)} + +
`; + 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 ? '' + escapeHtml(chatFilesSourceLabel(f.source)) + '' : ''; 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(``); } - if (!bin) { + if (isInternal) { + menuParts.push(`
${escapeHtml(chatFilesSourceLabel(f.source))}
`); + } else if (!bin) { menuParts.push(``); } else { menuParts.push(`
${editUnavailable}
`); } - menuParts.push(``); - menuParts.push(``); + if (!isInternal) { + menuParts.push(``); + menuParts.push(``); + } const menuHtml = menuParts.join(''); const subRaw = (f.subPath && String(f.subPath).trim()) ? String(f.subPath).trim() : ''; @@ -728,7 +1013,7 @@ function renderChatFilesTable() { ${escapeHtml(f.date || '—')} ${convEsc} ${subCellInner} - ${nameEsc} + ${nameEsc}${sourceBadge} ${formatChatFileBytes(f.size || 0)} ${escapeHtml(dt)} @@ -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 = ''; + const canMutateCurrentPath = chatFilesBrowseCanMutateCurrentPath(); const upDisabled = chatFilesBrowsePath.length === 0 ? ' disabled' : ''; + const mkdirButton = canMutateCurrentPath + ? '' + : ''; const toolbarHtml = '
' + breadcrumbHtml + - '' + + mkdirButton + '
'; const svgTrash = ''; @@ -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 + ? `` + : ''; + const deleteBtn = canDeleteFolder + ? `` + : ''; return ` - ${svgFolder}${escapeHtml(name)} + ${svgFolder}${escapeHtml(chatFilesTreeDisplayName(name))} — —
- + ${uploadBtn} - + ${deleteBtn}
`; @@ -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 ? '' + escapeHtml(chatFilesSourceLabel(f.source)) + '' : ''; 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(``); } - if (!bin) { + if (isInternal) { + menuParts.push(`
${escapeHtml(chatFilesSourceLabel(f.source))}
`); + } else if (!bin) { menuParts.push(``); } else { menuParts.push(`
${editUnavailable}
`); } - menuParts.push(``); - menuParts.push(``); + if (!isInternal) { + menuParts.push(``); + menuParts.push(``); + } const menuHtml = menuParts.join(''); return ` - ${svgFile}${nameEsc} + ${svgFile}${nameEsc}${sourceBadge} ${formatChatFileBytes(f.size || 0)} ${escapeHtml(dt)} @@ -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()) { diff --git a/web/templates/index.html b/web/templates/index.html index 5a9e29a2..ab1d1d44 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -2714,6 +2714,7 @@