mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-19 06:18:11 +02:00
Add files via upload
This commit is contained in:
@@ -74,6 +74,8 @@ type ChatUploadFileItem struct {
|
||||
ModifiedUnix int64 `json:"modifiedUnix"`
|
||||
Date string `json:"date"`
|
||||
ConversationID string `json:"conversationId"`
|
||||
// SubPath 为日期、会话目录之下的子路径(不含文件名),如 date/conv/a/b/file 则为 "a/b";无嵌套则为 ""。
|
||||
SubPath string `json:"subPath"`
|
||||
}
|
||||
|
||||
// List GET /api/chat-uploads
|
||||
@@ -113,6 +115,10 @@ func (h *ChatUploadsHandler) List(c *gin.Context) {
|
||||
if len(parts) >= 3 {
|
||||
convID = parts[1]
|
||||
}
|
||||
var subPath string
|
||||
if len(parts) >= 4 {
|
||||
subPath = strings.Join(parts[2:len(parts)-1], "/")
|
||||
}
|
||||
if conversationFilter != "" && convID != conversationFilter {
|
||||
return nil
|
||||
}
|
||||
@@ -125,6 +131,7 @@ func (h *ChatUploadsHandler) List(c *gin.Context) {
|
||||
ModifiedUnix: info.ModTime().Unix(),
|
||||
Date: dateStr,
|
||||
ConversationID: convID,
|
||||
SubPath: subPath,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
@@ -171,7 +178,8 @@ func (h *ChatUploadsHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := os.Remove(abs); err != nil {
|
||||
st, err := os.Stat(abs)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||
return
|
||||
@@ -179,6 +187,21 @@ func (h *ChatUploadsHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if st.IsDir() {
|
||||
if err := os.RemoveAll(abs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(abs); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
@@ -295,30 +318,57 @@ func chatUploadShortRand(n int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Upload POST /api/chat-uploads (multipart: file, conversationId 可选)
|
||||
// Upload POST /api/chat-uploads multipart: file;conversationId 可选;relativeDir 可选(chat_uploads 下目录的相对路径,将文件直接上传至该目录)
|
||||
func (h *ChatUploadsHandler) Upload(c *gin.Context) {
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil || fh == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
convID := strings.TrimSpace(c.PostForm("conversationId"))
|
||||
convDir := convID
|
||||
if convDir == "" {
|
||||
convDir = "_manual"
|
||||
} else {
|
||||
convDir = strings.ReplaceAll(convDir, string(filepath.Separator), "_")
|
||||
}
|
||||
root, err := h.absRoot()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
dateStr := time.Now().Format("2006-01-02")
|
||||
targetDir := filepath.Join(root, dateStr, convDir)
|
||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
var targetDir string
|
||||
targetRel := strings.TrimSpace(c.PostForm("relativeDir"))
|
||||
if targetRel != "" {
|
||||
absDir, err := h.resolveUnderChatUploads(targetRel)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
st, err := os.Stat(absDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(absDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else if !st.IsDir() {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "relativeDir is not a directory"})
|
||||
return
|
||||
}
|
||||
targetDir = absDir
|
||||
} else {
|
||||
convID := strings.TrimSpace(c.PostForm("conversationId"))
|
||||
convDir := convID
|
||||
if convDir == "" {
|
||||
convDir = "_manual"
|
||||
} else {
|
||||
convDir = strings.ReplaceAll(convDir, string(filepath.Separator), "_")
|
||||
}
|
||||
dateStr := time.Now().Format("2006-01-02")
|
||||
targetDir = filepath.Join(root, dateStr, convDir)
|
||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
baseName := filepath.Base(fh.Filename)
|
||||
if baseName == "" || baseName == "." {
|
||||
|
||||
@@ -12900,6 +12900,267 @@ header {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 分组视图:外层不再套一层大边框,由各分组卡片承担 */
|
||||
.chat-files-table-wrap.chat-files-table-wrap--grouped {
|
||||
border: none;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* GitHub 式:单表 + 首列缩进,无嵌套子表、无重复表头 */
|
||||
.chat-files-table-wrap.chat-files-table-wrap--tree {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.chat-files-browse-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.chat-files-browse-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 16px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary, #f8f9fa);
|
||||
}
|
||||
|
||||
.chat-files-breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 2px;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-files-breadcrumb-link {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 2px 4px;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: var(--accent-color);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.chat-files-breadcrumb-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.chat-files-breadcrumb-sep {
|
||||
color: var(--text-secondary);
|
||||
user-select: none;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.chat-files-breadcrumb-current {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.chat-files-browse-up {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-files-browse-up:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chat-files-tr-folder--nav {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-files-tr-folder--nav:hover {
|
||||
background: rgba(0, 102, 255, 0.06);
|
||||
}
|
||||
|
||||
.chat-files-folder-empty {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
padding: 24px 12px !important;
|
||||
}
|
||||
|
||||
.chat-files-table--tree-flat {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.chat-files-table--tree-flat thead th {
|
||||
background: var(--bg-secondary, #f8f9fa);
|
||||
}
|
||||
|
||||
.chat-files-table--tree-flat .chat-files-tr-folder {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.chat-files-table--tree-flat .chat-files-tr-folder .chat-files-tree-name-cell--folder {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-files-table--tree-flat .chat-files-tr-file:hover {
|
||||
background: rgba(128, 128, 128, 0.04);
|
||||
}
|
||||
|
||||
.chat-files-tree-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--accent-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-files-tree-icon path {
|
||||
fill: var(--bg-primary);
|
||||
stroke: var(--accent-color);
|
||||
}
|
||||
|
||||
.chat-files-tree-file-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.chat-files-tree-name-cell {
|
||||
max-width: min(100%, 560px);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chat-files-tree-name-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-files-tree-name-text {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-all;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.chat-files-tree-muted {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.chat-files-path-breadcrumb {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 2px;
|
||||
line-height: 1.45;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.chat-files-path-sep {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.chat-files-path-crumb {
|
||||
color: var(--text-primary);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.chat-files-path-root {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.chat-files-grouped {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.chat-files-group {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-files-group > summary.chat-files-group-summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-secondary, #f8f9fa);
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chat-files-group > summary.chat-files-group-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-files-group > summary.chat-files-group-summary::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid var(--text-secondary);
|
||||
margin-right: 2px;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-files-group[open] > summary.chat-files-group-summary::before {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.chat-files-group-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.chat-files-group-count {
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.chat-files-group-body {
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chat-files-group-body .chat-files-table {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-files-group-body .chat-files-table th {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.chat-files-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -12941,6 +13202,19 @@ header {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.chat-files-cell-subpath {
|
||||
max-width: 280px;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chat-files-group-title--folder {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.chat-files-actions {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
@@ -1007,6 +1007,25 @@
|
||||
"conversationPlaceholder": "Leave empty for all",
|
||||
"searchName": "File name",
|
||||
"searchNamePlaceholder": "Filter by file name",
|
||||
"groupBy": "Group by",
|
||||
"groupNone": "None (flat list)",
|
||||
"groupByDate": "By date",
|
||||
"groupByConversation": "By conversation",
|
||||
"groupByFolder": "By folder (path navigation)",
|
||||
"browseRoot": "chat_uploads",
|
||||
"browseUp": "Up",
|
||||
"enterFolderTitle": "Open folder",
|
||||
"copyFolderPathTitle": "Copy relative path under chat_uploads/…",
|
||||
"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.",
|
||||
"deleteFolderTitle": "Delete folder",
|
||||
"uploadToFolderTitle": "Upload file into this folder",
|
||||
"colSubPath": "Subfolder",
|
||||
"folderRoot": "(root)",
|
||||
"groupCount": "{{count}} files",
|
||||
"convManual": "Manual upload",
|
||||
"convNew": "New chat",
|
||||
"colDate": "Date",
|
||||
"colConversation": "Conversation",
|
||||
"colName": "Name",
|
||||
|
||||
@@ -1007,6 +1007,25 @@
|
||||
"conversationPlaceholder": "留空表示全部",
|
||||
"searchName": "文件名",
|
||||
"searchNamePlaceholder": "筛选文件名",
|
||||
"groupBy": "分组方式",
|
||||
"groupNone": "不分组(平铺)",
|
||||
"groupByDate": "按日期",
|
||||
"groupByConversation": "按会话",
|
||||
"groupByFolder": "按文件夹(路径浏览)",
|
||||
"browseRoot": "chat_uploads",
|
||||
"browseUp": "上级",
|
||||
"enterFolderTitle": "进入此文件夹",
|
||||
"copyFolderPathTitle": "复制该目录的相对路径(chat_uploads/…)",
|
||||
"folderPathCopied": "目录路径已复制,可粘贴到对话中",
|
||||
"folderEmpty": "此文件夹为空",
|
||||
"confirmDeleteFolder": "确定删除该文件夹及其中的全部文件?此操作不可恢复。",
|
||||
"deleteFolderTitle": "删除文件夹",
|
||||
"uploadToFolderTitle": "上传文件到此文件夹",
|
||||
"colSubPath": "子路径",
|
||||
"folderRoot": "(根目录)",
|
||||
"groupCount": "{{count}} 个文件",
|
||||
"convManual": "手动上传",
|
||||
"convNew": "新对话",
|
||||
"colDate": "日期",
|
||||
"colConversation": "会话",
|
||||
"colName": "文件名",
|
||||
|
||||
+553
-10
@@ -5,8 +5,76 @@ 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 = '';
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -95,6 +163,8 @@ function ensureChatFilesDocClickClose() {
|
||||
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);
|
||||
@@ -118,6 +188,8 @@ async function loadChatFilesPage() {
|
||||
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>';
|
||||
}
|
||||
@@ -127,7 +199,11 @@ 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((f) => (f.name || '').toLowerCase().includes(q));
|
||||
return files.filter(function (f) {
|
||||
const name = (f.name || '').toLowerCase();
|
||||
const sub = (f.subPath || '').toLowerCase();
|
||||
return name.includes(q) || sub.includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
/** 仅前端按文件名筛选,不重新请求 */
|
||||
@@ -236,6 +312,144 @@ function chatFilesAlertMessage(raw) {
|
||||
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 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;
|
||||
@@ -243,6 +457,8 @@ function renderChatFilesTable() {
|
||||
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);
|
||||
@@ -252,6 +468,7 @@ function renderChatFilesTable() {
|
||||
|
||||
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') : '修改时间';
|
||||
@@ -260,12 +477,14 @@ function renderChatFilesTable() {
|
||||
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') : '更多操作');
|
||||
|
||||
const rows = chatFilesDisplayed.map((f, idx) => {
|
||||
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 || '');
|
||||
@@ -295,9 +514,25 @@ function renderChatFilesTable() {
|
||||
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>
|
||||
@@ -312,20 +547,302 @@ function renderChatFilesTable() {
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
ensureChatFilesDocClickClose();
|
||||
|
||||
wrap.innerHTML = `<table class="chat-files-table"><thead><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><tbody>${rows}</tbody></table>`;
|
||||
</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 = chatFilesBuildTree(chatFilesDisplayed);
|
||||
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 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-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 = chatFilesBuildTree(chatFilesDisplayed);
|
||||
chatFilesNormalizeBrowsePathForTree(root);
|
||||
const next = chatFilesBrowsePath.concat([name]);
|
||||
if (!chatFilesResolveTreeNode(root, next)) return;
|
||||
chatFilesSetBrowsePath(next);
|
||||
renderChatFilesTable();
|
||||
}
|
||||
|
||||
function chatFilesNavigateBreadcrumb(level) {
|
||||
const root = chatFilesBuildTree(chatFilesDisplayed);
|
||||
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());
|
||||
}
|
||||
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;
|
||||
|
||||
function openChatFilesConversationIdx(idx) {
|
||||
const f = chatFilesDisplayed[idx];
|
||||
if (!f || !f.conversationId) return;
|
||||
@@ -501,15 +1018,41 @@ async function submitChatFilesRename() {
|
||||
}
|
||||
}
|
||||
|
||||
function chatFilesOpenUploadPicker() {
|
||||
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 conv = document.getElementById('chat-files-filter-conv');
|
||||
if (conv && conv.value.trim()) {
|
||||
form.append('conversationId', conv.value.trim());
|
||||
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 });
|
||||
|
||||
@@ -1013,7 +1013,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="chatFilesPage.title">文件管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button type="button" class="btn-primary" onclick="document.getElementById('chat-files-upload-input').click()" data-i18n="chatFilesPage.upload">上传文件</button>
|
||||
<button type="button" class="btn-primary" onclick="chatFilesOpenUploadPicker()" data-i18n="chatFilesPage.upload">上传文件</button>
|
||||
<input type="file" id="chat-files-upload-input" style="display:none" onchange="onChatFilesUploadPick(event)" />
|
||||
<button class="btn-secondary" onclick="loadChatFilesPage()" data-i18n="common.refresh">刷新</button>
|
||||
</div>
|
||||
@@ -1029,6 +1029,15 @@
|
||||
<span data-i18n="chatFilesPage.searchName">文件名</span>
|
||||
<input type="text" id="chat-files-filter-name" class="form-control" data-i18n="chatFilesPage.searchNamePlaceholder" data-i18n-attr="placeholder" placeholder="筛选文件名" oninput="chatFilesFilterNameOnInput()" onkeydown="if(event.key==='Enter') loadChatFilesPage()" />
|
||||
</label>
|
||||
<label>
|
||||
<span data-i18n="chatFilesPage.groupBy">分组</span>
|
||||
<select id="chat-files-group-by" class="form-control" onchange="chatFilesGroupByChange()">
|
||||
<option value="none" data-i18n="chatFilesPage.groupNone">不分组</option>
|
||||
<option value="date" data-i18n="chatFilesPage.groupByDate">按日期</option>
|
||||
<option value="conversation" data-i18n="chatFilesPage.groupByConversation">按会话</option>
|
||||
<option value="folder" data-i18n="chatFilesPage.groupByFolder">按文件夹</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn-secondary" type="button" onclick="loadChatFilesPage()" data-i18n="common.search">搜索</button>
|
||||
</div>
|
||||
<div id="chat-files-list-wrap" class="chat-files-table-wrap">
|
||||
|
||||
Reference in New Issue
Block a user