feat: 拖拽/多文件上传,文件芯片展示,发送时才读取内容

This commit is contained in:
root
2026-04-29 09:43:05 +08:00
parent 2cc4d8b907
commit a1654db365

View File

@ -194,6 +194,101 @@ body {
background: var(--surface-darken);
}
/* File Chips */
.file-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 6px;
min-height: 0;
}
.file-chip {
position: relative;
display: flex;
align-items: center;
gap: 4px;
background: var(--surface-darken);
border: 1px solid var(--primary);
border-radius: 4px;
padding: 4px 8px 4px 8px;
font-size: 12px;
}
.file-chip-remove {
position: absolute;
top: -7px;
left: -7px;
width: 16px;
height: 16px;
background: var(--accent);
color: #fff;
border: 1px solid var(--surface);
border-radius: 50%;
font-size: 11px;
line-height: 16px;
text-align: center;
cursor: pointer;
font-weight: bold;
user-select: none;
}
.file-chip-remove:hover {
background: #ff1744;
transform: scale(1.1);
}
.file-chip-icon {
font-size: 16px;
}
.file-chip-name {
color: var(--text);
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-chip-size {
color: var(--text-muted);
font-size: 10px;
}
/* Drag & Drop */
.input-box.drag-over {
outline: 2px dashed var(--accent);
outline-offset: -4px;
background: rgba(233, 69, 96, 0.05);
border-radius: 4px;
}
.drag-overlay {
display: none;
position: absolute;
inset: 0;
background: rgba(26, 26, 46, 0.85);
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 4px;
pointer-events: none;
}
.drag-over .drag-overlay {
display: flex;
}
.drag-overlay-text {
color: var(--accent);
font-size: 18px;
font-weight: bold;
border: 3px dashed var(--accent);
padding: 20px 40px;
border-radius: 8px;
background: var(--surface-darken);
}
.btn {
@ -598,12 +693,14 @@ body {
</div>
</div>
<div class="input-box">
<textarea class="input-area" id="messageInput" placeholder="输入消息Enter 发送Shift+Enter 换行..." rows="3"></textarea>
<div class="input-box" id="inputBox">
<div class="file-chips" id="fileChips"></div>
<!-- Drag overlay -->
<div class="drag-overlay"><span class="drag-overlay-text">📄 松开以上传文件</span></div>
<textarea class="input-area" id="messageInput" placeholder="输入消息,拖拽文件或点击 📎 上传Enter 发送Shift+Enter 换行..." rows="3"></textarea>
<div class="file-upload-row">
<input type="file" class="file-input" id="fileInput">
<input type="file" class="file-input" id="fileInput" multiple>
<button class="file-upload-btn" id="fileUploadBtn">📎 添加文件</button>
</div>
<div class="input-buttons">
<button class="btn" id="sendBtn">发送</button>
@ -723,6 +820,8 @@ const closeHelpBtn = document.getElementById('closeHelp');
// File upload
const fileInputEl = document.getElementById('fileInput');
const fileUploadBtn = document.getElementById('fileUploadBtn');
const fileChipsEl = document.getElementById('fileChips');
let uploadedFiles = [];
// Config inputs
const apiKeyEl = document.getElementById('apiKey');
@ -995,10 +1094,52 @@ function readFileAsText(file) {
});
}
// Send Message
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';
}
// ── File chips ──
function addFileChip(file) {
const chip = document.createElement('div');
chip.className = 'file-chip';
chip.dataset.fileId = file._fileId;
chip.innerHTML = `
<span class="file-chip-remove" data-file-id="${file._fileId}">✕</span>
<span class="file-chip-icon">📄</span>
<span class="file-chip-name">${escapeHtml(file.name)}</span>
<span class="file-chip-size">${formatFileSize(file.size)}</span>
`;
fileChipsEl.appendChild(chip);
}
function removeFileChip(fileId) {
const chip = fileChipsEl.querySelector(`.file-chip[data-file-id="${fileId}"]`);
if (chip) chip.remove();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ── Add files to the pending list ──
function addFilesToList(fileList) {
for (const file of fileList) {
// Skip if already in list (by name + size)
if (uploadedFiles.some(f => f.name === file.name && f.size === file.size)) continue;
file._fileId = 'f' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
uploadedFiles.push(file);
addFileChip(file);
}
}
// ── Send Message ──
async function sendMessage() {
const message = messageInputEl.value.trim();
if (!message) return;
if (!message && uploadedFiles.length === 0) return;
// Check if API is configured
if (apiStatusEl.textContent === '未配置') {
@ -1006,16 +1147,37 @@ async function sendMessage() {
return;
}
// Add user message to UI
addMessageToUI('user', message);
// Build final message: files + user text
let finalMessage = '';
if (uploadedFiles.length > 0) {
const fileBlocks = await Promise.all(uploadedFiles.map(async (file) => {
try {
const content = await readFileAsText(file);
return `文件名:${file.name}\n内容:\n${content}`;
} catch (e) {
return `文件名:${file.name}\n内容:(读取失败: ${e.message})`;
}
}));
finalMessage = fileBlocks.join('\n\n---\n\n');
if (message) finalMessage += '\n\n' + message;
} else {
finalMessage = message;
}
// Clear UI
uploadedFiles = [];
fileChipsEl.innerHTML = '';
messageInputEl.value = '';
// Add user message to UI
addMessageToUI('user', finalMessage);
// Add processing message
addMessageToUI('assistant', '⏳ 正在处理...');
setProcessing(true);
// Send to API
const result = await apiRequest('/api/message', 'POST', { message });
const result = await apiRequest('/api/message', 'POST', { message: finalMessage });
if (result.success) {
const data = result.data || {};
@ -1125,28 +1287,55 @@ function closeSidebar() {
}
// File upload helpers
function insertFileContent(file) {
readFileAsText(file).then(content => {
const block = `文件名:${file.name}\n内容:\n${content}`;
const current = messageInputEl.value;
const cursorPos = messageInputEl.selectionStart;
if (current.length === 0) {
messageInputEl.value = block;
} else {
// 插入到光标位置
messageInputEl.value = current.slice(0, cursorPos) + block + '\n\n' + current.slice(cursorPos);
}
messageInputEl.focus();
fileInputEl.value = '';
}).catch(e => {
alert(`文件读取失败: ${e.message}`);
});
}
// ── Event: file picker ──
fileUploadBtn.addEventListener('click', () => fileInputEl.click());
fileInputEl.addEventListener('change', () => {
if (fileInputEl.files && fileInputEl.files[0]) {
insertFileContent(fileInputEl.files[0]);
if (fileInputEl.files && fileInputEl.files.length > 0) {
addFilesToList(fileInputEl.files);
fileInputEl.value = '';
}
});
// ── Event: chip remove via delegation ──
fileChipsEl.addEventListener('click', (e) => {
const removeBtn = e.target.closest('.file-chip-remove');
if (!removeBtn) return;
const fileId = removeBtn.dataset.fileId;
uploadedFiles = uploadedFiles.filter(f => f._fileId !== fileId);
removeFileChip(fileId);
});
// ── Drag & Drop ──
const inputBoxEl = document.getElementById('inputBox');
inputBoxEl.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
inputBoxEl.classList.add('drag-over');
});
inputBoxEl.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
inputBoxEl.classList.add('drag-over');
});
inputBoxEl.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
// Only remove if leaving the whole input-box
if (!inputBoxEl.contains(e.relatedTarget)) {
inputBoxEl.classList.remove('drag-over');
}
});
inputBoxEl.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
inputBoxEl.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files && files.length > 0) {
addFilesToList(files);
}
});