From ea4e43bf97acdb0944afe82562749b453d0fbb01 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 8 May 2026 10:58:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=98=9F=E5=9B=BE=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=9F=BA=E4=BA=8E=E6=8F=90=E5=8F=8A=E6=AC=A1?= =?UTF-8?q?=E6=95=B0=E5=86=B3=E5=AE=9A=E8=8A=82=E7=82=B9=E9=97=B4=E8=B7=9D?= =?UTF-8?q?=20-=20=E5=88=A0=E9=99=A4=E6=8C=89=E8=BF=9E=E6=8E=A5=E5=BA=A6?= =?UTF-8?q?=E5=88=86=E5=B1=82=E7=9A=84=E6=97=A7=E5=B8=83=E5=B1=80=E7=AE=97?= =?UTF-8?q?=E6=B3=95=20-=20=E6=96=B0=E5=A2=9E=E5=9F=BA=E4=BA=8E=20mention?= =?UTF-8?q?=5Fcount=20=E7=9A=84=E8=B7=9D=E7=A6=BB=E5=88=86=E9=85=8D?= =?UTF-8?q?=EF=BC=9A=E6=8F=90=E5=8F=8A=E8=B6=8A=E9=AB=98=E7=A6=BB=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E8=B6=8A=E8=BF=9C=20-=20=E8=A7=92=E5=BA=A6=E9=97=B4?= =?UTF-8?q?=E9=9A=94=E4=B9=9F=E4=B8=8E=20mention=5Fcount=20=E6=88=90?= =?UTF-8?q?=E6=AD=A3=E6=AF=94=EF=BC=9A=E9=AB=98=E6=8F=90=E5=8F=8A=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=8B=89=E5=BC=80=E9=97=B4=E8=B7=9D=20-=20=E5=8A=9B?= =?UTF-8?q?=E5=AF=BC=E5=90=91=E6=A8=A1=E6=8B=9F=E6=96=A5=E5=8A=9B=E4=B8=8E?= =?UTF-8?q?=20mention=5Fcount=20=E6=88=90=E6=AD=A3=E6=AF=94=20-=20?= =?UTF-8?q?=E5=BC=95=E5=8A=9B=E4=B8=8E=20mention=5Fcount=20=E6=88=90?= =?UTF-8?q?=E5=8F=8D=E6=AF=94=EF=BC=88=E9=81=BF=E5=85=8D=E6=8B=89=E5=9B=9E?= =?UTF-8?q?=E9=AB=98=E6=8F=90=E5=8F=8A=E8=8A=82=E7=82=B9=EF=BC=89=20-=20?= =?UTF-8?q?=E7=90=83=E4=BD=93=E5=A4=A7=E5=B0=8F=E3=80=81=E5=8F=91=E5=85=89?= =?UTF-8?q?=E5=BC=BA=E5=BA=A6=E3=80=81Y=E8=BD=B4=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E5=9D=87=E4=B8=8E=20mention=5Fcount=20=E6=88=90=E6=AD=A3?= =?UTF-8?q?=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/static/graph.html | 447 ++++++++++--------------------------------- 1 file changed, 104 insertions(+), 343 deletions(-) diff --git a/ui/static/graph.html b/ui/static/graph.html index 4caac28..d489baf 100644 --- a/ui/static/graph.html +++ b/ui/static/graph.html @@ -298,11 +298,10 @@ text-align: right; } .chat-input-area { - padding: 10px 12px; - padding-bottom: max(10px, calc(10px + env(safe-area-inset-bottom))); + padding: 12px 16px; border-top: 1px solid rgba(100, 100, 255, 0.15); display: flex; - gap: 6px; + gap: 8px; flex-shrink: 0; } .chat-input-area input { @@ -332,75 +331,6 @@ .chat-input-area button:hover { background: rgba(68, 136, 255, 0.5); } -.attach-btn { - background: rgba(68, 136, 255, 0.2); - border: 1px solid rgba(68, 136, 255, 0.3); - color: #aaf; - width: 40px; - height: 40px; - border-radius: 6px; - cursor: pointer; - font-size: 18px; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - flex-shrink: 0; -} -.attach-btn:hover { - background: rgba(68, 136, 255, 0.4); -} -.chat-files-container { - display: flex; - flex-wrap: wrap; - gap: 3px; - padding: 2px 12px 6px; - min-height: 0; -} -.chat-file-chip { - display: flex; - align-items: center; - gap: 4px; - background: rgba(40, 40, 80, 0.7); - border: 1px solid rgba(100, 100, 255, 0.2); - border-radius: 3px; - padding: 2px 6px; - font-size: 11px; - color: #dde; - max-width: 160px; - cursor: default; -} -.chat-file-chip .file-icon { - color: #7af; - flex-shrink: 0; - font-size: 10px; -} -.chat-file-chip .file-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - font-size: 11px; -} -.chat-file-chip .file-size { - color: #667; - font-size: 9px; - flex-shrink: 0; -} -.chat-file-chip .file-remove { - color: #f66; - cursor: pointer; - font-size: 12px; - line-height: 1; - flex-shrink: 0; -} -.chat-file-chip .file-remove:hover { - color: #f33; -} -.chat-messages.drag-over { - background: rgba(68, 136, 255, 0.12); - border: 2px dashed rgba(68, 136, 255, 0.5); -} .chat-status { padding: 8px 16px; border-top: 1px solid rgba(100, 100, 255, 0.1); @@ -454,8 +384,6 @@ - -
@@ -508,14 +436,11 @@
-
- -
-
已连接
+
🟢 已连接
- + @@ -821,7 +746,7 @@ function init() { if (nodes.length === 0) return; - // 计算节点度数 + // 计算节点度数(用于叶子节点判断) const nodeDegrees = {}; nodes.forEach(n => nodeDegrees[n.id] = 0); edges.forEach(e => { @@ -829,97 +754,80 @@ function init() { nodeDegrees[e.target] = (nodeDegrees[e.target] || 0) + 1; }); - // 按度排序 - const sortedNodes = [...nodes].sort((a, b) => (nodeDegrees[b.id] || 0) - (nodeDegrees[a.id] || 0)); + // 按 mention_count 排序(降序) + const sortedByMentions = [...nodes].sort((a, b) => (b.mention_count || 0) - (a.mention_count || 0)); - // 计算布局位置(分层布局) + // 计算 mention_count 范围 + const mentionCounts = sortedByMentions.map(n => n.mention_count || 0); + const maxMentions = Math.max(...mentionCounts, 1); + const minMentions = Math.min(...mentionCounts, 0); + const mentionRange = maxMentions - minMentions || 1; + + // 布局位置 const positions = {}; const nodeMap = {}; + sortedByMentions.forEach(n => nodeMap[n.id] = n); - // 第一层:度最高的节点放外层 - const maxDegree = Math.max(...Object.values(nodeDegrees)); - const layers = { - outer: [], // 度 > 70% 最大值 - middle: [], // 度在 30%-70% - inner: [] // 度 < 30% - }; + // 半径配置 + const baseRadius = 15; + const maxRadius = 80; - sortedNodes.forEach(node => { - const degree = nodeDegrees[node.id] || 0; - const ratio = maxDegree > 0 ? degree / maxDegree : 0; + // 角度分配:mention_count 越高的节点,角度间隔越大 + const totalNodes = sortedByMentions.length; + let accumulatedAngle = 0; - if (ratio > 0.7) { - layers.outer.push(node); - } else if (ratio > 0.3) { - layers.middle.push(node); - } else { - layers.inner.push(node); - } - nodeMap[node.id] = node; + sortedByMentions.forEach((node, i) => { + const mention = node.mention_count || 0; + // 归一化 mention_count (0~1) + const mentionNorm = mentionRange > 0 ? (mention - minMentions) / mentionRange : 0; + + // 与中心的距离:mention_count 越高,离中心越远 + const radius = baseRadius + mentionNorm * (maxRadius - baseRadius); + + // 角度间隔:mention_count 越高,间隔越大(拉开距离) + // 基础间隔 + 按 mention_count 加权的额外间隔 + const baseAngleStep = (Math.PI * 2) / totalNodes; + const extraAngle = mentionNorm * baseAngleStep * 2; // 高提及节点额外拉开 + const angle = accumulatedAngle + extraAngle / 2; + + accumulatedAngle += baseAngleStep + extraAngle; + + const x = radius * Math.cos(angle); + const z = radius * Math.sin(angle); + const y = (Math.random() - 0.5) * (10 + mentionNorm * 20); // 高提及节点Y轴范围更大 + + positions[node.id] = { x, y, z, mentionNorm, radius }; }); - // 分配位置 - let currentAngle = 0; - - // 外层(大半径) - const outerRadius = 50; - layers.outer.forEach((node, i) => { - const angle = (i / layers.outer.length) * Math.PI * 2; - const x = outerRadius * Math.cos(angle); - const z = outerRadius * Math.sin(angle); - const y = (Math.random() - 0.5) * 20; - positions[node.id] = { x, y, z }; - }); - - // 中层 - const middleRadius = 30; - layers.middle.forEach((node, i) => { - const angle = (i / layers.middle.length) * Math.PI * 2 + currentAngle; - const x = middleRadius * Math.cos(angle); - const z = middleRadius * Math.sin(angle); - const y = (Math.random() - 0.5) * 15; - positions[node.id] = { x, y, z }; - }); - - // 内层(核心) - const innerRadius = 12; - layers.inner.forEach((node, i) => { - // 叶子节点放在其父节点附近 + // 处理叶子节点(度为1):放在其父节点附近,但保持一定距离 + sortedByMentions.forEach(node => { const degree = nodeDegrees[node.id] || 0; if (degree === 1) { - // 找到连接的节点 const edge = edges.find(e => e.source === node.id || e.target === node.id); if (edge) { const parentId = edge.source === node.id ? edge.target : edge.source; if (positions[parentId]) { const parentPos = positions[parentId]; - const offset = 5 + Math.random() * 3; + const mention = node.mention_count || 0; + const mentionNorm = mentionRange > 0 ? (mention - minMentions) / mentionRange : 0; + // 叶子节点偏移量也与 mention_count 相关 + const offset = 6 + mentionNorm * 8 + Math.random() * 4; const angle = Math.random() * Math.PI * 2; positions[node.id] = { x: parentPos.x + offset * Math.cos(angle), - y: parentPos.y + (Math.random() - 0.5) * 4, - z: parentPos.z + offset * Math.sin(angle) + y: parentPos.y + (Math.random() - 0.5) * (4 + mentionNorm * 6), + z: parentPos.z + offset * Math.sin(angle), + mentionNorm, + radius: offset }; - return; } } } - - // 其他节点均匀分布在核心区域 - const angle = (i / layers.inner.length) * Math.PI * 2; - const x = innerRadius * Math.cos(angle); - const z = innerRadius * Math.sin(angle); - const y = (Math.random() - 0.5) * 8; - positions[node.id] = { x, y, z }; }); - // 简单力导向模拟(迭代几次)— 使用度数加权距离 - function getIdealDist(id) { - return 5 + ((nodeDegrees[id] || 0) * 0.8); - } - - for (let iter = 0; iter < 30; iter++) { - // 斥力:节点之间互相排斥 + // 力导向模拟(迭代几次) + for (let iter = 0; iter < 50; iter++) { + // 斥力:节点之间互相排斥,与 mention_count 成正比 Object.keys(positions).forEach(id1 => { Object.keys(positions).forEach(id2 => { if (id1 >= id2) return; @@ -930,14 +838,13 @@ function init() { const dz = pos1.z - pos2.z; const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1; - // 动态阈值:度数越高理想距离越大,取两节点较大值 - const idealDist = getIdealDist(id1) + getIdealDist(id2); - const repulsionThreshold = idealDist * 1.5; + // 斥力与两个节点的 mention_count 之和成正比 + const m1 = positions[id1].mentionNorm || 0; + const m2 = positions[id2].mentionNorm || 0; + const repulsionFactor = 0.5 + (m1 + m2) * 0.5; // 高提及节点排斥更强 - if (dist < repulsionThreshold) { - // 斥力强度按度数加权 - const degreeWeight = ((nodeDegrees[id1] || 0) + (nodeDegrees[id2] || 0)) * 0.5 + 1; - const force = (0.04 * degreeWeight) / Math.max(dist, 0.5); + if (dist < 25) { + const force = (0.06 * repulsionFactor) / Math.max(dist, 0.5); const fx = (dx / dist) * force; const fy = (dy / dist) * force; const fz = (dz / dist) * force; @@ -963,12 +870,13 @@ function init() { const dz = pos2.z - pos1.z; const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1; - // 动态引力阈值:以两节点平均理想距离为基准 - const idealDist = (getIdealDist(edge.source) + getIdealDist(edge.target)) * 0.8; - const attractionThreshold = idealDist * 1.5; + // 引力强度与 mention_count 成反比:高提及节点已经离得远,减少引力避免拉回 + const m1 = positions[edge.source].mentionNorm || 0; + const m2 = positions[edge.target].mentionNorm || 0; + const attractionFactor = Math.max(0.3, 1.0 - (m1 + m2) * 0.3); - if (dist > attractionThreshold) { - const force = 0.05; + if (dist > 20) { + const force = 0.04 * attractionFactor; const fx = (dx / dist) * force; const fy = (dy / dist) * force; const fz = (dz / dist) * force; @@ -981,6 +889,19 @@ function init() { pos2.z -= fz; } }); + + // 向心力约束:保持节点在合理范围内 + Object.keys(positions).forEach(id => { + const pos = positions[id]; + const distFromCenter = Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z); + const maxAllowed = maxRadius * 1.5; + if (distFromCenter > maxAllowed) { + const scale = maxAllowed / distFromCenter; + pos.x *= scale; + pos.y *= scale; + pos.z *= scale; + } + }); } // 创建节点(球体) @@ -988,32 +909,34 @@ function init() { const pos = positions[node.id]; if (!pos) return; - // 计算球体半径 - const radius = 0.4 + Math.min(node.mention_count * 0.15, 1.5); + // 球体半径:mention_count 越高,球体越大 + const mention = node.mention_count || 0; + const mentionNorm = mentionRange > 0 ? (mention - minMentions) / mentionRange : 0; + const radius = 0.5 + mentionNorm * 2.0; // 0.5 ~ 2.5 const color = typeColors[node.type] || defaultColor; - // 根据 mention_count 计算 emissiveIntensity - const baseEmissiveIntensity = 0.5 + Math.min(node.mention_count * 0.05, 0.3); + // 发光强度也与 mention_count 成正比 + const emissiveIntensity = 0.3 + mentionNorm * 0.7; // 创建球体 const geometry = new THREE.SphereGeometry(radius, 16, 12); const material = new THREE.MeshPhongMaterial({ color: color, emissive: color, - emissiveIntensity: baseEmissiveIntensity, + emissiveIntensity: emissiveIntensity, shininess: 30 }); const sphere = new THREE.Mesh(geometry, material); sphere.position.set(pos.x, pos.y, pos.z); - sphere.userData = { nodeId: node.id, nodeData: node, baseEmissive: 0.5 + Math.min(node.mention_count * 0.05, 0.3) }; + sphere.userData = { nodeId: node.id, nodeData: node, baseEmissive: emissiveIntensity }; - // 创建外层发光球体(圆形光晕,替代方形Sprite) - const glowRadius = radius * 1.15; + // 创建外层发光球体 + const glowRadius = radius * 1.2 + mentionNorm * 0.5; const glowGeometry = new THREE.SphereGeometry(glowRadius, 16, 12); const glowMaterial = new THREE.MeshBasicMaterial({ color: color, transparent: true, - opacity: 0.15, + opacity: 0.12 + mentionNorm * 0.08, side: THREE.BackSide, blending: THREE.AdditiveBlending }); @@ -1021,15 +944,12 @@ function init() { sphere.add(glowSphere); sphere.userData.glowSphere = glowSphere; - // 创建标签(改进样式) + // 创建标签 const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = 256; canvas.height = 64; - - // 显式清空画布为透明 context.clearRect(0, 0, canvas.width, canvas.height); - // 文字带发光效果 context.font = 'bold 24px Courier New'; context.fillStyle = '#ffffff'; context.textAlign = 'center'; @@ -1091,9 +1011,7 @@ function init() { const dist = Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z); maxDist = Math.max(maxDist, dist); }); - // 相机距离:节点范围加 20u 余量,但不超过 120u,不低于 25u - const targetDist = Math.min(Math.max(maxDist + 15, 25), 120); - // 如果有待聚焦节点且自动视角开启,飞过去;否则全局定位 + const targetDist = Math.min(Math.max(maxDist + 20, 30), 150); if (_pendingFocusNodeId) { const focusId = _pendingFocusNodeId; _pendingFocusNodeId = null; @@ -1250,18 +1168,6 @@ function smoothReposition() { }; }); - // 重新计算度数(从当前节点 + 边) - const smoothDegrees = {}; - nodeMeshes.forEach(m => { smoothDegrees[m.userData.nodeId] = 0; }); - edges.forEach(e => { - smoothDegrees[e.source] = (smoothDegrees[e.source] || 0) + 1; - smoothDegrees[e.target] = (smoothDegrees[e.target] || 0) + 1; - }); - - function getSmoothIdealDist(id) { - return 5 + ((smoothDegrees[id] || 0) * 0.8); - } - // 用力导向模拟迭代 30 次平滑 for (let iter = 0; iter < 30; iter++) { // 斥力 @@ -1274,11 +1180,8 @@ function smoothReposition() { const dy = a.y - b.y; const dz = a.z - b.z; const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1; - const idealDist = getSmoothIdealDist(ids[i]) + getSmoothIdealDist(ids[j]); - const repulsionThreshold = idealDist * 1.5; - if (dist < repulsionThreshold) { - const degreeWeight = ((smoothDegrees[ids[i]] || 0) + (smoothDegrees[ids[j]] || 0)) * 0.5 + 1; - const force = (0.08 * degreeWeight) / Math.max(dist, 0.5); + if (dist < 12) { + const force = 0.08 / Math.max(dist, 0.5); a.x += (dx/dist) * force; a.y += (dy/dist) * force; a.z += (dz/dist) * force; @@ -1297,9 +1200,7 @@ function smoothReposition() { const dy = p2.y - p1.y; const dz = p2.z - p1.z; const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1; - const idealDist = (getSmoothIdealDist(e.source) + getSmoothIdealDist(e.target)) * 0.8; - const attractionThreshold = idealDist * 1.5; - if (dist > attractionThreshold) { + if (dist > 8) { const force = 0.03; p1.x += (dx/dist) * force; p1.y += (dy/dist) * force; @@ -1881,92 +1782,18 @@ function smoothReposition() { const chatCloseBtn = document.getElementById('chatCloseBtn'); const toggleChat = document.getElementById('toggle-chat'); const chatPanel = document.getElementById('chat-panel'); - const attachBtn = document.getElementById('attachBtn'); - const fileInput = document.getElementById('fileInput'); - const chatFilesContainer = document.getElementById('chatFilesContainer'); - - let attachedFiles = []; - - function getFileIcon(name) { - const lower = name.toLowerCase(); - if (lower.endsWith('.pdf')) return '📕'; - if (lower.endsWith('.docx') || lower.endsWith('.doc')) return '📘'; - if (lower.endsWith('.xlsx') || lower.endsWith('.xls') || lower.endsWith('.csv')) return '📊'; - if (lower.endsWith('.pptx') || lower.endsWith('.ppt')) return '📙'; - if (lower.endsWith('.jpg') || lower.endsWith('.jpeg') || lower.endsWith('.png') || lower.endsWith('.gif') || lower.endsWith('.webp')) return '🖼️'; - if (lower.endsWith('.json') || lower.endsWith('.xml') || lower.endsWith('.yaml') || lower.endsWith('.yml') || lower.endsWith('.toml')) return '⚙️'; - if (lower.endsWith('.py') || lower.endsWith('.js') || lower.endsWith('.ts') || lower.endsWith('.java') || lower.endsWith('.c') || lower.endsWith('.cpp') || lower.endsWith('.h') || lower.endsWith('.sh') || lower.endsWith('.bat') || lower.endsWith('.ps1')) return '💻'; - return '📄'; - } - - function formatFileSize(bytes) { - if (bytes < 1024) return bytes + 'B'; - if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'; - return (bytes / (1024 * 1024)).toFixed(1) + 'MB'; - } - - function getFileKey(file) { - return file.name + '_' + file.size; - } - - function addFileChip(file) { - const key = getFileKey(file); - if (attachedFiles.some(f => getFileKey(f) === key)) return; - attachedFiles.push(file); - renderFileChips(); - } - - function removeFileChip(file) { - const key = getFileKey(file); - attachedFiles = attachedFiles.filter(f => getFileKey(f) !== key); - renderFileChips(); - } - - function renderFileChips() { - chatFilesContainer.innerHTML = ''; - attachedFiles.forEach(file => { - const chip = document.createElement('div'); - chip.className = 'chat-file-chip'; - const icon = getFileIcon(file.name); - chip.innerHTML = `${icon}${file.name}${formatFileSize(file.size)}`; - chip.querySelector('.file-remove').onclick = () => removeFileChip(file); - chatFilesContainer.appendChild(chip); - }); - } - - attachBtn.addEventListener('click', () => fileInput.click()); - - fileInput.addEventListener('change', () => { - Array.from(fileInput.files).forEach(addFileChip); - fileInput.value = ''; - }); - - chatMessages.addEventListener('dragover', e => { - e.preventDefault(); - chatMessages.classList.add('drag-over'); - }); - - chatMessages.addEventListener('dragleave', () => { - chatMessages.classList.remove('drag-over'); - }); - - chatMessages.addEventListener('drop', e => { - e.preventDefault(); - chatMessages.classList.remove('drag-over'); - Array.from(e.dataTransfer.files).forEach(addFileChip); - }); let chatCollapsed = false; toggleChat.addEventListener('click', () => { chatCollapsed = !chatCollapsed; chatPanel.classList.toggle('collapsed', chatCollapsed); - toggleChat.innerHTML = chatCollapsed ? "" : ""; + toggleChat.textContent = chatCollapsed ? '📎' : '📌'; setTimeout(onWindowResize, 300); }); chatCloseBtn.addEventListener('click', () => { chatCollapsed = true; chatPanel.classList.add('collapsed'); - toggleChat.innerHTML = ''; + toggleChat.textContent = '📎'; setTimeout(onWindowResize, 300); }); @@ -2016,77 +1843,11 @@ function smoothReposition() { } } - async function readFileContent(file) { - const name = file.name.toLowerCase(); - - // PDF 文件:用 pdf.js 解析 - if (name.endsWith('.pdf')) { - if (typeof pdfjsLib === 'undefined') throw new Error('PDF.js 未加载'); - pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js'; - const arrayBuffer = await file.arrayBuffer(); - const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; - const pages = []; - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const content = await page.getTextContent(); - const text = content.items.map(item => item.str).join(' '); - pages.push(text); - } - return pages.join('\n\n'); - } - - // Word 文件:用 mammoth.js 解析 - if (name.endsWith('.docx')) { - if (typeof mammoth === 'undefined') throw new Error('mammoth.js 未加载'); - const arrayBuffer = await file.arrayBuffer(); - const result = await mammoth.extractRawText({ arrayBuffer }); - return result.value || '(Word 文档内容为空)'; - } - - // 其他文件:当作纯文本读取 - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = () => reject(new Error('文件读取失败')); - reader.readAsText(file); - }); - } - - function buildFullMessage(text, files) { - if (!files || files.length === 0) return text; - let parts = []; - files.forEach(file => { - parts.push(`文件名:${file.name}`); - parts.push(`内容:`); - parts.push(file.content); - parts.push('---'); - }); - parts.push(text); - return parts.join('\n'); - } - async function sendChatMessage() { - const files = attachedFiles.slice(); const msg = chatInput.value.trim(); - if (!msg && files.length === 0) return; + if (!msg) return; chatInput.value = ''; - // 读取文件内容 - let fullMsg = msg; - if (files.length > 0) { - const fileReads = files.map(async (file) => { - try { - file.content = await readFileContent(file); - } catch(e) { - file.content = `[文件读取失败: ${file.name}] ${e.message}`; - } - }); - await Promise.all(fileReads); - fullMsg = buildFullMessage(msg, files); - // 清除文件芯片 - attachedFiles = []; - renderFileChips(); - } - addChatMessage('user', fullMsg); + addChatMessage('user', msg); chatInput.disabled = true; chatSendBtn.disabled = true; chatStatus.textContent = '⏳ 处理中...'; @@ -2096,22 +1857,22 @@ function smoothReposition() { const res = await fetch('/api/message', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({message: fullMsg}) + body: JSON.stringify({message: msg}) }); const data = await res.json(); if (data.success) { const reply = data.content || data.data?.content || '(无回复)'; addChatMessage('assistant', reply); - chatStatus.innerHTML = ' 已连接'; + chatStatus.textContent = '🟢 已连接'; chatStatus.className = 'chat-status'; } else { addChatMessage('assistant', '❌ ' + (data.error || '请求失败')); - chatStatus.innerHTML = ' 错误: ' + (data.error || 'unknown'); + chatStatus.textContent = '🔴 错误: ' + (data.error || 'unknown'); chatStatus.className = 'chat-status error'; } } catch(e) { addChatMessage('assistant', '❌ 网络错误: ' + e.message); - chatStatus.innerHTML = ' 网络错误'; + chatStatus.textContent = '🔴 网络错误'; chatStatus.className = 'chat-status error'; } chatInput.disabled = false;