Compare commits
10 Commits
17d677923a
...
b7a9f8da9b
| Author | SHA1 | Date |
|---|---|---|
|
|
b7a9f8da9b | |
|
|
f0fb54e41f | |
|
|
8d5af9799c | |
|
|
6a25d6aa3c | |
|
|
3890cca114 | |
|
|
869e15d4ce | |
|
|
6c78f08769 | |
|
|
96f136a9bc | |
|
|
fcd490b1f5 | |
|
|
95eb004c18 |
|
|
@ -1,2 +1,67 @@
|
|||
{
|
||||
}
|
||||
"window.title": "${activeEditorShort}${separator}${separator}deer-flow/frontend",
|
||||
"todo-tree.regex.regex": "((%|#|//|<!--|\\{/\\*|^\\s*\\*)\\s*($TAGS)|^\\s*- \\[ \\])",
|
||||
"todo-tree.general.tags": [
|
||||
"TODO:",
|
||||
"BUG:",
|
||||
"TAG:",
|
||||
"DONE:",
|
||||
"MARK:",
|
||||
"TEST:",
|
||||
"XXX:"
|
||||
],
|
||||
"todo-tree.regex.regexCaseSensitive": false,
|
||||
"todo-tree.highlights.defaultHighlight": {
|
||||
"foreground": "#000000",
|
||||
"background": "#fff700",
|
||||
"icon": "check",
|
||||
"rulerColour": "#fff700",
|
||||
"type": "tag",
|
||||
"iconColour": "#fff700"
|
||||
},
|
||||
"todo-tree.highlights.customHighlight": {
|
||||
"TODO:": {
|
||||
"icon": "todo",
|
||||
"background": "#fff700",
|
||||
"rulerColour": "#fff700",
|
||||
"iconColour": "#fff700"
|
||||
},
|
||||
"BUG:": {
|
||||
"background": "#eb5c5c",
|
||||
"icon": "bug",
|
||||
"rulerColour": "#eb5c5c",
|
||||
"iconColour": "#eb5c5c"
|
||||
},
|
||||
"TAG:": {
|
||||
"background": "#38b2f4",
|
||||
"icon": "tag",
|
||||
"rulerColour": "#38b2f4",
|
||||
"iconColour": "#38b2f4",
|
||||
"rulerLane": "full"
|
||||
},
|
||||
"DONE:": {
|
||||
"background": "#5eec95",
|
||||
"icon": "check",
|
||||
"rulerColour": "#5eec95",
|
||||
"iconColour": "#5eec95"
|
||||
},
|
||||
"MARK:": {
|
||||
"background": "#f90",
|
||||
"icon": "note",
|
||||
"rulerColour": "#f90",
|
||||
"iconColour": "#f90"
|
||||
},
|
||||
"TEST:": {
|
||||
"background": "#df7be6",
|
||||
"icon": "flame",
|
||||
"rulerColour": "#df7be6",
|
||||
"iconColour": "#df7be6"
|
||||
},
|
||||
"XXX:": {
|
||||
"background": "#d65d8e",
|
||||
"icon": "versions",
|
||||
"rulerColour": "#d65d8e",
|
||||
"iconColour": "#d65d8e"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,16 +98,16 @@
|
|||
}
|
||||
|
||||
/* 标题和 meta 信息合并到一行 */
|
||||
#immersive-main-content-area .container > h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
#immersive-main-content-area .container #fileName {
|
||||
color: var(--333333, #333);
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: var(--spacing-md);
|
||||
font-size: 0.7em;
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
min-height: 20px; /* 确保最小高度 */
|
||||
}
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
display: flex;
|
||||
gap: 2px;
|
||||
height: 32px;
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--color-text-muted);
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
margin: 0;
|
||||
|
|
@ -170,9 +170,9 @@
|
|||
|
||||
#immersive-main-content-area .tabs-container .tab-btn.active {
|
||||
background-color: transparent;
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: #000F33;
|
||||
border-bottom-color: #000F33;
|
||||
/* font-weight: var(--font-weight-semibold); */
|
||||
}
|
||||
|
||||
/* ==================== 4. 滚动容器管理(核心重构)==================== */
|
||||
|
|
@ -399,7 +399,7 @@ body.immersive-active #immersive-main-content-area .container .tab-content {
|
|||
overflow: hidden !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-bg-secondary) !important;
|
||||
/* background: var(--color-bg-secondary) !important; */
|
||||
border: none !important;
|
||||
transition: all var(--transition-base) ease !important;
|
||||
}
|
||||
|
|
@ -674,10 +674,7 @@ body.immersive-active #immersive-main-content-area .container {
|
|||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* 沉浸模式下显示 PDF 对照按钮(用户要求在沉浸模式中也能使用) */
|
||||
body.immersive-active #tab-pdf-compare {
|
||||
display: flex !important;
|
||||
}
|
||||
/* PDF 对照按钮已隐藏(用户要求页面始终沉浸模式,无需此功能) */
|
||||
|
||||
/* Modal 和右键菜单的 z-index 提升 */
|
||||
body.immersive-active .modal-overlay {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ body {
|
|||
border-radius: 0; /* 移除圆角,纯下划线风格 */
|
||||
}
|
||||
|
||||
/* 隐藏的 Tab 按钮(用于中文文档隐藏翻译相关功能) */
|
||||
.tab-btn.hidden-tab {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.tab-btn:hover:not(.active) {
|
||||
color: var(--slate-700);
|
||||
background: transparent; /* 移除悬停背景,保持极简 */
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
/* ==================== Chatbot Modal Window (High-end Minimalist) ==================== */
|
||||
|
||||
.chatbot-window {
|
||||
background: var(--color-bg-base); /* Pure white background */
|
||||
|
||||
max-width: 720px;
|
||||
width: 92vw;
|
||||
min-height: 520px;
|
||||
|
|
@ -30,7 +30,6 @@
|
|||
border-radius: 24px;
|
||||
|
||||
/* The Shadow Fix: Using variable for consistency and lightness */
|
||||
box-shadow: var(--shadow-modal);
|
||||
border: 1px solid var(--color-border-light); /* Very subtle border */
|
||||
|
||||
position: absolute;
|
||||
|
|
@ -86,6 +85,7 @@
|
|||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -291,13 +291,13 @@ body.dark #chatbot-toast {
|
|||
/* 用户消息容器 */
|
||||
.user-message-container {
|
||||
justify-content: flex-end;
|
||||
padding-left: 20%;
|
||||
/* padding-left: 20%; */
|
||||
}
|
||||
|
||||
/* 助手消息容器 */
|
||||
.assistant-message-container {
|
||||
justify-content: flex-start;
|
||||
padding-right: 10%; /* 减少右侧留白,利用更多空间 */
|
||||
/* padding-right: 10%; */
|
||||
|
||||
/*
|
||||
IMPORTANT: No top padding here anymore.
|
||||
|
|
@ -344,6 +344,7 @@ body.dark #chatbot-toast {
|
|||
.chat-bubble.assistant {
|
||||
background: transparent;
|
||||
color: var(--color-text-primary);
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
|
@ -381,10 +382,17 @@ body.dark #chatbot-toast {
|
|||
/* ==================== 思考过程块样式 (Redesigned) ==================== */
|
||||
|
||||
.reasoning-block {
|
||||
background: rgba(248, 250, 252, 0.6); /* Slate-50 with opacity */
|
||||
display: inline-flex;
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
max-width: 100%;
|
||||
|
||||
background: #F8F9FA; /* Slate-50 with opacity */
|
||||
color: var(--slate-500);
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--slate-200); /* 更细腻的边框 */
|
||||
border-radius: 8px; /* 圆角 */
|
||||
/*
|
||||
margin-top removed because .assistant-message padding handles the offset now.
|
||||
|
|
@ -392,58 +400,52 @@ body.dark #chatbot-toast {
|
|||
*/
|
||||
margin: 0 0 16px 0;
|
||||
position: relative;
|
||||
font-size: 13px; /* 稍微小一点的字体 */
|
||||
}
|
||||
|
||||
.reasoning-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.reasoning-title {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--slate-400);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.reasoning-title::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--slate-300);
|
||||
border-radius: 50%;
|
||||
.reasoning-title {
|
||||
color: var(--666666, #666);
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.reasoning-toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
color: var(--slate-400);
|
||||
font-size: 12px;
|
||||
transition: color var(--transition-fast);
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.reasoning-toggle-btn:hover {
|
||||
color: var(--slate-600);
|
||||
.reasoning-toggle-icon {
|
||||
display: block;
|
||||
transform: rotate(180deg);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.reasoning-block.is-collapsed .reasoning-toggle-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.reasoning-content {
|
||||
margin-top: 8px;
|
||||
color: var(--slate-600);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--slate-100); /* 内部细线分隔 */
|
||||
font-family: var(--font-family-monospace); /* 使用等宽字体或更加技术感的字体 */
|
||||
color: var(--999999, #999);
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
/* ==================== 输入指示器动画 ==================== */
|
||||
|
|
@ -492,9 +494,14 @@ body.dark #chatbot-toast {
|
|||
|
||||
/* 输入区容器 */
|
||||
.chatbot-input-container {
|
||||
padding: 16px 24px 20px 24px;
|
||||
height: 160px;
|
||||
display: flex;
|
||||
margin: 0 20px 20px;
|
||||
border-radius: 10px;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.03);
|
||||
background: rgba(255, 255, 255, 0.8); /* 稍微降低不透明度以增强毛玻璃感 */
|
||||
background: #F8F9FA; /* 稍微降低不透明度以增强毛玻璃感 */
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
flex-shrink: 0;
|
||||
|
|
@ -505,9 +512,9 @@ body.dark #chatbot-toast {
|
|||
/* 输入区内部 Wrapper */
|
||||
.chatbot-input-wrapper {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
background: rgba(249, 250, 251, 0.8); /* var(--slate-50) with opacity */
|
||||
flex: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 16px;
|
||||
padding: 8px 8px 8px 14px;
|
||||
|
|
@ -516,14 +523,107 @@ body.dark #chatbot-toast {
|
|||
}
|
||||
|
||||
.chatbot-input-wrapper:focus-within {
|
||||
background: white;
|
||||
border-color: var(--slate-200); /* 柔和的边框颜色,避免刺眼的蓝线 */
|
||||
/* background: white; */
|
||||
/* border-color: var(--slate-200); 柔和的边框颜色,避免刺眼的蓝线 */
|
||||
/*
|
||||
使用更柔和的扩散阴影,而不是实心光晕
|
||||
原先的 0 0 0 3px 会产生类似边框的实心线效果
|
||||
改为 0 0 0 4px rgba(...) 并降低透明度,使其更像光晕
|
||||
*/
|
||||
box-shadow: 0 0 0 4px rgba(224, 231, 255, 0.4);
|
||||
/* box-shadow: 0 0 0 4px rgba(224, 231, 255, 0.4); */
|
||||
}
|
||||
|
||||
/* ==================== Floating Options (Switch/Selector) ==================== */
|
||||
|
||||
#chatbot-floating-options .chatbot-floating-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-floating-option-label {
|
||||
font-size: 11px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-floating-option.is-active {
|
||||
background: rgba(0, 15, 51, 0.08);
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-floating-option.is-active .chatbot-floating-option-label {
|
||||
color: #000F33;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Switch */
|
||||
#chatbot-floating-options .chatbot-floating-option-switch {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-switch {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-switch-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-switch-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(148, 163, 184, 0.55);
|
||||
border-radius: 999px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-switch-input:checked + .chatbot-switch-track {
|
||||
background: #000F33;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-switch-input:checked + .chatbot-switch-track .chatbot-switch-thumb {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
|
||||
/* Selector */
|
||||
#chatbot-floating-options .chatbot-floating-option-select {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-floating-selector {
|
||||
font-size: 11px;
|
||||
height: 22px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
color: #334155;
|
||||
padding: 0 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#chatbot-floating-options .chatbot-floating-option.is-active .chatbot-floating-selector {
|
||||
border-color: #000F33;
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
|
|
@ -784,4 +884,4 @@ body.dark #chatbot-toast {
|
|||
.markdown-content table::-webkit-scrollbar-thumb {
|
||||
background: var(--slate-300);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,19 +159,19 @@
|
|||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
transition: all var(--transition-fast) ease;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity var(--transition-fast) ease;
|
||||
}
|
||||
|
||||
.reasoning-toggle-btn:hover {
|
||||
color: #475569;
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.reasoning-toggle-btn:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* ==================== 9. 思维导图打开按钮 ==================== */
|
||||
|
|
|
|||
|
|
@ -61,10 +61,7 @@
|
|||
}
|
||||
|
||||
/* 将标题和 meta 信息合并到一行 */
|
||||
#immersive-main-content-area .container > h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
#immersive-main-content-area .container #fileName {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 12px;
|
||||
|
|
@ -289,7 +286,7 @@
|
|||
overflow: hidden !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--immersive-surface-bg) !important;
|
||||
/* background: var(--immersive-surface-bg) !important; */
|
||||
border: none !important;
|
||||
transition: all var(--immersive-transition-duration) ease !important;
|
||||
}
|
||||
|
|
@ -631,7 +628,7 @@ body.toc-dock-resizing #toc-vs-dock-resize-handle::before {
|
|||
}
|
||||
|
||||
/* 第一行:h2 和 meta 在同一行 - 极简设计 */
|
||||
#immersive-main-content-area .container > h2 {
|
||||
#immersive-main-content-area .container h2 {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
height: 24px;
|
||||
|
|
|
|||
3248
index.html
3248
index.html
File diff suppressed because it is too large
Load Diff
|
|
@ -90,7 +90,7 @@ async function uploadToMistral(fileToProcess, mistralKey) {
|
|||
*
|
||||
* @param {File|Blob} fileToProcess - 需要上传的 File 或 Blob 对象。
|
||||
* @param {string} fileName - 文件名。
|
||||
* @returns {Promise<string>} 上传成功后返回 OSS 的公网访问 URL。
|
||||
* @returns {Promise<{url: string, key: string}>} 上传成功后返回 OSS 的公网访问 URL 和 key。
|
||||
*/
|
||||
async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
||||
const formData = new FormData();
|
||||
|
|
@ -115,7 +115,7 @@ async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
|||
if (!resultData || !resultData.url) {
|
||||
throw new Error('上传成功但未返回有效的URL');
|
||||
}
|
||||
return resultData.url;
|
||||
return { url: resultData.url, key: resultData.key };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
88
js/app.js
88
js/app.js
|
|
@ -720,12 +720,28 @@ function setupEventListeners() {
|
|||
saveCurrentSettings();
|
||||
});
|
||||
|
||||
// 文件上传
|
||||
dropZone.addEventListener('dragover', handleDragOver);
|
||||
dropZone.addEventListener('dragleave', handleDragLeave);
|
||||
dropZone.addEventListener('drop', handleDrop);
|
||||
browseBtn.addEventListener('click', () => { if (!isProcessing) fileInput.click(); });
|
||||
fileInput.addEventListener('change', handleFileSelect);
|
||||
// 文件上传
|
||||
dropZone.addEventListener('dragover', handleDragOver);
|
||||
dropZone.addEventListener('dragleave', handleDragLeave);
|
||||
dropZone.addEventListener('drop', handleDrop);
|
||||
dropZone.addEventListener('click', (event) => {
|
||||
if (isProcessing) return;
|
||||
// 点击上传区空白区域时触发文件选择;避免与内部按钮重复触发。
|
||||
if (event.target.closest('button, a, input, label, select, textarea')) return;
|
||||
if (browseBtn) {
|
||||
browseBtn.click();
|
||||
return;
|
||||
}
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
});
|
||||
if (browseBtn && fileInput) {
|
||||
browseBtn.addEventListener('click', () => { if (!isProcessing) fileInput.click(); });
|
||||
}
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileSelect);
|
||||
}
|
||||
if (browseFolderBtn && folderInput) {
|
||||
browseFolderBtn.addEventListener('click', () => { if (!isProcessing) folderInput.click(); });
|
||||
}
|
||||
|
|
@ -1103,14 +1119,14 @@ async function extractFilesFromDataTransfer(dataTransfer) {
|
|||
return fallbackSnapshot;
|
||||
}
|
||||
|
||||
function refreshFormatFilters() {
|
||||
const container = document.getElementById('fileFormatFilters');
|
||||
if (!container) return;
|
||||
const counts = new Map();
|
||||
pdfFiles.forEach(file => {
|
||||
const ext = deriveExtension(file.name || '') || '';
|
||||
counts.set(ext, (counts.get(ext) || 0) + 1);
|
||||
});
|
||||
function refreshFormatFilters() {
|
||||
const container = document.getElementById('fileFormatFilters');
|
||||
if (!container) return;
|
||||
const counts = new Map();
|
||||
pdfFiles.forEach(file => {
|
||||
const ext = deriveExtension(file.name || '') || '';
|
||||
counts.set(ext, (counts.get(ext) || 0) + 1);
|
||||
});
|
||||
// 清理不存在的扩展
|
||||
Array.from(excludedExtensions).forEach(ext => {
|
||||
if (!counts.has(ext)) {
|
||||
|
|
@ -1125,23 +1141,39 @@ function refreshFormatFilters() {
|
|||
|
||||
const fragments = [];
|
||||
const entries = Array.from(counts.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
entries.forEach(([ext, count]) => {
|
||||
const checked = !isExtensionExcluded(ext) ? 'checked' : '';
|
||||
const label = ext ? ext.toUpperCase() : '未知';
|
||||
// XSS 防护:转义文件扩展名,防止恶意文件名注入
|
||||
const safeExt = escapeHtml(ext);
|
||||
const safeLabel = escapeHtml(label);
|
||||
fragments.push(`
|
||||
<label class="flex items-center space-x-1 bg-white border border-gray-200 rounded px-2 py-1 shadow-sm">
|
||||
<input type="checkbox" class="format-filter-checkbox" data-ext="${safeExt}" ${checked}>
|
||||
<span>${safeLabel} <span class="text-gray-400">(${count})</span></span>
|
||||
</label>
|
||||
entries.forEach(([ext, count]) => {
|
||||
const checked = !isExtensionExcluded(ext) ? 'checked' : '';
|
||||
const label = ext ? ext.toUpperCase() : '未知';
|
||||
// XSS 防护:转义文件扩展名,防止恶意文件名注入
|
||||
const safeExt = escapeHtml(ext);
|
||||
const safeLabel = escapeHtml(label);
|
||||
if ((ext || '').toLowerCase() === 'pdf') {
|
||||
excludedExtensions.delete(ext);
|
||||
fragments.push(`
|
||||
<!--
|
||||
<label class="flex items-center space-x-1 bg-white border border-gray-200 rounded px-2 py-1 shadow-sm">
|
||||
<input type="checkbox" class="format-filter-checkbox" data-ext="${safeExt}" ${checked}>
|
||||
<span>${safeLabel} <span class="text-gray-400">(${count})</span></span>
|
||||
</label>
|
||||
-->
|
||||
`);
|
||||
return;
|
||||
}
|
||||
fragments.push(`
|
||||
<label class="flex items-center space-x-1 bg-white border border-gray-200 rounded px-2 py-1 shadow-sm">
|
||||
<input type="checkbox" class="format-filter-checkbox" data-ext="${safeExt}" ${checked}>
|
||||
<span>${safeLabel} <span class="text-gray-400">(${count})</span></span>
|
||||
</label>
|
||||
`);
|
||||
});
|
||||
|
||||
// fragments.push('<div class="flex-grow"></div><button type="button" id="resetFormatFilters" class="text-xs text-blue-600">重置</button>');
|
||||
container.innerHTML = `<div class="flex flex-wrap gap-2 items-center">${fragments.join('')}</div>`;
|
||||
}
|
||||
// fragments.push('<div class="flex-grow"></div><button type="button" id="resetFormatFilters" class="text-xs text-blue-600">重置</button>');
|
||||
if (fragments.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `<div class="flex flex-wrap gap-2 items-center">${fragments.join('')}</div>`;
|
||||
}
|
||||
|
||||
function getActiveFiles() {
|
||||
return pdfFiles.filter(file => {
|
||||
|
|
|
|||
|
|
@ -110,8 +110,31 @@ window.ChatbotActions = {
|
|||
|
||||
// 调用上传
|
||||
console.log('[handleProvidePdf] 开始上传到 OSS...');
|
||||
const ossUrl = await window.uploadFileToOssViaProxy(pdfFileContent, `${currentDocName}`);
|
||||
console.log('[handleProvidePdf] OSS 上传成功, URL:', ossUrl);
|
||||
const { url: ossUrl, key: ossKey } = await window.uploadFileToOssViaProxy(pdfFileContent, `${currentDocName}`);
|
||||
console.log('[handleProvidePdf] OSS 上传成功, URL:', ossUrl, 'Key:', ossKey);
|
||||
|
||||
// 保存 OSS URL 到数据库
|
||||
if (window.storageAdapter && typeof window.storageAdapter.saveOssUrl === 'function') {
|
||||
try {
|
||||
await window.storageAdapter.saveOssUrl(dbDocId, ossUrl, ossKey);
|
||||
console.log('[handleProvidePdf] OSS URL 已保存到数据库');
|
||||
} catch (saveErr) {
|
||||
console.warn('[handleProvidePdf] 保存 OSS URL 到数据库失败:', saveErr);
|
||||
}
|
||||
} else {
|
||||
// 直接调用 API
|
||||
try {
|
||||
const apiUrl = (window.PBX_PROXY_BASE_URL || '/api') + `/documents/${dbDocId}/oss`;
|
||||
await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ossUrl, ossKey })
|
||||
});
|
||||
console.log('[handleProvidePdf] OSS URL 已通过 API 保存');
|
||||
} catch (apiErr) {
|
||||
console.warn('[handleProvidePdf] 通过 API 保存 OSS URL 失败:', apiErr);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建包含 file_url 的消息内容(智谱 AI 格式)
|
||||
const userMessageContent = [
|
||||
|
|
|
|||
|
|
@ -14,11 +14,8 @@ if (typeof window.ChatbotFloatingOptionsScriptLoaded === 'undefined') {
|
|||
const _chatbotOptionsConfig = [
|
||||
{ key: 'semanticGroups', texts: ['意群'], title: '查看/搜索意群', activeStyleColor: '#059669', isAction: true },
|
||||
{ key: 'useContext', texts: ['上下文:关', '上下文:开'], values: [false, true], title: '切换是否使用对话历史', activeStyleColor: '#1d4ed8' },
|
||||
{ key: 'useReActMode', texts: ['ReAct'], activeStyleColor: '#9ca3af', isDisabled: true, title: 'ReAct框架(开发中):推理+工具调用交织,智能动态构建上下文' },
|
||||
{ key: 'multiHopRetrieval', texts: ['检索Agent:关', '检索Agent:开'], values: [false, true], defaultKey: false, title: '开启后自动启用:多轮取材+流式显示+意群分析+向量搜索+重排', activeStyleColor: '#059669' },
|
||||
{ key: 'summarySource', texts: ['提供全文:OCR', '提供全文:无', '提供全文:翻译'], values: ['ocr', 'none', 'translation'], defaultKey: 'ocr', title: '切换总结时使用的文本源 (OCR/不使用文档内容/翻译)', activeStyleColor: '#1d4ed8' },
|
||||
{ key: 'interestPointsActive', texts: ['兴趣点'], activeStyleColor: '#059669', isPlaceholder: true, title: '兴趣点功能 (待实现)' },
|
||||
{ key: 'memoryManagementActive', texts: ['记忆管理'], activeStyleColor: '#059669', isPlaceholder: true, title: '记忆管理功能 (待实现)' }
|
||||
{ key: 'summarySource', texts: ['提供全文:OCR', '提供全文:无', '提供全文:翻译'], values: ['ocr', 'none', 'translation'], defaultKey: 'ocr', title: '切换总结时使用的文本源 (OCR/不使用文档内容/翻译)', activeStyleColor: '#1d4ed8' }
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -26,16 +23,16 @@ if (typeof window.ChatbotFloatingOptionsScriptLoaded === 'undefined') {
|
|||
* @param {HTMLElement} parentElement - 选项栏将被添加到的父容器。
|
||||
* @param {function} globalUpdateUICallback - 全局UI更新回调函数,例如 window.ChatbotUI.updateChatbotUI。
|
||||
*/
|
||||
function _createFloatingOptionsBar(parentElement, globalUpdateUICallback) {
|
||||
if (!parentElement || document.getElementById('chatbot-floating-options')) {
|
||||
return; // 如果父元素不存在或选项栏已存在,则不重复创建
|
||||
}
|
||||
function _createFloatingOptionsBar(parentElement, globalUpdateUICallback) {
|
||||
if (!parentElement || document.getElementById('chatbot-floating-options')) {
|
||||
return; // 如果父元素不存在或选项栏已存在,则不重复创建
|
||||
}
|
||||
|
||||
const floatingOptionsContainer = document.createElement('div');
|
||||
floatingOptionsContainer.id = 'chatbot-floating-options';
|
||||
floatingOptionsContainer.style.cssText = `
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0 8px 0;
|
||||
gap: 5px;
|
||||
|
|
@ -44,115 +41,197 @@ if (typeof window.ChatbotFloatingOptionsScriptLoaded === 'undefined') {
|
|||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
_chatbotOptionsConfig.forEach((optConf, index) => {
|
||||
const optionButton = document.createElement('button');
|
||||
optionButton.id = `chatbot-option-${optConf.key}`;
|
||||
optionButton.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
`;
|
||||
optionButton.title = optConf.title;
|
||||
_chatbotOptionsConfig.forEach((optConf, index) => {
|
||||
const optionEl = (function createOptionElement() {
|
||||
// useContext:switch 组件(替代“上下文:开/关”文字)
|
||||
if (optConf.key === 'useContext') {
|
||||
const wrapper = document.createElement('label');
|
||||
wrapper.id = `chatbot-option-${optConf.key}`;
|
||||
wrapper.className = 'chatbot-floating-option chatbot-floating-option-switch';
|
||||
wrapper.title = optConf.title;
|
||||
|
||||
const labelSpan = document.createElement('span');
|
||||
labelSpan.className = 'chatbot-floating-option-label';
|
||||
labelSpan.textContent = '上下文';
|
||||
|
||||
const switchWrap = document.createElement('span');
|
||||
switchWrap.className = 'chatbot-switch';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.id = 'chatbot-use-context-switch';
|
||||
input.className = 'chatbot-switch-input';
|
||||
window.chatbotActiveOptions = window.chatbotActiveOptions || {};
|
||||
input.checked = !!window.chatbotActiveOptions.useContext;
|
||||
wrapper.classList.toggle('is-active', !!window.chatbotActiveOptions.useContext);
|
||||
|
||||
const track = document.createElement('span');
|
||||
track.className = 'chatbot-switch-track';
|
||||
|
||||
const thumb = document.createElement('span');
|
||||
thumb.className = 'chatbot-switch-thumb';
|
||||
track.appendChild(thumb);
|
||||
|
||||
switchWrap.appendChild(input);
|
||||
switchWrap.appendChild(track);
|
||||
|
||||
wrapper.appendChild(labelSpan);
|
||||
wrapper.appendChild(switchWrap);
|
||||
|
||||
input.addEventListener('change', function() {
|
||||
window.chatbotActiveOptions = window.chatbotActiveOptions || {};
|
||||
window.chatbotActiveOptions.useContext = !!input.checked;
|
||||
wrapper.classList.toggle('is-active', !!input.checked);
|
||||
if (typeof globalUpdateUICallback === 'function') globalUpdateUICallback();
|
||||
else console.error("ChatbotFloatingOptionsUI: globalUpdateUICallback is not provided or not a function.");
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// summarySource:selector 组件(替代“提供全文:xxx”循环切换按钮)
|
||||
if (optConf.key === 'summarySource') {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.id = `chatbot-option-${optConf.key}`;
|
||||
wrapper.className = 'chatbot-floating-option chatbot-floating-option-select';
|
||||
wrapper.title = optConf.title;
|
||||
|
||||
const labelSpan = document.createElement('span');
|
||||
labelSpan.className = 'chatbot-floating-option-label';
|
||||
labelSpan.textContent = '提供全文';
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.id = 'chatbot-summary-source-select';
|
||||
select.className = 'chatbot-floating-selector';
|
||||
|
||||
optConf.values.forEach((val, idx) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = val;
|
||||
const rawText = optConf.texts[idx] || String(val);
|
||||
opt.textContent = rawText.replace(/^提供全文:/, '');
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
window.chatbotActiveOptions = window.chatbotActiveOptions || {};
|
||||
select.value = window.chatbotActiveOptions.summarySource || optConf.defaultKey || optConf.values[0];
|
||||
wrapper.classList.toggle('is-active', select.value !== optConf.defaultKey);
|
||||
|
||||
select.addEventListener('change', function() {
|
||||
window.chatbotActiveOptions = window.chatbotActiveOptions || {};
|
||||
window.chatbotActiveOptions.summarySource = select.value;
|
||||
wrapper.classList.toggle('is-active', select.value !== optConf.defaultKey);
|
||||
if (typeof globalUpdateUICallback === 'function') globalUpdateUICallback();
|
||||
else console.error("ChatbotFloatingOptionsUI: globalUpdateUICallback is not provided or not a function.");
|
||||
});
|
||||
|
||||
wrapper.appendChild(labelSpan);
|
||||
wrapper.appendChild(select);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// 默认:沿用按钮
|
||||
const optionButton = document.createElement('button');
|
||||
optionButton.id = `chatbot-option-${optConf.key}`;
|
||||
optionButton.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
`;
|
||||
optionButton.title = optConf.title;
|
||||
|
||||
optionButton.onclick = function() {
|
||||
window.chatbotActiveOptions = window.chatbotActiveOptions || {};
|
||||
|
||||
if (optConf.isDisabled) {
|
||||
console.log(`${optConf.key} clicked, but is disabled.`);
|
||||
if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) {
|
||||
ChatbotUtils.showToast(`${optConf.texts[0]} 功能开发中,暂不可用。`, 'info', 2000);
|
||||
} else {
|
||||
alert(`${optConf.texts[0]} 功能开发中,暂不可用。`);
|
||||
}
|
||||
return; // 阻止后续逻辑执行
|
||||
}
|
||||
|
||||
if (optConf.isPlaceholder) {
|
||||
console.log(`${optConf.key} clicked, placeholder for future feature.`);
|
||||
if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) {
|
||||
ChatbotUtils.showToast(`${optConf.texts[0]} 功能正在开发中。`, 'info', 2000);
|
||||
} else {
|
||||
alert(`${optConf.texts[0]} 功能正在开发中。`);
|
||||
}
|
||||
} else if (optConf.isAction && optConf.key === 'semanticGroups') {
|
||||
if (window.SemanticGroupsUI && typeof window.SemanticGroupsUI.toggle === 'function') {
|
||||
window.SemanticGroupsUI.toggle();
|
||||
} else if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) {
|
||||
ChatbotUtils.showToast('意群面板未就绪', 'warning', 2000);
|
||||
} else {
|
||||
alert('意群面板未就绪');
|
||||
}
|
||||
} else {
|
||||
const currentValue = window.chatbotActiveOptions[optConf.key];
|
||||
if (optConf.key === 'contentLengthStrategy') {
|
||||
window.chatbotActiveOptions.contentLengthStrategy = currentValue === optConf.values[0] ? optConf.values[1] : optConf.values[0];
|
||||
} else if (optConf.key === 'multiHopRetrieval') {
|
||||
window.chatbotActiveOptions.multiHopRetrieval = !currentValue;
|
||||
// 传统多轮检索开启时,自动关闭ReAct模式(避免冲突)
|
||||
if (window.chatbotActiveOptions.multiHopRetrieval) {
|
||||
window.chatbotActiveOptions.useReActMode = false;
|
||||
}
|
||||
} else if (optConf.key === 'streamingRetrieval') {
|
||||
window.chatbotActiveOptions.streamingRetrieval = !currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof globalUpdateUICallback === 'function') {
|
||||
globalUpdateUICallback(); // 更新UI以反映选项变化
|
||||
} else {
|
||||
console.error("ChatbotFloatingOptionsUI: globalUpdateUICallback is not provided or not a function.");
|
||||
}
|
||||
};
|
||||
|
||||
return optionButton;
|
||||
})();
|
||||
|
||||
floatingOptionsContainer.appendChild(optionEl);
|
||||
|
||||
if (index < _chatbotOptionsConfig.length - 1) {
|
||||
const nextOptConf = _chatbotOptionsConfig[index + 1];
|
||||
const separator = document.createElement('span');
|
||||
separator.id = `chatbot-separator-${nextOptConf.key}`;
|
||||
separator.textContent = '丨';
|
||||
separator.style.color = '#cbd5e1';
|
||||
separator.style.margin = '0 2px';
|
||||
floatingOptionsContainer.appendChild(separator);
|
||||
}
|
||||
});
|
||||
|
||||
optionButton.onclick = function() {
|
||||
if (optConf.isDisabled) {
|
||||
console.log(`${optConf.key} clicked, but is disabled.`);
|
||||
if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) {
|
||||
ChatbotUtils.showToast(`${optConf.texts[0]} 功能开发中,暂不可用。`, 'info', 2000);
|
||||
} else {
|
||||
alert(`${optConf.texts[0]} 功能开发中,暂不可用。`);
|
||||
}
|
||||
return; // 阻止后续逻辑执行
|
||||
}
|
||||
if (optConf.isPlaceholder) {
|
||||
console.log(`${optConf.key} clicked, placeholder for future feature.`);
|
||||
if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) {
|
||||
ChatbotUtils.showToast(`${optConf.texts[0]} 功能正在开发中。`, 'info', 2000);
|
||||
} else {
|
||||
alert(`${optConf.texts[0]} 功能正在开发中。`);
|
||||
}
|
||||
} else if (optConf.isAction && optConf.key === 'semanticGroups') {
|
||||
if (window.SemanticGroupsUI && typeof window.SemanticGroupsUI.toggle === 'function') {
|
||||
window.SemanticGroupsUI.toggle();
|
||||
} else if (typeof ChatbotUtils !== 'undefined' && ChatbotUtils.showToast) {
|
||||
ChatbotUtils.showToast('意群面板未就绪', 'warning', 2000);
|
||||
} else {
|
||||
alert('意群面板未就绪');
|
||||
}
|
||||
} else {
|
||||
const currentValue = window.chatbotActiveOptions[optConf.key];
|
||||
if (optConf.key === 'useContext') {
|
||||
window.chatbotActiveOptions.useContext = !currentValue;
|
||||
} else if (optConf.key === 'useReActMode') {
|
||||
window.chatbotActiveOptions.useReActMode = !currentValue;
|
||||
// ReAct模式开启时,自动关闭传统多轮检索(避免冲突)
|
||||
if (window.chatbotActiveOptions.useReActMode) {
|
||||
window.chatbotActiveOptions.multiHopRetrieval = false;
|
||||
}
|
||||
} else if (optConf.key === 'contentLengthStrategy') {
|
||||
window.chatbotActiveOptions.contentLengthStrategy = currentValue === optConf.values[0] ? optConf.values[1] : optConf.values[0];
|
||||
} else if (optConf.key === 'multiHopRetrieval') {
|
||||
window.chatbotActiveOptions.multiHopRetrieval = !currentValue;
|
||||
// 传统多轮检索开启时,自动关闭ReAct模式(避免冲突)
|
||||
if (window.chatbotActiveOptions.multiHopRetrieval) {
|
||||
window.chatbotActiveOptions.useReActMode = false;
|
||||
}
|
||||
} else if (optConf.key === 'streamingRetrieval') {
|
||||
window.chatbotActiveOptions.streamingRetrieval = !currentValue;
|
||||
} else if (optConf.key === 'summarySource') {
|
||||
const currentIndex = optConf.values.indexOf(currentValue);
|
||||
const nextIndex = (currentIndex + 1) % optConf.values.length;
|
||||
window.chatbotActiveOptions.summarySource = optConf.values[nextIndex];
|
||||
}
|
||||
}
|
||||
if (typeof globalUpdateUICallback === 'function') {
|
||||
globalUpdateUICallback(); // 更新UI以反映选项变化
|
||||
} else {
|
||||
console.error("ChatbotFloatingOptionsUI: globalUpdateUICallback is not provided or not a function.");
|
||||
}
|
||||
};
|
||||
floatingOptionsContainer.appendChild(optionButton);
|
||||
|
||||
if (index < _chatbotOptionsConfig.length - 1) {
|
||||
const nextOptConf = _chatbotOptionsConfig[index + 1];
|
||||
const separator = document.createElement('span');
|
||||
separator.id = `chatbot-separator-${nextOptConf.key}`;
|
||||
separator.textContent = '丨';
|
||||
separator.style.color = '#cbd5e1';
|
||||
separator.style.margin = '0 2px';
|
||||
floatingOptionsContainer.appendChild(separator);
|
||||
}
|
||||
});
|
||||
|
||||
// 将创建的选项栏插入到父容器的合适位置
|
||||
const selectedImagesPreview = parentElement.querySelector('#chatbot-selected-images-preview');
|
||||
if (selectedImagesPreview) {
|
||||
parentElement.insertBefore(floatingOptionsContainer, selectedImagesPreview);
|
||||
// 将创建的选项栏插入到父容器的合适位置:放在输入框容器(chatbot-input-wrapper)后面
|
||||
const inputWrapper = parentElement.querySelector('#chatbot-input-wrapper, .chatbot-input-wrapper');
|
||||
if (inputWrapper && inputWrapper.parentNode === parentElement) {
|
||||
parentElement.insertBefore(floatingOptionsContainer, inputWrapper.nextSibling);
|
||||
} else if (inputWrapper && inputWrapper.parentNode) {
|
||||
inputWrapper.parentNode.insertBefore(floatingOptionsContainer, inputWrapper.nextSibling);
|
||||
} else {
|
||||
const mainInputDiv = parentElement.querySelector('div[style*="display:flex;align-items:center;gap:12px;"]');
|
||||
if (mainInputDiv) {
|
||||
parentElement.insertBefore(floatingOptionsContainer, mainInputDiv);
|
||||
} else {
|
||||
parentElement.appendChild(floatingOptionsContainer); // Fallback
|
||||
}
|
||||
parentElement.appendChild(floatingOptionsContainer); // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新浮动高级选项栏中各个按钮的显示状态(文本、样式、可见性)。
|
||||
*/
|
||||
function _updateFloatingOptionsDisplay() {
|
||||
const floatingOptionsContainer = document.getElementById('chatbot-floating-options');
|
||||
if (!floatingOptionsContainer) return;
|
||||
function _updateFloatingOptionsDisplay() {
|
||||
const floatingOptionsContainer = document.getElementById('chatbot-floating-options');
|
||||
if (!floatingOptionsContainer) return;
|
||||
window.chatbotActiveOptions = window.chatbotActiveOptions || {};
|
||||
|
||||
_chatbotOptionsConfig.forEach(optConf => {
|
||||
const button = document.getElementById(`chatbot-option-${optConf.key}`);
|
||||
// 注意:分隔符ID是基于 *下一个* 选项的key来创建的,所以查找ID为 chatbot-separator-NEXT_KEY
|
||||
_chatbotOptionsConfig.forEach(optConf => {
|
||||
const button = document.getElementById(`chatbot-option-${optConf.key}`);
|
||||
// 注意:分隔符ID是基于 *下一个* 选项的key来创建的,所以查找ID为 chatbot-separator-NEXT_KEY
|
||||
// 但是其显示与否是基于 *当前* 按钮的显示状态
|
||||
|
||||
if (button) {
|
||||
|
|
@ -238,42 +317,42 @@ if (typeof window.ChatbotFloatingOptionsScriptLoaded === 'undefined') {
|
|||
}
|
||||
});
|
||||
|
||||
// 第三遍:更新按钮文本和样式
|
||||
_chatbotOptionsConfig.forEach(optConf => {
|
||||
const button = document.getElementById(`chatbot-option-${optConf.key}`);
|
||||
// 第三遍:更新按钮文本和样式
|
||||
_chatbotOptionsConfig.forEach(optConf => {
|
||||
const button = document.getElementById(`chatbot-option-${optConf.key}`);
|
||||
|
||||
if (!button || button.style.display === 'none') {
|
||||
return; // 跳过不可见的按钮
|
||||
}
|
||||
if (!button || button.style.display === 'none') {
|
||||
return; // 跳过不可见的按钮
|
||||
}
|
||||
|
||||
const currentOptionValue = window.chatbotActiveOptions[optConf.key];
|
||||
let currentText = '';
|
||||
let color = '#4b5563';
|
||||
let fontWeight = 'normal';
|
||||
const currentOptionValue = window.chatbotActiveOptions[optConf.key];
|
||||
let currentText = '';
|
||||
let color = '#4b5563';
|
||||
let fontWeight = 'normal';
|
||||
let isActiveStyle = false;
|
||||
|
||||
if (optConf.isAction && optConf.key === 'semanticGroups') {
|
||||
const count = (window.data && Array.isArray(window.data.semanticGroups)) ? window.data.semanticGroups.length : 0;
|
||||
currentText = count > 0 ? `意群(${count})` : '意群';
|
||||
// 显示为激活风格以便更醒目(当有意群时)
|
||||
if (count > 0) { color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true; }
|
||||
} else if (optConf.isPlaceholder) {
|
||||
currentText = optConf.texts[0];
|
||||
} else if (optConf.isDisabled) {
|
||||
// 禁用状态:固定显示灰色,无法切换
|
||||
currentText = optConf.texts[0];
|
||||
if (optConf.isAction && optConf.key === 'semanticGroups') {
|
||||
const count = (window.data && Array.isArray(window.data.semanticGroups)) ? window.data.semanticGroups.length : 0;
|
||||
currentText = count > 0 ? `意群(${count})` : '意群';
|
||||
// 显示为激活风格以便更醒目(当有意群时)
|
||||
if (count > 0) { color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true; }
|
||||
} else if (optConf.key === 'useContext') {
|
||||
const isOn = !!currentOptionValue;
|
||||
const switchInput = document.getElementById('chatbot-use-context-switch');
|
||||
if (switchInput) switchInput.checked = isOn;
|
||||
button.classList.toggle('is-active', isOn);
|
||||
return;
|
||||
} else if (optConf.isPlaceholder) {
|
||||
currentText = optConf.texts[0];
|
||||
} else if (optConf.isDisabled) {
|
||||
// 禁用状态:固定显示灰色,无法切换
|
||||
currentText = optConf.texts[0];
|
||||
color = '#9ca3af'; // 灰色
|
||||
fontWeight = 'normal';
|
||||
isActiveStyle = false;
|
||||
button.style.opacity = '0.6'; // 降低透明度
|
||||
button.style.cursor = 'not-allowed'; // 禁用光标
|
||||
} else if (optConf.key === 'useContext') {
|
||||
currentText = currentOptionValue ? optConf.texts[1] : optConf.texts[0];
|
||||
if (currentOptionValue) { color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true; }
|
||||
} else if (optConf.key === 'useReActMode') {
|
||||
currentText = currentOptionValue ? optConf.texts[1] : optConf.texts[0];
|
||||
if (currentOptionValue) { color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true; }
|
||||
} else if (optConf.key === 'contentLengthStrategy') {
|
||||
} else if (optConf.key === 'contentLengthStrategy') {
|
||||
currentText = currentOptionValue === optConf.defaultKey ? optConf.texts[0] : optConf.texts[1];
|
||||
if (currentOptionValue !== optConf.defaultKey) {
|
||||
color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true;
|
||||
|
|
@ -281,20 +360,19 @@ if (typeof window.ChatbotFloatingOptionsScriptLoaded === 'undefined') {
|
|||
} else if (optConf.key === 'multiHopRetrieval') {
|
||||
currentText = currentOptionValue ? optConf.texts[1] : optConf.texts[0];
|
||||
if (currentOptionValue) { color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true; }
|
||||
} else if (optConf.key === 'streamingRetrieval') {
|
||||
currentText = currentOptionValue ? optConf.texts[1] : optConf.texts[0];
|
||||
if (currentOptionValue) { color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true; }
|
||||
} else if (optConf.key === 'summarySource') {
|
||||
const currentIndex = optConf.values.indexOf(currentOptionValue);
|
||||
currentText = optConf.texts[currentIndex] || optConf.texts[0];
|
||||
// 对于 summarySource, 'ocr' (index 0) 是默认状态,'none' (index 1) 和 'translation' (index 2) 算作激活状态
|
||||
if (currentOptionValue !== optConf.defaultKey) {
|
||||
color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true;
|
||||
}
|
||||
}
|
||||
button.textContent = currentText;
|
||||
button.style.color = color;
|
||||
button.style.fontWeight = fontWeight;
|
||||
} else if (optConf.key === 'streamingRetrieval') {
|
||||
currentText = currentOptionValue ? optConf.texts[1] : optConf.texts[0];
|
||||
if (currentOptionValue) { color = optConf.activeStyleColor; fontWeight = '600'; isActiveStyle = true; }
|
||||
} else if (optConf.key === 'summarySource') {
|
||||
const select = document.getElementById('chatbot-summary-source-select');
|
||||
const desiredValue = currentOptionValue || optConf.defaultKey || optConf.values[0];
|
||||
if (select && select.value !== desiredValue) select.value = desiredValue;
|
||||
button.classList.toggle('is-active', desiredValue !== optConf.defaultKey);
|
||||
return;
|
||||
}
|
||||
button.textContent = currentText;
|
||||
button.style.color = color;
|
||||
button.style.fontWeight = fontWeight;
|
||||
|
||||
if (isActiveStyle) {
|
||||
button.style.backgroundColor = 'rgba(59, 130, 246, 0.1)'; // 淡蓝色背景表示激活
|
||||
|
|
|
|||
|
|
@ -310,6 +310,7 @@ window.ChatbotMessageRenderer = {
|
|||
if (m.reasoningContent) {
|
||||
const reasoningId = `reasoning-block-${index}`;
|
||||
const collapsed = window[`reasoningCollapsed_${index}`] === true;
|
||||
const collapsedClass = collapsed ? ' is-collapsed' : '';
|
||||
let renderedReasoningContent = '';
|
||||
try {
|
||||
if (typeof renderWithKatexStreaming === 'function') {
|
||||
|
|
@ -324,13 +325,17 @@ window.ChatbotMessageRenderer = {
|
|||
// Phase 3: 思考过程折叠按钮事件委托
|
||||
if (USE_EVENT_DELEGATION) {
|
||||
reasoningBlock = `
|
||||
<div id="${reasoningId}" class="reasoning-block">
|
||||
<div id="${reasoningId}" class="reasoning-block${collapsedClass}">
|
||||
<div class="reasoning-header">
|
||||
<span class="reasoning-title">思考过程</span>
|
||||
<button class="reasoning-toggle-btn"
|
||||
data-action="toggle-reasoning"
|
||||
data-index="${index}">
|
||||
${collapsed ? '▼' : '▲'}
|
||||
data-index="${index}"
|
||||
aria-label="展开/收起思考过程"
|
||||
aria-expanded="${collapsed ? 'false' : 'true'}">
|
||||
<svg class="reasoning-toggle-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6L8 10L4 6" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="reasoning-content" style="${collapsed ? 'display:none;' : ''}">
|
||||
|
|
@ -341,11 +346,13 @@ window.ChatbotMessageRenderer = {
|
|||
} else {
|
||||
// 旧版本:内联事件
|
||||
reasoningBlock = `
|
||||
<div id="${reasoningId}" class="reasoning-block">
|
||||
<div id="${reasoningId}" class="reasoning-block${collapsedClass}">
|
||||
<div class="reasoning-header">
|
||||
<span class="reasoning-title">思考过程</span>
|
||||
<button class="reasoning-toggle-btn" onclick="(function(){window['reasoningCollapsed_${index}']=!window['reasoningCollapsed_${index}'];window.ChatbotUI.updateChatbotUI();})()">
|
||||
${collapsed ? '▼' : '▲'}
|
||||
<svg class="reasoning-toggle-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6L8 10L4 6" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="reasoning-content" style="${collapsed ? 'display:none;' : ''}">
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ window.ChatbotPresetQuestionsUI = {
|
|||
headerLeftGroup.style.display = 'flex';
|
||||
headerLeftGroup.style.alignItems = 'center';
|
||||
headerLeftGroup.style.gap = '8px';
|
||||
headerLeftGroup.appendChild(presetTitle);
|
||||
headerLeftGroup.appendChild(presetToggleBtn);
|
||||
newPresetHeader.appendChild(headerLeftGroup);
|
||||
// headerLeftGroup.appendChild(presetTitle);
|
||||
// headerLeftGroup.appendChild(presetToggleBtn);
|
||||
// newPresetHeader.appendChild(headerLeftGroup);
|
||||
|
||||
// 齿轮按钮(模型配置)- 始终显示
|
||||
const gearBtn = document.createElement('button');
|
||||
|
|
@ -92,8 +92,8 @@ window.ChatbotPresetQuestionsUI = {
|
|||
updateChatbotUICallback(); // 调用主UI更新
|
||||
}
|
||||
};
|
||||
newPresetHeader.appendChild(gearBtn);
|
||||
presetContainer.appendChild(newPresetHeader);
|
||||
// newPresetHeader.appendChild(gearBtn);
|
||||
// presetContainer.appendChild(newPresetHeader);
|
||||
|
||||
const newPresetBody = document.createElement('div');
|
||||
newPresetBody.id = 'chatbot-preset-body';
|
||||
|
|
@ -114,31 +114,31 @@ window.ChatbotPresetQuestionsUI = {
|
|||
|
||||
|
||||
// 填充预设问题按钮
|
||||
const presetQuestions = (window.ChatbotPreset && window.ChatbotPreset.PRESET_QUESTIONS) ? window.ChatbotPreset.PRESET_QUESTIONS : [
|
||||
'总结本文', '有哪些关键公式?', '研究背景与意义?', '研究方法及发现?',
|
||||
'应用与前景?', '用通俗语言解释全文', '生成思维导图🧠', '生成流程图🔄',
|
||||
'生成更多配图🎨'
|
||||
];
|
||||
presetQuestions.forEach(q => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'preset-btn';
|
||||
// 使用 encodeURIComponent/decodeURIComponent 来处理特殊字符
|
||||
button.onclick = function() {
|
||||
const text = decodeURIComponent(encodeURIComponent(q));
|
||||
// 对“生成更多配图”做特殊处理:只帮用户打出前缀 [加入配图],不直接发送复杂提示
|
||||
if (text.startsWith('生成更多配图')) {
|
||||
const input = document.getElementById('chatbot-input');
|
||||
if (input) {
|
||||
input.value = '[加入配图] ';
|
||||
input.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
handlePresetQuestionCallback(text);
|
||||
};
|
||||
button.textContent = q;
|
||||
newPresetBody.appendChild(button);
|
||||
});
|
||||
// const presetQuestions = (window.ChatbotPreset && window.ChatbotPreset.PRESET_QUESTIONS) ? window.ChatbotPreset.PRESET_QUESTIONS : [
|
||||
// '总结本文', '有哪些关键公式?', '研究背景与意义?', '研究方法及发现?',
|
||||
// '应用与前景?', '用通俗语言解释全文', '生成思维导图🧠', '生成流程图🔄',
|
||||
// '生成更多配图🎨'
|
||||
// ];
|
||||
// presetQuestions.forEach(q => {
|
||||
// const button = document.createElement('button');
|
||||
// button.className = 'preset-btn';
|
||||
// // 使用 encodeURIComponent/decodeURIComponent 来处理特殊字符
|
||||
// button.onclick = function() {
|
||||
// const text = decodeURIComponent(encodeURIComponent(q));
|
||||
// // 对“生成更多配图”做特殊处理:只帮用户打出前缀 [加入配图],不直接发送复杂提示
|
||||
// if (text.startsWith('生成更多配图')) {
|
||||
// const input = document.getElementById('chatbot-input');
|
||||
// if (input) {
|
||||
// input.value = '[加入配图] ';
|
||||
// input.focus();
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// handlePresetQuestionCallback(text);
|
||||
// };
|
||||
// button.textContent = q;
|
||||
// newPresetBody.appendChild(button);
|
||||
// });
|
||||
|
||||
// 自动收起逻辑 (依赖全局状态 window.isPresetQuestionsCollapsed, window.presetAutoCollapseTriggeredForDoc, window.isModelSelectorOpen 和 ChatbotCore)
|
||||
let userMessageCount = 0;
|
||||
|
|
|
|||
|
|
@ -353,8 +353,6 @@ function updateChatbotUI() {
|
|||
}
|
||||
|
||||
const chatBody = document.getElementById('chatbot-body');
|
||||
const chatbotPresetHeader = document.getElementById('chatbot-preset-header');
|
||||
const chatbotPresetBody = document.getElementById('chatbot-preset-body');
|
||||
let modelSelectorDiv = document.getElementById('chatbot-model-selector');
|
||||
|
||||
const existingPresetContainer = document.getElementById('chatbot-preset-container');
|
||||
|
|
@ -383,42 +381,14 @@ function updateChatbotUI() {
|
|||
console.error("Error getting chatbot config for UI:", e);
|
||||
}
|
||||
|
||||
const presetContainer = window.ChatbotPresetQuestionsUI.render(
|
||||
chatbotWindow,
|
||||
isCustomModel,
|
||||
currentDocId,
|
||||
updateChatbotUI,
|
||||
window.ChatbotPreset?.handlePresetQuestion || window.handlePresetQuestion
|
||||
);
|
||||
|
||||
chatbotWindow.appendChild(presetContainer);
|
||||
|
||||
let userMessageCount = 0;
|
||||
if (window.ChatbotCore && window.ChatbotCore.chatHistory) {
|
||||
userMessageCount = window.ChatbotCore.chatHistory.filter(m => m.role === 'user').length;
|
||||
}
|
||||
|
||||
if (userMessageCount >= 3 &&
|
||||
!window.presetAutoCollapseTriggeredForDoc[currentDocId] &&
|
||||
!window.isPresetQuestionsCollapsed &&
|
||||
!window.isModelSelectorOpen) {
|
||||
window.isPresetQuestionsCollapsed = true;
|
||||
window.presetAutoCollapseTriggeredForDoc[currentDocId] = true;
|
||||
const presetToggleBtn = presetContainer.querySelector('#chatbot-preset-toggle-btn');
|
||||
if (presetToggleBtn) {
|
||||
presetToggleBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
||||
presetToggleBtn.title = "展开快捷指令";
|
||||
}
|
||||
}
|
||||
// 按需禁用:不渲染快捷指令(chatbot-preset-container)
|
||||
// 仅确保历史遗留 DOM 被移除,避免占用空间或影响布局
|
||||
|
||||
// 获取 mainContentArea 元素
|
||||
const mainContentArea = document.getElementById('chatbot-main-content-area');
|
||||
|
||||
if (mainContentArea) {
|
||||
let current_padding_top_for_main_content_area = 12;
|
||||
if (presetContainer && presetContainer.style.display !== 'none' && presetContainer.offsetHeight) {
|
||||
current_padding_top_for_main_content_area = presetContainer.offsetHeight;
|
||||
}
|
||||
mainContentArea.style.paddingTop = current_padding_top_for_main_content_area + 'px';
|
||||
|
||||
// 仅在“固定模式”下根据内容自适应高度;浮动/全屏不改动用户设置的尺寸
|
||||
|
|
@ -451,7 +421,6 @@ function updateChatbotUI() {
|
|||
} else {
|
||||
const existingModelSelectorDiv = document.getElementById('chatbot-model-selector');
|
||||
if (existingModelSelectorDiv) existingModelSelectorDiv.remove();
|
||||
if (presetContainer) presetContainer.style.display = '';
|
||||
if (chatBody) chatBody.style.display = '';
|
||||
}
|
||||
if (chatBody) {
|
||||
|
|
@ -1221,7 +1190,7 @@ function initChatbotUI() {
|
|||
</button>
|
||||
</div>
|
||||
<!-- 标题栏 (可拖拽移动窗口) -->
|
||||
<div id="chatbot-title-bar" class="chatbot-draggable-header" style="padding:12px 24px;display:flex;align-items:center;gap:8px;border-bottom:1px dashed rgba(0,0,0,0.1);flex-shrink:0;">
|
||||
<div id="chatbot-title-bar" class="chatbot-draggable-header" style="padding:12px 24px;display:flex;justify-content:center;align-items:center;gap:8px;border-bottom:1px dashed rgba(0,0,0,0.1);flex-shrink:0;">
|
||||
<div style="width:32px;height:32px;border-radius:16px;background:linear-gradient(135deg,#3b82f6,#1d4ed8);display:flex;align-items:center;justify-content:center;display:none;">
|
||||
<i class="fa-solid fa-robot" style="font-size: 14px; color: white;"></i>
|
||||
</div>
|
||||
|
|
@ -1234,7 +1203,7 @@ function initChatbotUI() {
|
|||
</div>
|
||||
<!-- 输入区域容器 (Refactored) -->
|
||||
<div id="chatbot-input-container" class="chatbot-input-container">
|
||||
<!-- 浮动高级选项将由JS插入此处 -->
|
||||
<!-- 浮动高级选项将由JS插入到 chatbot-input-wrapper 后面 -->
|
||||
<!-- 已选图片预览区 -->
|
||||
<div id="chatbot-selected-images-preview" class="chatbot-image-preview-area">
|
||||
{/* 图片预览由 ChatbotImageUtils.updateSelectedImagesPreview 更新 */}
|
||||
|
|
@ -1254,10 +1223,10 @@ function initChatbotUI() {
|
|||
<iconify-icon icon="carbon:document-pdf" width="20"></iconify-icon>
|
||||
</button>
|
||||
<!-- 文本输入框 -->
|
||||
<input id="chatbot-input" type="text" placeholder="请输入问题..."
|
||||
<textarea id="chatbot-input" type="text" placeholder="请输入问题..."
|
||||
class="chatbot-input-field"
|
||||
onkeydown="if(event.key==='Enter'){window.handleChatbotSend();}"
|
||||
/>
|
||||
></textarea>
|
||||
<!-- 发送按钮 -->
|
||||
<button id="chatbot-send-btn"
|
||||
class="chatbot-input-btn chatbot-send-btn"
|
||||
|
|
@ -1281,12 +1250,13 @@ function initChatbotUI() {
|
|||
</button>
|
||||
</div>
|
||||
<!-- 免责声明 -->
|
||||
<div class="chatbot-disclaimer">
|
||||
<p style="margin:0;">AI助手可能会犯错。请核实重要信息。</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// <div class="chatbot-disclaimer">
|
||||
// <p style="margin:0;">AI助手可能会犯错。请核实重要信息。</p>
|
||||
// </div>
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
|
|
@ -1296,6 +1266,44 @@ function initChatbotUI() {
|
|||
window.ChatbotFloatingOptionsUI.createBar(inputContainerElement, updateChatbotUI);
|
||||
}
|
||||
|
||||
// 将输入栏的核心按钮移动到浮动选项栏中
|
||||
// 目标容器由 ChatbotFloatingOptionsUI.createBar 创建:#chatbot-floating-options
|
||||
(function movePrimaryButtonsIntoFloatingOptions() {
|
||||
const floatingOptionsContainer = document.getElementById('chatbot-floating-options');
|
||||
if (!floatingOptionsContainer) return;
|
||||
|
||||
const addImageBtn = document.getElementById('chatbot-add-image-btn');
|
||||
const providePdfBtn = document.getElementById('chatbot-provide-pdf-btn');
|
||||
const sendBtn = document.getElementById('chatbot-send-btn');
|
||||
|
||||
// 1) 先把“添加图片/提供PDF”放到浮动选项栏最前面
|
||||
const prefixFragment = document.createDocumentFragment();
|
||||
for (const el of [addImageBtn, providePdfBtn]) {
|
||||
if (!el) continue;
|
||||
if (el.parentElement === floatingOptionsContainer) continue;
|
||||
prefixFragment.appendChild(el);
|
||||
}
|
||||
if (prefixFragment.childNodes.length) {
|
||||
floatingOptionsContainer.insertBefore(prefixFragment, floatingOptionsContainer.firstChild);
|
||||
}
|
||||
|
||||
// 2) 再把“发送”放到 chatbot-option-summarySource 后面
|
||||
if (!sendBtn) return;
|
||||
if (sendBtn.parentElement === floatingOptionsContainer) {
|
||||
// 仍然允许重新定位(appendChild 会自动移动节点)
|
||||
}
|
||||
const summarySourceOptionBtn = document.getElementById('chatbot-option-summarySource');
|
||||
if (summarySourceOptionBtn && summarySourceOptionBtn.parentElement === floatingOptionsContainer) {
|
||||
if (summarySourceOptionBtn.nextSibling) {
|
||||
floatingOptionsContainer.insertBefore(sendBtn, summarySourceOptionBtn.nextSibling);
|
||||
} else {
|
||||
floatingOptionsContainer.appendChild(sendBtn);
|
||||
}
|
||||
} else {
|
||||
floatingOptionsContainer.appendChild(sendBtn);
|
||||
}
|
||||
})();
|
||||
|
||||
// --- 核心控制按钮事件绑定 ---
|
||||
// 浮动模式切换按钮点击事件
|
||||
document.getElementById('chatbot-float-toggle-btn').onclick = function() {
|
||||
|
|
|
|||
|
|
@ -71,10 +71,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const timeStr = `${timeObj.getMonth() + 1}/${timeObj.getDate()} ${String(timeObj.getHours()).padStart(2, '0')}:${String(timeObj.getMinutes()).padStart(2, '0')}`;
|
||||
|
||||
return `
|
||||
<div class="group flex items-center gap-2 px-2 py-1.5 text-[13px] text-[#000f33] hover:bg-[#000f33] hover:text-[#ffffff] transition-colors cursor-pointer rounded-md mx-2 mb-0.5" onclick="showHistoryDetail('${safeId}')" title="${name}\n${timeObj.toLocaleString()}">
|
||||
<div class="group h-[58px] flex flex-col px-[15px] py-[10px] text-[13px] text-[#666666] hover:bg-[#E6E7EB] transition-colors cursor-pointer rounded-md mb-0.5" onclick="showHistoryDetail('${safeId}')" title="${name}\n${timeObj.toLocaleString()}">
|
||||
|
||||
<span class="truncate flex-1">${name}</span>
|
||||
<span class="text-[10px] text-slate-400 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">${timeStr}</span>
|
||||
<span class="text-[12px] text-[#999999] flex-shrink-0 ">${timeStr}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
|
@ -273,14 +273,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (historyClearMode === 'record') {
|
||||
if (pendingDeleteRecordId) {
|
||||
removeFolderAssignmentForRecord(pendingDeleteRecordId);
|
||||
await deleteResultFromDB(pendingDeleteRecordId);
|
||||
// 使用 storageAdapter 删除(支持后端模式)
|
||||
if (window.storageAdapter && typeof window.storageAdapter.deleteResultFromDB === 'function') {
|
||||
await window.storageAdapter.deleteResultFromDB(pendingDeleteRecordId);
|
||||
} else if (typeof deleteResultFromDB === 'function') {
|
||||
await deleteResultFromDB(pendingDeleteRecordId);
|
||||
}
|
||||
await renderHistoryList();
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('历史记录已删除。', 'success');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await clearAllResultsFromDB();
|
||||
// 使用 storageAdapter 清空(支持后端模式)
|
||||
if (window.storageAdapter && typeof window.storageAdapter.clearAllResultsFromDB === 'function') {
|
||||
await window.storageAdapter.clearAllResultsFromDB();
|
||||
} else if (typeof clearAllResultsFromDB === 'function') {
|
||||
await clearAllResultsFromDB();
|
||||
}
|
||||
clearFolderAssignments();
|
||||
historyUIState.activeFolder = 'all';
|
||||
historyUIState.searchQuery = '';
|
||||
|
|
@ -844,18 +854,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
function renderFolderListItem({ id, name, count, system }) {
|
||||
const isActive = historyUIState.activeFolder === id;
|
||||
const baseClasses = 'group flex items-center justify-between px-2 py-1.5 rounded-lg border transition-colors';
|
||||
const activeClasses = isActive ? 'border-blue-200 bg-blue-50 text-blue-600' : 'border-transparent hover:bg-gray-100 text-gray-700';
|
||||
const baseClasses = 'group flex items-center justify-between px-2 py-1.5 rounded-[5px] border transition-colors';
|
||||
const activeClasses = isActive ? ' bg-[#E4E6EA] text-[#000F33]' : 'border-transparent hover:bg-gray-100 text-[#666666]';
|
||||
const title = escapeAttr(name || '未命名');
|
||||
const countBadge = `<span class="ml-2 inline-flex min-w-[1.5rem] justify-center rounded-full bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium text-gray-500">${typeof count === 'number' ? count : 0}</span>`;
|
||||
const selectButton = `
|
||||
<button type="button" class="flex-1 text-left text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-300 focus-visible:ring-offset-1" data-folder-action="select" data-folder-id="${escapeAttr(id)}" title="查看${title}">
|
||||
<button type="button" class="flex items-center justify-between flex-1 text-left text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-300 focus-visible:ring-offset-1" data-folder-action="select" data-folder-id="${escapeAttr(id)}" title="查看${title}">
|
||||
<span>${escapeHtml(name || '未命名')}</span>
|
||||
${countBadge}
|
||||
</button>`;
|
||||
const actionButtons = system ? '' : `
|
||||
<div class="ml-2 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
|
||||
<button type="button" class="rounded p-1 text-gray-400 hover:text-blue-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-300" data-folder-action="rename" data-folder-id="${escapeAttr(id)}" title="重命名">
|
||||
<button type="button" class="rounded p-1 text-gray-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-300" data-folder-action="rename" data-folder-id="${escapeAttr(id)}" title="重命名">
|
||||
<iconify-icon icon="carbon:edit" width="14"></iconify-icon>
|
||||
</button>
|
||||
<button type="button" class="rounded p-1 text-gray-400 hover:text-red-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-300" data-folder-action="delete" data-folder-id="${escapeAttr(id)}" title="删除">
|
||||
|
|
@ -1049,10 +1059,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
preserve: 'preserve',
|
||||
flat: 'flat'
|
||||
};
|
||||
const ICON_BUTTON_CLASS = 'inline-flex items-center justify-center w-9 h-9 rounded-full border border-slate-200 bg-white text-gray-500 hover:text-blue-600 hover:border-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-1';
|
||||
const ICON_BUTTON_CLASS = 'inline-flex items-center justify-center w-9 h-9 text-gray-500 hover:text-[#000F33] hover:border-[#E4E6EA]transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-1';
|
||||
const ICON_BUTTON_DANGER_EXTRA = 'hover:text-red-500 hover:border-red-200 focus:ring-red-300';
|
||||
const ICON_BUTTON_SUCCESS_EXTRA = 'hover:text-emerald-500 hover:border-emerald-200 focus:ring-emerald-300';
|
||||
|
||||
const ICON_BUTTON_SUCCESS_EXTRA = '';
|
||||
// hover:text-emerald-500 hover:border-emerald-200 focus:ring-emerald-300
|
||||
const historyListElement = document.getElementById('historyList');
|
||||
if (historyListElement) {
|
||||
historyListElement.addEventListener('click', handleHistoryListAction);
|
||||
|
|
@ -1212,39 +1222,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const escapedRecordId = escapeAttr(record.id || '');
|
||||
const exportBtnHtml = `
|
||||
<button type="button" class="${ICON_BUTTON_CLASS}" data-history-action="open-record-export" data-record-id="${escapeAttr(record.id)}" data-target="${configId}" aria-label="配置导出" title="配置导出">
|
||||
<iconify-icon icon="carbon:share" width="18"></iconify-icon>
|
||||
导出
|
||||
</button>`;
|
||||
const downloadBtnHtml = `
|
||||
<button type="button" class="${ICON_BUTTON_CLASS} ${ICON_BUTTON_SUCCESS_EXTRA}" onclick="downloadHistoryRecord('${escapedRecordId}')" aria-label="下载记录" title="下载记录">
|
||||
<iconify-icon icon="carbon:download" width="18"></iconify-icon>
|
||||
下载
|
||||
</button>`;
|
||||
const recordDisplayName = record.name || relativePathLabel || record.id || '历史记录';
|
||||
const escapedRecordNameAttr = escapeAttr(recordDisplayName);
|
||||
const deleteBtnHtml = withinBatch ? '' : `
|
||||
<button type="button" class="${ICON_BUTTON_CLASS} ${ICON_BUTTON_DANGER_EXTRA}" data-history-action="delete-record" data-record-id="${escapedRecordId}" data-record-name="${escapedRecordNameAttr}" aria-label="删除记录" title="删除记录">
|
||||
<iconify-icon icon="carbon:trash-can" width="18"></iconify-icon>
|
||||
删除
|
||||
</button>`;
|
||||
const startReadingBtnHtml = `
|
||||
<button type="button" class="inline-flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1" onclick="showHistoryDetail('${escapedRecordId}')">
|
||||
<iconify-icon icon="carbon:document-view" width="18"></iconify-icon>
|
||||
<button type="button" class="inline-flex items-center gap-2 px-5 py-2 bg-[#000F33] text-white rounded-md " onclick="showHistoryDetail('${escapedRecordId}')">
|
||||
<span>开始阅读</span>
|
||||
</button>`;
|
||||
|
||||
const containerClasses = withinBatch
|
||||
? 'border border-slate-200 rounded-xl p-3 bg-white shadow-sm hover:border-blue-200 transition'
|
||||
: 'border border-slate-200 rounded-xl p-4 bg-white shadow-sm hover:border-blue-200 hover:shadow-md transition';
|
||||
? 'rounded-[20px] p-3 bg-[#F8F9FA] hover:border-blue-200 transition'
|
||||
: 'rounded-[20px] p-4 bg-[#F8F9FA] hover:border-blue-200 transition';
|
||||
|
||||
return `
|
||||
<div class="${containerClasses}" id="history-item-${safeId}" data-record-id="${escapeAttr(record.id)}">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between md:gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold text-gray-800 flex flex-wrap items-center gap-2 break-all">
|
||||
<div class="text-sm text-[#333333] flex flex-wrap items-center gap-2 break-all">
|
||||
<span>${escapeHtml(record.name || '未命名')}</span> ${statusBadge}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
${timeLabel}${targetLang ? ` · 语言:${escapeHtml(targetLang)}` : ''}
|
||||
</div>
|
||||
|
||||
${(ocrLabel || (transLabel && transLabel !== '未翻译')) ? `
|
||||
<div class="text-xs text-gray-500">
|
||||
${ocrLabel ? `OCR:${ocrLabel}` : ''}
|
||||
|
|
@ -1259,7 +1266,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
</div>
|
||||
<div class="hidden md:flex flex-wrap gap-2 text-xs text-gray-600 justify-end md:text-sm items-center">
|
||||
${exportBtnHtml}
|
||||
${startReadingBtnHtml}
|
||||
${downloadBtnHtml}
|
||||
${deleteBtnHtml}
|
||||
</div>
|
||||
|
|
@ -1272,7 +1278,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
</summary>
|
||||
<div class="mt-2 flex flex-wrap gap-2 text-xs text-gray-600">
|
||||
${exportBtnHtml}
|
||||
${startReadingBtnHtml}
|
||||
${downloadBtnHtml}
|
||||
${deleteBtnHtml}
|
||||
</div>
|
||||
|
|
@ -1280,10 +1285,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
</div>
|
||||
<div class="text-xs text-gray-600 break-words">OCR:${ocrSnippet}</div>
|
||||
<div class="text-xs text-gray-600 break-words">翻译:${translationSnippet}</div>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-600 mt-2">
|
||||
<div class="flex flex-wrap justify-between items-center text-xs text-gray-600">
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
${timeLabel}${targetLang ? ` · 语言:${escapeHtml(targetLang)}` : ''}
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-between items-center gap-3">
|
||||
<button id="retry-failed-btn-${safeId}" onclick="retryTranslateRecord('${record.id}','failed')" class="px-2 py-1 border border-gray-200 rounded hover:bg-gray-100 ${retryDisabled}">重试失败段</button>
|
||||
<button id="retry-all-btn-${safeId}" onclick="retryTranslateRecord('${record.id}','all')" class="px-2 py-1 border border-gray-200 rounded hover:bg-gray-100">重新翻译全部</button>
|
||||
${startReadingBtnHtml}
|
||||
<span id="retry-status-${safeId}" class="text-xs text-gray-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
${renderExportConfigPanel({
|
||||
id: configId,
|
||||
|
|
@ -1491,22 +1502,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
function buildStatusBadge(status) {
|
||||
if (!status || status.total === 0) {
|
||||
return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-gray-100 text-gray-500">未分块</span>';
|
||||
return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-[#ffffff] text-gray-500">未分块</span>';
|
||||
}
|
||||
// 若没有任何成功块(0/total),改为“预览中,无翻译块”
|
||||
if (status.success === 0) {
|
||||
// 结构化翻译下,将提示文案替换为“PDF对照”
|
||||
if (status.isStructured) {
|
||||
return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-blue-100 text-blue-700">PDF对照</span>';
|
||||
return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-[#ffffff] text-blue-700">PDF对照</span>';
|
||||
}
|
||||
return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-gray-100 text-gray-600">预览中,无翻译块</span>';
|
||||
return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-[#ffffff] text-gray-600">预览中,无翻译块</span>';
|
||||
}
|
||||
// 有成功也有失败 → 部分失败
|
||||
if (status.failed > 0) {
|
||||
return `<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-amber-100 text-amber-700">部分失败 ${status.success}/${status.total}</span>`;
|
||||
return `<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-[#ffffff] text-amber-700">部分失败 ${status.success}/${status.total}</span>`;
|
||||
}
|
||||
// 全部成功
|
||||
return `<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-green-100 text-green-700">完成 ${status.success}/${status.total}</span>`;
|
||||
return `<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-[#ffffff] text-green-700">完成 ${status.success}/${status.total}</span>`;
|
||||
}
|
||||
|
||||
function buildSnippetText(text) {
|
||||
|
|
@ -2194,7 +2205,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const records = batchCache[batchId];
|
||||
if (!records || !records.length) return;
|
||||
for (const record of records) {
|
||||
await deleteResultFromDB(record.id);
|
||||
// 使用 storageAdapter 删除(支持后端模式)
|
||||
if (window.storageAdapter && typeof window.storageAdapter.deleteResultFromDB === 'function') {
|
||||
await window.storageAdapter.deleteResultFromDB(record.id);
|
||||
} else if (typeof deleteResultFromDB === 'function') {
|
||||
await deleteResultFromDB(record.id);
|
||||
}
|
||||
removeFolderAssignmentForRecord(record.id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,157 @@
|
|||
/**
|
||||
* 检测文本是否主要为中文
|
||||
* 通过统计中文字符占比来判断
|
||||
* @param {string} text - 待检测的文本
|
||||
* @returns {boolean} - 如果中文字符占比超过 30% 则认为是中文文档
|
||||
*/
|
||||
function isChineseDocument(text) {
|
||||
if (!text || typeof text !== 'string') return false;
|
||||
|
||||
// 移除空白字符后进行检测
|
||||
const cleanText = text.replace(/\s+/g, '');
|
||||
if (cleanText.length === 0) return false;
|
||||
|
||||
// 匹配中文字符(包括简体和繁体)
|
||||
const chineseCharRegex = /[\u4e00-\u9fff]/g;
|
||||
const chineseChars = cleanText.match(chineseCharRegex);
|
||||
|
||||
if (!chineseChars) return false;
|
||||
|
||||
// 计算中文字符占比
|
||||
const ratio = chineseChars.length / cleanText.length;
|
||||
|
||||
// 如果中文字符占比超过 30%,认为是中文文档
|
||||
return ratio > 0.3;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 PDF.js 从 PDF base64 数据中提取文本
|
||||
* @param {string} base64Data - PDF 文件的 base64 编码数据
|
||||
* @param {number} maxPages - 最大提取页数(默认提取前3页用于检测)
|
||||
* @returns {Promise<string>} 提取的文本内容
|
||||
*/
|
||||
async function extractTextFromPdfBase64(base64Data, maxPages = 3) {
|
||||
if (!base64Data || typeof pdfjsLib === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
// 将 base64 转换为 Uint8Array
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// 使用 PDF.js 加载 PDF
|
||||
const loadingTask = pdfjsLib.getDocument({ data: bytes });
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
|
||||
// 提取前几页的文本
|
||||
const totalPages = Math.min(pdfDocument.numPages, maxPages);
|
||||
let extractedText = '';
|
||||
|
||||
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
||||
const page = await pdfDocument.getPage(pageNum);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items.map(item => item.str).join(' ');
|
||||
extractedText += pageText + ' ';
|
||||
}
|
||||
|
||||
return extractedText;
|
||||
} catch (error) {
|
||||
console.error('[extractTextFromPdfBase64] 提取PDF文本失败:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文档语言隐藏不需要的 Tab
|
||||
* 中文文档隐藏:PDF对照、仅翻译、分块对比
|
||||
* @param {Object} data - 文档数据对象
|
||||
* @returns {Promise<boolean>} - 是否为中文文档
|
||||
*/
|
||||
async function hideTabsForChineseDocument(data) {
|
||||
// 获取 OCR 文本用于判断语言
|
||||
// 优先级:contentListJson > ocr > ocrChunks > originalBinary(PDF提取)
|
||||
let textToDetect = '';
|
||||
const meta = data?.metadata?.metadata || data?.metadata || {};
|
||||
|
||||
// 1. 优先从 contentListJson 提取文本(MinerU 结构化数据)
|
||||
if (meta.contentListJson && Array.isArray(meta.contentListJson)) {
|
||||
const textItems = meta.contentListJson
|
||||
.filter(item => item.type === 'text' && item.text)
|
||||
.map(item => item.text);
|
||||
textToDetect = textItems.join(' ');
|
||||
console.log('[hideTabsForChineseDocument] 从 contentListJson 提取文本,共', textItems.length, '个文本块');
|
||||
}
|
||||
|
||||
// 2. 如果没有 contentListJson,尝试 ocr 字段
|
||||
if (!textToDetect && data.ocr && typeof data.ocr === 'string') {
|
||||
textToDetect = data.ocr;
|
||||
console.log('[hideTabsForChineseDocument] 使用 data.ocr 字段');
|
||||
}
|
||||
|
||||
// 3. 尝试 metadata.ocr
|
||||
if (!textToDetect && meta.ocr && typeof meta.ocr === 'string') {
|
||||
textToDetect = meta.ocr;
|
||||
console.log('[hideTabsForChineseDocument] 使用 metadata.ocr 字段');
|
||||
}
|
||||
|
||||
// 4. 尝试 ocrChunks
|
||||
if (!textToDetect && data.ocrChunks && Array.isArray(data.ocrChunks) && data.ocrChunks.length > 0) {
|
||||
textToDetect = data.ocrChunks.map(chunk =>
|
||||
typeof chunk === 'string' ? chunk : (chunk.text || chunk.content || '')
|
||||
).join(' ');
|
||||
console.log('[hideTabsForChineseDocument] 从 data.ocrChunks 提取文本');
|
||||
}
|
||||
|
||||
// 5. 尝试 metadata.ocrChunks
|
||||
if (!textToDetect && meta.ocrChunks && Array.isArray(meta.ocrChunks) && meta.ocrChunks.length > 0) {
|
||||
textToDetect = meta.ocrChunks.map(chunk =>
|
||||
typeof chunk === 'string' ? chunk : (chunk.text || chunk.content || '')
|
||||
).join(' ');
|
||||
console.log('[hideTabsForChineseDocument] 从 metadata.ocrChunks 提取文本');
|
||||
}
|
||||
|
||||
// 6. 如果以上都没有,尝试从原始PDF中提取文本
|
||||
if (!textToDetect && meta.originalPdfBase64) {
|
||||
console.log('[hideTabsForChineseDocument] 尝试从原始PDF提取文本进行语言检测...');
|
||||
textToDetect = await extractTextFromPdfBase64(meta.originalPdfBase64, 3);
|
||||
}
|
||||
|
||||
// 检测是否为中文文档
|
||||
const isChinese = isChineseDocument(textToDetect);
|
||||
|
||||
console.log('[hideTabsForChineseDocument] 文档语言检测结果:', {
|
||||
isChinese,
|
||||
textLength: textToDetect.length,
|
||||
textSample: textToDetect.substring(0, 100) + '...'
|
||||
});
|
||||
|
||||
// 需要隐藏的 Tab ID 列表
|
||||
const tabsToHide = [
|
||||
'tab-pdf-compare', // PDF对照
|
||||
'tab-translation', // 仅翻译
|
||||
'tab-chunk-compare' // 分块对比
|
||||
];
|
||||
|
||||
tabsToHide.forEach(tabId => {
|
||||
const tabElement = document.getElementById(tabId);
|
||||
if (tabElement) {
|
||||
if (isChinese) {
|
||||
tabElement.classList.add('hidden-tab');
|
||||
console.log(`[hideTabsForChineseDocument] 隐藏 Tab: ${tabId}`);
|
||||
} else {
|
||||
tabElement.classList.remove('hidden-tab');
|
||||
console.log(`[hideTabsForChineseDocument] 显示 Tab: ${tabId}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return isChinese;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步渲染历史详情页面的主函数。
|
||||
* - 从 URL 查询参数中获取记录 ID。
|
||||
|
|
@ -64,6 +218,25 @@ async function renderDetail() {
|
|||
// 兼容嵌套的 metadata 结构
|
||||
const meta = data?.metadata?.metadata || data?.metadata || {};
|
||||
|
||||
// 调试:打印后端数据结构
|
||||
console.log('[renderDetail] 后端数据结构:', {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
fileType: data.fileType,
|
||||
hasMetadata: !!data.metadata,
|
||||
metadataType: typeof data.metadata,
|
||||
metadataKeys: data.metadata ? Object.keys(data.metadata) : [],
|
||||
hasNestedMetadata: !!(data.metadata?.metadata),
|
||||
nestedMetadataKeys: data.metadata?.metadata ? Object.keys(data.metadata.metadata) : [],
|
||||
metaKeys: Object.keys(meta),
|
||||
hasOriginalPdfBase64: !!meta.originalPdfBase64,
|
||||
hasOcrChunks: !!(meta.ocrChunks?.length),
|
||||
hasTranslatedChunks: !!(meta.translatedChunks?.length),
|
||||
hasImages: !!(meta.images?.length),
|
||||
hasContentListJson: !!meta.contentListJson,
|
||||
hasTranslatedContentList: !!meta.translatedContentList
|
||||
});
|
||||
|
||||
const hasMinerUStructuredData =
|
||||
meta.originalPdfBase64 &&
|
||||
meta.contentListJson &&
|
||||
|
|
@ -72,6 +245,36 @@ async function renderDetail() {
|
|||
|
||||
const hasOriginalPdf = !!(meta.originalPdfBase64);
|
||||
|
||||
// ========== 将 PDF base64 转换为 File 对象,供 AI 助手的 PDF 功能使用 ==========
|
||||
if (hasOriginalPdf && data.name) {
|
||||
try {
|
||||
// 使用 history_detail_scripts.js 中定义的 base64ToFile 函数
|
||||
if (typeof base64ToFile === 'function') {
|
||||
window.currentOriginalFile = base64ToFile(meta.originalPdfBase64, data.name);
|
||||
console.log('[renderDetail] 已设置 window.currentOriginalFile,供 AI 助手使用');
|
||||
} else {
|
||||
// 如果 base64ToFile 不可用,手动转换
|
||||
const base64Data = meta.originalPdfBase64;
|
||||
const byteString = atob(base64Data);
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
window.currentOriginalFile = new File([ab], data.name, { type: 'application/pdf' });
|
||||
console.log('[renderDetail] 已设置 window.currentOriginalFile(手动转换),供 AI 助手使用');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[renderDetail] 转换 PDF base64 为 File 失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[renderDetail] 检测结果:', {
|
||||
hasMinerUStructuredData,
|
||||
hasOriginalPdf,
|
||||
defaultTab: hasOriginalPdf ? 'original-file' : 'ocr'
|
||||
});
|
||||
|
||||
document.getElementById('fileName').textContent = data.name;
|
||||
if (fileMetaTimeEl) {
|
||||
fileMetaTimeEl.textContent = `时间: ${new Date(data.time).toLocaleString()}`;
|
||||
|
|
@ -128,6 +331,10 @@ async function renderDetail() {
|
|||
console.error("initAnnotationSystem is not defined. Check js/annotation_logic.js");
|
||||
}
|
||||
|
||||
// ========== 检测文档语言并隐藏不必要的 Tab ==========
|
||||
const isChinese = await hideTabsForChineseDocument(data);
|
||||
// =============================================
|
||||
|
||||
// Determine initial tab, AFTER annotations are loaded
|
||||
let initialTab = hasOriginalPdf ? 'original-file' : 'ocr'; // 有原始文件时默认显示原始文件
|
||||
if (docIdForLocalStorage) {
|
||||
|
|
@ -139,6 +346,11 @@ async function renderDetail() {
|
|||
!(savedTab !== 'ocr' && (!data.translation || data.translation.trim() === ""))
|
||||
) {
|
||||
initialTab = savedTab;
|
||||
// 如果是中文文档,且保存的 Tab 是被隐藏的,则切换到默认 Tab
|
||||
if (isChinese && ['translation', 'chunk-compare', 'pdf-compare'].includes(savedTab)) {
|
||||
console.log(`[renderDetail] 中文文档,切换保存的 Tab 从 ${savedTab} 到 ocr`);
|
||||
initialTab = hasOriginalPdf ? 'original-file' : 'ocr';
|
||||
}
|
||||
} else if (hasOriginalPdf) {
|
||||
// 如果有原始文件,优先显示原始文件
|
||||
initialTab = 'original-file';
|
||||
|
|
@ -149,6 +361,10 @@ async function renderDetail() {
|
|||
data.translation && data.translation.trim() !== ""
|
||||
) {
|
||||
initialTab = 'chunk-compare';
|
||||
// 如果是中文文档,不显示分块对比
|
||||
if (isChinese) {
|
||||
initialTab = 'ocr';
|
||||
}
|
||||
}
|
||||
} else if (hasOriginalPdf) {
|
||||
// 如果有原始文件,优先显示原始文件
|
||||
|
|
@ -160,6 +376,10 @@ async function renderDetail() {
|
|||
data.translation && data.translation.trim() !== ""
|
||||
) {
|
||||
initialTab = 'chunk-compare';
|
||||
// 如果是中文文档,不显示分块对比
|
||||
if (isChinese) {
|
||||
initialTab = 'ocr';
|
||||
}
|
||||
}
|
||||
|
||||
// 现在,在批注肯定加载完毕后,才调用 showTab
|
||||
|
|
|
|||
|
|
@ -743,7 +743,8 @@ async function triggerReprocessWithMinerU() {
|
|||
settings.defaultUserPromptTemplate || '',
|
||||
settings.useCustomPrompts || false,
|
||||
null, // batchContext
|
||||
() => {} // onFileSuccess
|
||||
() => {}, // onFileSuccess
|
||||
docId // existingDocId - 更新现有文档而非创建新记录
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
|
|
@ -765,12 +766,7 @@ async function triggerReprocessWithMinerU() {
|
|||
Object.assign(window.data.metadata, result.metadata);
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if (window.storageAdapter && typeof window.storageAdapter.saveResultToDB === 'function') {
|
||||
await window.storageAdapter.saveResultToDB(window.data);
|
||||
} else if (typeof saveResultToDB === 'function') {
|
||||
await saveResultToDB(window.data);
|
||||
}
|
||||
// 注意:processSinglePdf 已经保存到数据库,无需重复保存
|
||||
|
||||
showToast('处理完成!正在加载 PDF 对照视图...', 'success');
|
||||
|
||||
|
|
@ -951,6 +947,18 @@ async function executeMinerUStructuredTranslation() {
|
|||
|
||||
addLog('翻译完成,保存数据...');
|
||||
|
||||
// 从 translatedContentList 生成合并的翻译文本
|
||||
const translatedTextParts = [];
|
||||
translatedContentList.forEach((item) => {
|
||||
if (item && item.text && item.type === 'text') {
|
||||
translatedTextParts.push(item.text);
|
||||
}
|
||||
});
|
||||
const combinedTranslation = translatedTextParts.join('\n\n');
|
||||
|
||||
// 更新 dataObj 的翻译字段
|
||||
dataObj.translation = combinedTranslation;
|
||||
|
||||
// 保存到 metadata
|
||||
if (!dataObj.metadata) dataObj.metadata = {};
|
||||
dataObj.metadata.translatedContentList = translatedContentList;
|
||||
|
|
@ -971,10 +979,8 @@ async function executeMinerUStructuredTranslation() {
|
|||
dataObj.metadata.failedStructuredItems = failedItems;
|
||||
dataObj.metadata.structuredFailedCount = failedItems.length;
|
||||
|
||||
// 更新全局数据
|
||||
if (typeof data !== 'undefined') {
|
||||
data.metadata = dataObj.metadata;
|
||||
}
|
||||
// 更新全局数据(dataObj 是 window.data 的引用,修改即生效)
|
||||
// 注意:不再尝试更新可能为 null 的全局 data 变量
|
||||
window.data = dataObj;
|
||||
|
||||
// 保存到数据库
|
||||
|
|
|
|||
|
|
@ -112,7 +112,8 @@ async function processSinglePdf(
|
|||
defaultUserPromptTemplateSetting,
|
||||
useCustomPromptsSetting, // 新增参数
|
||||
batchContext,
|
||||
onFileSuccess
|
||||
onFileSuccess,
|
||||
existingDocId // 新增:现有文档ID,用于更新而非创建新记录
|
||||
) {
|
||||
let currentMarkdownContent = '';
|
||||
let currentTranslationContent = '';
|
||||
|
|
@ -823,8 +824,14 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
|
|||
);
|
||||
|
||||
// 5. 保存结果
|
||||
// 结构化翻译完成后:不生成常规译文,以免展示译文/分块对比标签
|
||||
currentTranslationContent = '';
|
||||
// 从 translatedContentList 生成合并的翻译文本用于数据库保存
|
||||
const translatedTextParts = [];
|
||||
(translatedContentList || []).forEach((item) => {
|
||||
if (item && item.text && item.type === 'text') {
|
||||
translatedTextParts.push(item.text);
|
||||
}
|
||||
});
|
||||
currentTranslationContent = translatedTextParts.join('\n\n');
|
||||
|
||||
// 将翻译后的 JSON 保存在元数据中供未来使用
|
||||
if (!ocrResult.metadata.translatedContentList) {
|
||||
|
|
@ -1090,8 +1097,10 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
|
|||
}
|
||||
|
||||
// 保存文档记录
|
||||
// 如果提供了 existingDocId,则更新现有文档(UUID格式);否则创建新记录
|
||||
const docId = existingDocId || `${fileToProcess.name}_${fileToProcess.size}`;
|
||||
const documentData = {
|
||||
id: `${fileToProcess.name}_${fileToProcess.size}`,
|
||||
id: docId,
|
||||
name: fileToProcess.name,
|
||||
size: fileToProcess.size,
|
||||
time: processedAt,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ let DEPLOYMENT_MODE = (getQueryModeOverride() || (window.ENV_DEPLOYMENT_MODE &&
|
|||
|
||||
const API_BASE_URL = window.ENV_API_BASE_URL || '/api';
|
||||
|
||||
// 后端认证状态(当 AUTH_DISABLED=true 时,前端也需要绕过认证检查)
|
||||
let backendAuthDisabled = false;
|
||||
|
||||
async function autoDetectBackendAvailability(timeoutMs = 900) {
|
||||
// file:// 明确无后端
|
||||
if (window.location.protocol === 'file:') return false;
|
||||
|
|
@ -37,7 +40,20 @@ async function autoDetectBackendAvailability(timeoutMs = 900) {
|
|||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const res = await fetch(`${API_BASE_URL}/health`, { signal: controller.signal, cache: 'no-store' });
|
||||
clearTimeout(id);
|
||||
return res.ok;
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// 如果后端禁用认证,设置假 token 并标记状态
|
||||
// TODO: 生产环境必须移除此逻辑,强制要求用户登录认证
|
||||
if (data.authDisabled) {
|
||||
backendAuthDisabled = true;
|
||||
// 设置假 token 让 AuthManager.isAuthenticated() 返回 true
|
||||
if (!localStorage.getItem('auth_token')) {
|
||||
localStorage.setItem('auth_token', 'dev-mode-no-auth');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -253,6 +269,36 @@ class BackendStorage {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 OSS URL 到文档(供 AI 助手使用)
|
||||
* @param {string} id - 文档 ID
|
||||
* @param {string} ossUrl - OSS 公网访问 URL
|
||||
* @param {string} ossKey - OSS 对象 key(用于删除)
|
||||
*/
|
||||
async saveOssUrl(id, ossUrl, ossKey) {
|
||||
try {
|
||||
if (!AuthManager.isAuthenticated()) {
|
||||
// 前端模式:保存到 IndexedDB metadata
|
||||
const doc = await this._fallbackTo('getResultFromDB', id);
|
||||
if (doc) {
|
||||
doc.metadata = doc.metadata || {};
|
||||
doc.metadata.ossUrl = ossUrl;
|
||||
doc.metadata.ossKey = ossKey;
|
||||
await this._fallbackTo('saveResultToDB', doc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await this.fetchAPI(`/documents/${id}/oss`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ossUrl, ossKey })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save OSS URL to document:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async clearAllResultsFromDB() {
|
||||
try {
|
||||
if (!AuthManager.isAuthenticated()) {
|
||||
|
|
@ -477,6 +523,20 @@ class StorageAdapterFactory {
|
|||
getResultFromDB: window.getResultFromDB,
|
||||
deleteResultFromDB: window.deleteResultFromDB,
|
||||
clearAllResultsFromDB: window.clearAllResultsFromDB,
|
||||
// OSS URL 保存(前端模式:保存到 IndexedDB metadata)
|
||||
saveOssUrl: async function(id, ossUrl, ossKey) {
|
||||
try {
|
||||
const doc = await window.getResultFromDB(id);
|
||||
if (doc) {
|
||||
doc.metadata = doc.metadata || {};
|
||||
doc.metadata.ossUrl = ossUrl;
|
||||
doc.metadata.ossKey = ossKey;
|
||||
await window.saveResultToDB(doc);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[StorageAdapter] saveOssUrl failed in frontend mode:', e);
|
||||
}
|
||||
},
|
||||
loadGlossarySets: window.loadGlossarySets,
|
||||
saveGlossarySets: window.saveGlossarySets,
|
||||
saveAnnotationToDB: window.saveAnnotationToDB,
|
||||
|
|
|
|||
|
|
@ -224,12 +224,12 @@
|
|||
// 检查是否为移动端设备
|
||||
if (window.innerWidth <= 700) {
|
||||
console.warn('拒绝在移动端(≤700px)进入沉浸式布局');
|
||||
// 即使不进入沉浸模式,也需要显示页面
|
||||
// 移动端:显示普通布局
|
||||
document.documentElement.classList.remove('immersive-pending');
|
||||
document.documentElement.classList.add('immersive-ready');
|
||||
document.body.classList.remove('immersive-pending');
|
||||
document.body.classList.add('immersive-ready');
|
||||
return;
|
||||
return true; // 移动端算作成功
|
||||
}
|
||||
|
||||
reQueryDynamicElements();
|
||||
|
|
@ -256,12 +256,12 @@
|
|||
immersiveChatbotArea: !!immersiveChatbotArea
|
||||
});
|
||||
|
||||
// 【严格检查】如果元素缺失,返回 false 表示失败,不显示页面
|
||||
// 调用者应该重试直到成功
|
||||
if (missingElements.length > 0) {
|
||||
console.warn('Immersive mode elements not found:', missingElements.join(', '));
|
||||
// 元素缺失时也需要显示页面
|
||||
document.body.classList.remove('immersive-pending');
|
||||
document.body.classList.add('immersive-ready');
|
||||
return;
|
||||
console.error('[enterImmersiveMode] 严重错误:沉浸模式元素缺失:', missingElements.join(', '));
|
||||
console.error('[enterImmersiveMode] 拒绝显示普通布局,等待重试...');
|
||||
return false; // 返回失败,不显示页面
|
||||
}
|
||||
|
||||
isImmersiveActive = true;
|
||||
|
|
@ -430,128 +430,13 @@
|
|||
initializeTocDockResizer();
|
||||
localStorage.setItem(LS_IMMERSIVE_KEY, 'true');
|
||||
document.dispatchEvent(new CustomEvent('immersiveModeEntered'));
|
||||
|
||||
return true; // 成功进入沉浸模式
|
||||
}
|
||||
// 禁用退出沉浸模式功能 - 页面始终保持在沉浸式布局
|
||||
function exitImmersiveMode() {
|
||||
reQueryDynamicElements();
|
||||
isImmersiveActive = false;
|
||||
|
||||
// 添加退出动画
|
||||
document.body.classList.add('immersive-exiting');
|
||||
if (immersiveContainer) {
|
||||
immersiveContainer.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
||||
immersiveContainer.style.opacity = '0';
|
||||
immersiveContainer.style.transform = 'scale(0.98)';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
destroyTocDockResizer();
|
||||
|
||||
// Remove the TOC vs Dock resize handle
|
||||
if (tocVsDockResizeHandle && tocVsDockResizeHandle.parentNode === immersiveTocArea) {
|
||||
immersiveTocArea.removeChild(tocVsDockResizeHandle);
|
||||
}
|
||||
|
||||
// 恢复元素位置的逻辑保持不变...
|
||||
if (originalTocPopupParent && tocPopupElement && tocPopupElement.parentNode === immersiveTocArea) {
|
||||
originalTocPopupParent.insertBefore(tocPopupElement, originalTocPopupNextSibling);
|
||||
} else if (tocPopupElement && immersiveTocArea.contains(tocPopupElement)) {
|
||||
immersiveTocArea.removeChild(tocPopupElement);
|
||||
if (originalTocPopupParent) {
|
||||
originalTocPopupParent.insertBefore(tocPopupElement, originalTocPopupNextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
if (originalMainContainerParent && mainPageContainer && mainPageContainer.parentNode === immersiveMainArea) {
|
||||
originalMainContainerParent.insertBefore(mainPageContainer, originalMainContainerNextSibling);
|
||||
} else if (mainPageContainer && immersiveMainArea.contains(mainPageContainer)) {
|
||||
immersiveMainArea.removeChild(mainPageContainer);
|
||||
if (originalMainContainerParent) {
|
||||
originalMainContainerParent.insertBefore(mainPageContainer, originalMainContainerNextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
if (chatbotModalElement && originalChatbotModalParent && chatbotModalElement.parentNode === immersiveChatbotArea) {
|
||||
originalChatbotModalParent.insertBefore(chatbotModalElement, originalChatbotModalNextSibling);
|
||||
} else if (chatbotModalElement && immersiveChatbotArea.contains(chatbotModalElement)) {
|
||||
immersiveChatbotArea.removeChild(chatbotModalElement);
|
||||
if (originalChatbotModalParent) {
|
||||
originalChatbotModalParent.insertBefore(chatbotModalElement, originalChatbotModalNextSibling);
|
||||
}
|
||||
} else if (!chatbotModalElement && originalChatbotModalParent) {
|
||||
immersiveChatbotArea.innerHTML = '';
|
||||
}
|
||||
|
||||
if (dockElement && originalDockElementParent) {
|
||||
if (immersiveDockPlaceholderElement && immersiveDockPlaceholderElement.contains(dockElement)) {
|
||||
immersiveDockPlaceholderElement.removeChild(dockElement);
|
||||
}
|
||||
originalDockElementParent.insertBefore(dockElement, originalDockElementNextSibling);
|
||||
} else if (dockElement && immersiveDockPlaceholderElement && immersiveDockPlaceholderElement.contains(dockElement)){
|
||||
immersiveDockPlaceholderElement.removeChild(dockElement);
|
||||
}
|
||||
|
||||
document.body.classList.remove('immersive-active', 'no-scroll', 'immersive-exiting');
|
||||
if (immersiveContainer) {
|
||||
immersiveContainer.style.display = 'none';
|
||||
immersiveContainer.style.transition = '';
|
||||
immersiveContainer.style.opacity = '';
|
||||
immersiveContainer.style.transform = '';
|
||||
}
|
||||
|
||||
// 清理滚动容器标记 class
|
||||
if (mainPageContainer) {
|
||||
const tabContent = mainPageContainer.querySelector('.tab-content');
|
||||
if (tabContent) {
|
||||
tabContent.classList.remove('js-scroll-container');
|
||||
console.log("[ImmersiveLayout] 已移除 tab-content 的 js-scroll-container 标记");
|
||||
}
|
||||
}
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '<i class="fas fa-expand-alt"></i>';
|
||||
toggleBtn.classList.remove('immersive-exit-btn-active');
|
||||
toggleBtn.title = '进入沉浸式布局';
|
||||
}
|
||||
|
||||
// 其他退出逻辑保持不变...
|
||||
if (typeof window.refreshTocList === 'function') {
|
||||
window.refreshTocList();
|
||||
}
|
||||
|
||||
if (typeof window.docIdForLocalStorage !== 'undefined' && window.docIdForLocalStorage) {
|
||||
const savedChatbotOpenState = localStorage.getItem(`chatbotOpenState_${window.docIdForLocalStorage}`);
|
||||
if (savedChatbotOpenState === 'true') {
|
||||
window.isChatbotOpen = true;
|
||||
} else if (savedChatbotOpenState === 'false') {
|
||||
window.isChatbotOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (window.ChatbotUI && typeof window.ChatbotUI.updateChatbotUI === 'function') {
|
||||
window.ChatbotUI.updateChatbotUI();
|
||||
}
|
||||
|
||||
if (window.DockLogic && typeof window.DockLogic.updateStats === 'function' && window.data && window.currentVisibleTabId) {
|
||||
dockElement.style.display = '';
|
||||
window.DockLogic.updateStats(window.data, window.currentVisibleTabId);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.DockLogic) {
|
||||
if (typeof window.DockLogic.unbindScrollForCurrentScrollable === 'function') {
|
||||
console.log("[ImmersiveLayout] 退出沉浸模式前,先解绑旧的滚动事件");
|
||||
window.DockLogic.unbindScrollForCurrentScrollable();
|
||||
}
|
||||
if (typeof window.DockLogic.forceUpdateReadingProgress === 'function') {
|
||||
console.log("[ImmersiveLayout] 退出沉浸模式后,延迟调用 forceUpdateReadingProgress");
|
||||
window.DockLogic.forceUpdateReadingProgress();
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
|
||||
localStorage.setItem(LS_IMMERSIVE_KEY, 'false');
|
||||
document.dispatchEvent(new CustomEvent('immersiveModeExited'));
|
||||
}, 300);
|
||||
// 已禁用:页面始终保持在沉浸式布局
|
||||
console.log('[ImmersiveLayout] 退出沉浸模式功能已禁用');
|
||||
}
|
||||
|
||||
function initResizeHandles() {
|
||||
|
|
@ -630,235 +515,50 @@
|
|||
|
||||
function mainInit() {
|
||||
if (!initializeDomElements()) {
|
||||
console.warn('Immersive layout core static elements not found on DOMContentLoaded. Retrying shortly...');
|
||||
setTimeout(mainInit, 500);
|
||||
console.warn('[ImmersiveLayout] 核心静态元素未找到,200ms 后重试...');
|
||||
setTimeout(mainInit, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
reQueryDynamicElements();
|
||||
|
||||
// 标准沉浸模式切换按钮
|
||||
const standardToggleBtn = document.getElementById('toggle-immersive-btn');
|
||||
// 简单沉浸模式切换按钮
|
||||
const simpleToggleBtn = document.getElementById('toggle-simple-immersive-btn');
|
||||
|
||||
// 标准沉浸模式功能
|
||||
if (standardToggleBtn) {
|
||||
standardToggleBtn.addEventListener('click', () => {
|
||||
// 检查是否为移动端设备(屏幕宽度小于等于700px)
|
||||
if (window.innerWidth <= 700) {
|
||||
console.warn('沉浸式布局在移动端(≤700px)不可用');
|
||||
// 可选:显示提示消息
|
||||
if (window.showToast) {
|
||||
window.showToast('沉浸式布局在手机端不可用', 'warning');
|
||||
} else {
|
||||
alert('沉浸式布局在手机端不可用,请在更大的屏幕上使用');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isImmersiveActive) {
|
||||
exitImmersiveMode();
|
||||
} else {
|
||||
enterImmersiveMode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 简单沉浸模式功能
|
||||
if (simpleToggleBtn) {
|
||||
simpleToggleBtn.addEventListener('click', () => {
|
||||
isSimpleImmersiveActive = !isSimpleImmersiveActive;
|
||||
|
||||
// 检查是否在"原始文件"标签页(PDF查看器)
|
||||
const isOriginalFileTab = window.currentVisibleTabId === 'original-file';
|
||||
const pdfIframeWrapper = document.getElementById('pdf-iframe-wrapper');
|
||||
const pdfIframe = document.getElementById('pdf-viewer-iframe');
|
||||
const mainContainer = document.querySelector('.container');
|
||||
|
||||
if (isSimpleImmersiveActive) {
|
||||
// 如果是PDF查看器标签页,进行特殊处理
|
||||
if (isOriginalFileTab && pdfIframeWrapper && mainContainer) {
|
||||
// 添加特殊类用于PDF全屏模式
|
||||
document.body.classList.add('simple-immersive-pdf-mode');
|
||||
|
||||
// 移除container的padding
|
||||
mainContainer.dataset.originalPadding = mainContainer.style.padding || '';
|
||||
mainContainer.style.padding = '0 !important';
|
||||
|
||||
// 让iframe wrapper充满container(接近全屏)
|
||||
pdfIframeWrapper.style.cssText = `
|
||||
width: 100%;
|
||||
height: calc(100vh - 20px);
|
||||
min-height: calc(100vh - 20px);
|
||||
position: relative;
|
||||
background: #525659;
|
||||
`;
|
||||
|
||||
if (pdfIframe) {
|
||||
pdfIframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
`;
|
||||
}
|
||||
|
||||
// 隐藏其他UI元素
|
||||
const elementsToHide = [
|
||||
{ selector: '.tabs-container', el: document.querySelector('.tabs-container') },
|
||||
{ selector: '.history-export-controls', el: document.querySelector('.history-export-controls') },
|
||||
{ selector: '#bottom-left-dock', el: document.getElementById('bottom-left-dock') },
|
||||
{ selector: '#toc-popup', el: document.getElementById('toc-popup') },
|
||||
{ selector: '#fileName', el: document.getElementById('fileName') },
|
||||
{ selector: '#fileMeta', el: document.getElementById('fileMeta') }
|
||||
];
|
||||
|
||||
elementsToHide.forEach(item => {
|
||||
if (item.el) {
|
||||
item.el.dataset.originalDisplay = item.el.style.display || '';
|
||||
item.el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// 隐藏loading指示器
|
||||
const loading = document.getElementById('pdf-viewer-loading');
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
// 确保iframe显示
|
||||
if (pdfIframe) pdfIframe.style.display = 'block';
|
||||
} else {
|
||||
// 非PDF查看器标签页,使用原有的简单沉浸模式
|
||||
document.body.classList.add('simple-immersive-pdf-mode');
|
||||
}
|
||||
|
||||
simpleToggleBtn.innerHTML = '<i class="fas fa-eye-slash"></i>';
|
||||
simpleToggleBtn.title = '退出简单沉浸模式';
|
||||
localStorage.setItem(LS_SIMPLE_IMMERSIVE_KEY, 'true');
|
||||
|
||||
// 自动进入全屏
|
||||
document.documentElement.requestFullscreen().catch(err => {
|
||||
console.warn('无法进入全屏模式:', err);
|
||||
});
|
||||
} else {
|
||||
// 退出简单沉浸模式
|
||||
document.body.classList.remove('simple-immersive-pdf-mode');
|
||||
document.body.classList.remove('simple-immersive-pdf-mode');
|
||||
|
||||
// 如果在全屏状态,退出全屏
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
|
||||
// 恢复container的padding
|
||||
if (mainContainer && mainContainer.dataset.originalPadding !== undefined) {
|
||||
mainContainer.style.padding = mainContainer.dataset.originalPadding;
|
||||
delete mainContainer.dataset.originalPadding;
|
||||
}
|
||||
|
||||
// 恢复所有被隐藏的元素
|
||||
const elementsToRestore = [
|
||||
{ selector: '.tabs-container', el: document.querySelector('.tabs-container') },
|
||||
{ selector: '.history-export-controls', el: document.querySelector('.history-export-controls') },
|
||||
{ selector: '#bottom-left-dock', el: document.getElementById('bottom-left-dock') },
|
||||
{ selector: '#toc-popup', el: document.getElementById('toc-popup') },
|
||||
{ selector: '#fileName', el: document.getElementById('fileName') },
|
||||
{ selector: '#fileMeta', el: document.getElementById('fileMeta') }
|
||||
];
|
||||
|
||||
elementsToRestore.forEach(item => {
|
||||
if (item.el && item.el.dataset.originalDisplay !== undefined) {
|
||||
item.el.style.display = item.el.dataset.originalDisplay;
|
||||
delete item.el.dataset.originalDisplay;
|
||||
}
|
||||
});
|
||||
|
||||
// 恢复iframe wrapper的原始样式
|
||||
if (pdfIframeWrapper) {
|
||||
pdfIframeWrapper.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
position: relative;
|
||||
background: #525659;
|
||||
`;
|
||||
}
|
||||
|
||||
simpleToggleBtn.innerHTML = '<i class="fas fa-eye"></i>';
|
||||
simpleToggleBtn.title = '进入简单沉浸模式';
|
||||
localStorage.setItem(LS_SIMPLE_IMMERSIVE_KEY, 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
// 按钮已隐藏,退出功能已禁用,页面始终处于沉浸模式
|
||||
// 不再需要按钮事件监听器
|
||||
|
||||
initResizeHandles();
|
||||
|
||||
// 监听窗口大小变化,如果变成移动端尺寸则自动退出沉浸模式
|
||||
function handleWindowResize() {
|
||||
if (window.innerWidth <= 700 && isImmersiveActive) {
|
||||
console.log('检测到屏幕缩小到移动端尺寸,自动退出沉浸式布局');
|
||||
exitImmersiveMode();
|
||||
}
|
||||
}
|
||||
// 禁用:窗口大小变化时不再自动退出沉浸模式
|
||||
// function handleWindowResize() {
|
||||
// if (window.innerWidth <= 700 && isImmersiveActive) {
|
||||
// console.log('检测到屏幕缩小到移动端尺寸,自动退出沉浸式布局');
|
||||
// exitImmersiveMode();
|
||||
// }
|
||||
// }
|
||||
// window.addEventListener('resize', handleWindowResize);
|
||||
|
||||
// 添加窗口大小变化监听器
|
||||
window.addEventListener('resize', handleWindowResize);
|
||||
// 【严格沉浸模式】必须成功进入沉浸模式才显示页面
|
||||
console.log('[ImmersiveLayout] 强制进入沉浸模式');
|
||||
|
||||
// Restore immersive state from localStorage
|
||||
const savedImmersiveState = localStorage.getItem(LS_IMMERSIVE_KEY);
|
||||
|
||||
// 历史详情页默认进入沉浸模式,只有用户明确退出过才不进入
|
||||
// 注意:null 表示首次访问,'true' 表示用户之前处于沉浸模式
|
||||
// 只有 'false' 才表示用户主动退出过沉浸模式
|
||||
const shouldEnterImmersive = savedImmersiveState !== 'false';
|
||||
|
||||
console.log('[ImmersiveLayout] savedImmersiveState:', savedImmersiveState, 'shouldEnterImmersive:', shouldEnterImmersive);
|
||||
|
||||
if (shouldEnterImmersive) {
|
||||
// 检查是否为移动端,如果是则不恢复沉浸模式
|
||||
if (window.innerWidth <= 700) {
|
||||
console.log('检测到移动端设备,不进入沉浸式布局');
|
||||
localStorage.setItem(LS_IMMERSIVE_KEY, 'false');
|
||||
// 显示页面
|
||||
document.documentElement.classList.remove('immersive-pending');
|
||||
document.documentElement.classList.add('immersive-ready');
|
||||
document.body.classList.remove('immersive-pending');
|
||||
document.body.classList.add('immersive-ready');
|
||||
} else {
|
||||
// 立即进入沉浸模式(静默模式,无动画)
|
||||
console.log('[ImmersiveLayout] 准备进入沉浸模式, isImmersiveActive:', isImmersiveActive);
|
||||
enterImmersiveMode({ silent: true });
|
||||
console.log('[ImmersiveLayout] enterImmersiveMode 调用完成, isImmersiveActive:', isImmersiveActive);
|
||||
}
|
||||
} else {
|
||||
// 不需要沉浸模式,直接显示页面
|
||||
// 检查是否为移动端,如果是则显示普通布局
|
||||
if (window.innerWidth <= 700) {
|
||||
console.log('[ImmersiveLayout] 检测到移动端设备,显示普通布局');
|
||||
document.documentElement.classList.remove('immersive-pending');
|
||||
document.documentElement.classList.add('immersive-ready');
|
||||
document.body.classList.remove('immersive-pending');
|
||||
document.body.classList.add('immersive-ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore simple immersive state from localStorage
|
||||
const savedSimpleImmersiveState = localStorage.getItem(LS_SIMPLE_IMMERSIVE_KEY);
|
||||
if (savedSimpleImmersiveState === 'true') {
|
||||
// 检查是否为移动端,如果是则不启用简单沉浸模式
|
||||
if (window.innerWidth <= 700) {
|
||||
console.log('检测到移动端设备,不启用简单沉浸式布局状态');
|
||||
localStorage.setItem(LS_SIMPLE_IMMERSIVE_KEY, 'false'); // 清除保存的状态
|
||||
} else {
|
||||
// 如果不在标准沉浸模式下,才启用简单沉浸模式
|
||||
if (!isImmersiveActive) {
|
||||
setTimeout(() => {
|
||||
if (!isSimpleImmersiveActive) {
|
||||
document.body.classList.add('simple-immersive-pdf-mode');
|
||||
isSimpleImmersiveActive = true;
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '<i class="fas fa-compress-alt"></i>';
|
||||
toggleBtn.title = '退出简单沉浸模式';
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
// 尝试进入沉浸模式(静默模式,无动画)
|
||||
const success = enterImmersiveMode({ silent: true });
|
||||
|
||||
if (success) {
|
||||
console.log('[ImmersiveLayout] 成功进入沉浸模式');
|
||||
} else {
|
||||
// 失败时持续重试,不显示页面
|
||||
console.error('[ImmersiveLayout] 进入沉浸模式失败,300ms 后重试...');
|
||||
setTimeout(mainInit, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Immersive layout logic initialized.');
|
||||
|
|
|
|||
|
|
@ -63,22 +63,22 @@
|
|||
<div class="flex items-center overflow-hidden mr-2">
|
||||
<iconify-icon icon="${icon}" class="${iconColor} mr-2 flex-shrink-0" width="20"></iconify-icon>
|
||||
<span class="flex flex-col overflow-hidden">
|
||||
<span class="text-sm text-gray-800 truncate" title="${displayName}">${displayName}</span>
|
||||
<span class="truncate text-[13px] text-[#999999]" title="${displayName} (${global.formatFileSize(file.size)})">${displayName} (${global.formatFileSize(file.size)})</span>
|
||||
${displayPath && displayPath !== displayName ? `<span class="text-[11px] text-gray-500 truncate" title="${displayPath}">${displayPath}</span>` : ''}
|
||||
</span>
|
||||
${virtualBadge}
|
||||
${isExcluded ? '<span class="ml-2 inline-block text-[10px] px-1.5 py-0.5 rounded bg-gray-200 text-gray-600 flex-shrink-0">已排除</span>' : ''}
|
||||
<span class="text-xs text-gray-500 ml-2 flex-shrink-0">(${global.formatFileSize(file.size)})</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button data-index="${index}" class="preview-file-btn text-gray-400 hover:text-blue-600 flex-shrink-0" title="预览">
|
||||
<iconify-icon icon="carbon:search" width="16"></iconify-icon>
|
||||
</button>
|
||||
|
||||
<button data-index="${index}" class="remove-file-btn text-gray-400 hover:text-red-600 flex-shrink-0" title="移除">
|
||||
<iconify-icon icon="carbon:close" width="16"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
/* <button data-index="${index}" class="preview-file-btn text-gray-400 hover:text-blue-600 flex-shrink-0" title="预览">
|
||||
<iconify-icon icon="carbon:search" width="16"></iconify-icon>
|
||||
</button>*/
|
||||
if (isExcluded) {
|
||||
listItem.classList.add('opacity-60');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,12 +63,14 @@ app.get('/health', async (req, res) => {
|
|||
res.json({
|
||||
status: 'ok',
|
||||
database: 'connected',
|
||||
authDisabled: process.env.AUTH_DISABLED === 'true',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
database: 'disconnected',
|
||||
authDisabled: process.env.AUTH_DISABLED === 'true',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import fetch from 'node-fetch';
|
|||
import { prisma } from '../db/client.js';
|
||||
|
||||
// 是否禁用认证(开发/测试模式)
|
||||
// TODO: 生产环境必须移除此配置,强制要求认证
|
||||
const AUTH_DISABLED = process.env.AUTH_DISABLED === 'true' || process.env.NODE_ENV === 'test';
|
||||
|
||||
// 默认认证 API URL
|
||||
|
|
@ -17,6 +18,7 @@ const tokenCache = new Map();
|
|||
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
|
||||
|
||||
// 测试用户(认证禁用时使用)
|
||||
// TODO: 生产环境必须移除此测试用户
|
||||
const TEST_USER = {
|
||||
id: 'test-user-001',
|
||||
username: 'testuser',
|
||||
|
|
@ -114,6 +116,7 @@ export async function ensureUserExists(userId, name) {
|
|||
*/
|
||||
export async function requireAuth(req, res, next) {
|
||||
// 认证禁用模式(开发/测试)
|
||||
// TODO: 生产环境必须移除此分支,强制要求认证
|
||||
if (AUTH_DISABLED) {
|
||||
await ensureUserExists(TEST_USER.id, TEST_USER.nickname);
|
||||
req.user = TEST_USER;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
"dev": "node --watch server.js",
|
||||
"prisma:migrate": "npx prisma migrate dev",
|
||||
"prisma:generate": "npx prisma generate",
|
||||
"prisma:studio": "npx prisma studio"
|
||||
},
|
||||
"keywords": [
|
||||
"paper-burner",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "documents" ADD COLUMN "ossKey" TEXT,
|
||||
ADD COLUMN "ossUrl" TEXT;
|
||||
|
|
@ -141,6 +141,10 @@ model Document {
|
|||
fileType String
|
||||
filePath String? // 如果启用文件上传存储
|
||||
|
||||
// OSS 存储(供 AI 助手使用)
|
||||
ossUrl String? // OSS 公网访问 URL
|
||||
ossKey String? // OSS 对象 key(用于删除)
|
||||
|
||||
// 处理状态
|
||||
status DocStatus @default(PENDING)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,54 @@
|
|||
|
||||
import express from 'express';
|
||||
import { prisma } from '../db/client.js';
|
||||
import OSS from 'ali-oss';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ==================== OSS 客户端初始化 ====================
|
||||
let ossClient = null;
|
||||
|
||||
function initOssClient() {
|
||||
const region = process.env.OSS_REGION;
|
||||
const accessKeyId = process.env.OSS_ACCESS_KEY_ID;
|
||||
const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET;
|
||||
const bucket = process.env.OSS_BUCKET || process.env.OSS_BUCKET_NAME;
|
||||
|
||||
if (region && accessKeyId && accessKeySecret && bucket) {
|
||||
let normalizedRegion = region;
|
||||
if (!region.startsWith('oss-')) {
|
||||
normalizedRegion = `oss-${region}`;
|
||||
}
|
||||
ossClient = new OSS({
|
||||
region: normalizedRegion,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
bucket,
|
||||
secure: true
|
||||
});
|
||||
console.log(`[Documents] OSS client initialized with bucket: ${bucket}`);
|
||||
}
|
||||
}
|
||||
initOssClient();
|
||||
|
||||
/**
|
||||
* 删除 OSS 文件
|
||||
* @param {string} ossKey - OSS 对象 key
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function deleteOssFile(ossKey) {
|
||||
if (!ossClient || !ossKey) return false;
|
||||
|
||||
try {
|
||||
await ossClient.delete(ossKey);
|
||||
console.log(`[OSS] Deleted file: ${ossKey}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[OSS] Failed to delete file ${ossKey}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 允许的状态值白名单
|
||||
const ALLOWED_STATUSES = ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'];
|
||||
|
||||
|
|
@ -229,10 +274,11 @@ router.put('/:id', async (req, res, next) => {
|
|||
translationModelName: 'translationModel'
|
||||
};
|
||||
|
||||
// 应用字段映射
|
||||
// 应用字段映射:复制到数据库字段名,然后删除前端字段名(避免进入 metadata)
|
||||
for (const [frontendField, dbField] of Object.entries(fieldMapping)) {
|
||||
if (body[frontendField] !== undefined && body[dbField] === undefined) {
|
||||
if (body[frontendField] !== undefined) {
|
||||
body[dbField] = body[frontendField];
|
||||
delete body[frontendField]; // 删除前端字段,避免进入 metadata
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +358,11 @@ router.delete('/:id', async (req, res, next) => {
|
|||
});
|
||||
|
||||
if (document) {
|
||||
// 删除 OSS 文件(如果有)
|
||||
if (document.ossKey) {
|
||||
await deleteOssFile(document.ossKey);
|
||||
}
|
||||
|
||||
await prisma.document.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
|
@ -323,6 +374,40 @@ router.delete('/:id', async (req, res, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
// 保存 OSS URL 到文档(供 AI 助手使用)
|
||||
router.post('/:id/oss', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { ossUrl, ossKey } = req.body;
|
||||
|
||||
if (!ossUrl) {
|
||||
return res.status(400).json({ error: 'ossUrl is required' });
|
||||
}
|
||||
|
||||
// 验证文档所有权
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id,
|
||||
userId: req.user.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
|
||||
// 更新文档的 OSS 信息
|
||||
await prisma.document.update({
|
||||
where: { id },
|
||||
data: { ossUrl, ossKey }
|
||||
});
|
||||
|
||||
res.json({ success: true, ossUrl, ossKey });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 标注管理 ====================
|
||||
|
||||
// 保存标注
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@ async function handleOssUpload(req, res, origin) {
|
|||
jsonResponse(res, {
|
||||
success: true,
|
||||
url: urlToReturn,
|
||||
key: objectName, // 返回 ossKey 用于后续删除
|
||||
file_name: filePart.filename
|
||||
}, 200, origin);
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,41 @@
|
|||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
/* 严格沉浸模式:默认隐藏普通布局 */
|
||||
body.immersive-pending .app-shell {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 只有沉浸模式激活后才显示内容 */
|
||||
body:not(.immersive-active) .app-shell {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 沉浸模式容器默认隐藏,由 JS 控制 */
|
||||
#immersive-layout-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 沉浸模式激活后显示容器 */
|
||||
body.immersive-active #immersive-layout-container {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* 移动端例外:显示普通布局 */
|
||||
@media (max-width: 700px) {
|
||||
body.immersive-pending .app-shell,
|
||||
body:not(.immersive-active) .app-shell {
|
||||
display: flex !important;
|
||||
}
|
||||
body.immersive-ready .app-shell {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
.title-container{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// 在 DOM 解析前就添加类,避免闪烁
|
||||
|
|
@ -111,7 +146,7 @@
|
|||
</button>
|
||||
|
||||
<!-- Simple Immersive Mode Toggle Button -->
|
||||
<button id="toggle-simple-immersive-btn" class="tiny-round-btn" style="top: 70px !important;" title="进入简单沉浸模式">
|
||||
<button id="toggle-simple-immersive-btn" class="tiny-round-btn hidden" style="display: none;" title="进入简单沉浸模式">
|
||||
<i class="fas fa-eye"></i>
|
||||
<!-- Icon for simple immersive mode -->
|
||||
</button>
|
||||
|
|
@ -402,7 +437,9 @@
|
|||
<!-- 主内容容器 -->
|
||||
<!-- ===================== -->
|
||||
<div class="container">
|
||||
<h2 id="fileName">历史详情</h2>
|
||||
<div class="title-container">
|
||||
<span id="fileName" style="display:block !important;">历史详情</span>
|
||||
</div>
|
||||
<div class="meta" id="fileMeta">
|
||||
<div class="meta-info">
|
||||
<!-- <span id="fileMetaTime"></span>
|
||||
|
|
@ -420,6 +457,7 @@
|
|||
<span>导出</span>
|
||||
</button>
|
||||
</div>
|
||||
<hr style="margin: 0; height: 1px;" />
|
||||
|
||||
<!-- 标签页导航 -->
|
||||
<div class="tabs-container">
|
||||
|
|
|
|||
Loading…
Reference in New Issue