Add files via upload

This commit is contained in:
公明
2026-03-02 20:13:16 +08:00
committed by GitHub
parent 2e482a3baf
commit dd311f7a3b
4 changed files with 434 additions and 22 deletions
+94
View File
@@ -1582,12 +1582,106 @@ header {
box-sizing: border-box;
}
.chat-input-container > .chat-input-with-files {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
gap: 6px;
}
.chat-input-container > .chat-input-field {
flex: 1;
display: flex;
min-width: 0;
}
.chat-file-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
min-height: 0;
}
.chat-file-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: rgba(0, 102, 255, 0.08);
border: 1px solid rgba(0, 102, 255, 0.2);
border-radius: 8px;
font-size: 12px;
color: var(--text-primary);
max-width: 200px;
}
.chat-file-chip-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-file-chip-remove {
flex-shrink: 0;
width: 18px;
height: 18px;
padding: 0;
border: none;
background: rgba(0, 0, 0, 0.08);
border-radius: 50%;
cursor: pointer;
color: var(--text-muted);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
}
.chat-file-chip-remove:hover {
background: rgba(0, 0, 0, 0.12);
color: var(--text-primary);
}
.chat-file-input-hidden {
position: absolute;
width: 0;
height: 0;
opacity: 0;
overflow: hidden;
pointer-events: none;
}
.chat-upload-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #ffffff;
color: var(--text-muted);
cursor: pointer;
transition: border-color 0.2s, color 0.2s, background 0.2s;
flex-shrink: 0;
}
.chat-upload-btn:hover {
border-color: var(--accent-color);
color: var(--accent-color);
background: rgba(0, 102, 255, 0.04);
}
.chat-input-container.drag-over {
background: rgba(0, 102, 255, 0.06);
border-radius: 12px;
outline: 2px dashed rgba(0, 102, 255, 0.35);
outline-offset: -2px;
}
.chat-input-field {
flex: 1;
position: relative;
+165 -15
View File
@@ -22,6 +22,12 @@ const DRAFT_STORAGE_KEY = 'cyberstrike-chat-draft';
let draftSaveTimer = null;
const DRAFT_SAVE_DELAY = 500; // 500ms防抖延迟
// 对话文件上传相关(后端会拼接路径与内容发给大模型,前端不再重复发文件列表)
const MAX_CHAT_FILES = 10;
const CHAT_FILE_DEFAULT_PROMPT = '请根据上传的文件内容进行分析。';
/** @type {{ fileName: string, content: string, mimeType: string }[]} */
let chatAttachments = [];
// 保存输入框草稿到localStorage(防抖版本)
function saveChatDraftDebounced(content) {
// 清除之前的定时器
@@ -107,14 +113,22 @@ function adjustTextareaHeight(textarea) {
// 发送消息
async function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) {
let message = input.value.trim();
const hasAttachments = chatAttachments && chatAttachments.length > 0;
if (!message && !hasAttachments) {
return;
}
// 显示用户消息
addMessage('user', message);
// 有附件且用户未输入时,发一句简短默认提示即可(后端会拼接路径和文件内容给大模型)
if (hasAttachments && !message) {
message = CHAT_FILE_DEFAULT_PROMPT;
}
// 显示用户消息(含附件名,便于用户确认)
const displayMessage = hasAttachments
? message + '\n' + chatAttachments.map(a => '📎 ' + a.fileName).join('\n')
: message;
addMessage('user', displayMessage);
// 清除防抖定时器,防止在清空输入框后重新保存草稿
if (draftSaveTimer) {
@@ -135,7 +149,24 @@ async function sendMessage() {
input.value = '';
// 强制重置输入框高度为初始高度(40px)
input.style.height = '40px';
// 构建请求体(含附件)
const body = {
message: message,
conversationId: currentConversationId,
role: typeof getCurrentRole === 'function' ? getCurrentRole() : ''
};
if (hasAttachments) {
body.attachments = chatAttachments.map(a => ({
fileName: a.fileName,
content: a.content,
mimeType: a.mimeType || ''
}));
}
// 发送后清空附件列表
chatAttachments = [];
renderChatFileChips();
// 创建进度消息容器(使用详细的进度展示)
const progressId = addProgressMessage();
const progressElement = document.getElementById(progressId);
@@ -145,19 +176,12 @@ async function sendMessage() {
let mcpExecutionIds = [];
try {
// 获取当前选中的角色(从 roles.js 的函数获取)
const roleName = typeof getCurrentRole === 'function' ? getCurrentRole() : '';
const response = await apiFetch('/api/agent-loop/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: message,
conversationId: currentConversationId,
role: roleName || undefined
}),
body: JSON.stringify(body),
});
if (!response.ok) {
@@ -222,6 +246,130 @@ async function sendMessage() {
}
}
// ---------- 对话文件上传 ----------
function renderChatFileChips() {
const list = document.getElementById('chat-file-list');
if (!list) return;
list.innerHTML = '';
if (!chatAttachments.length) return;
chatAttachments.forEach((a, i) => {
const chip = document.createElement('div');
chip.className = 'chat-file-chip';
chip.setAttribute('role', 'listitem');
const name = document.createElement('span');
name.className = 'chat-file-chip-name';
name.title = a.fileName;
name.textContent = a.fileName;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'chat-file-chip-remove';
remove.title = '移除';
remove.innerHTML = '×';
remove.setAttribute('aria-label', '移除 ' + a.fileName);
remove.addEventListener('click', () => removeChatAttachment(i));
chip.appendChild(name);
chip.appendChild(remove);
list.appendChild(chip);
});
}
function removeChatAttachment(index) {
chatAttachments.splice(index, 1);
renderChatFileChips();
}
// 有附件且输入框为空时,填入一句默认提示(可编辑);后端会单独拼接路径与内容给大模型
function appendChatFilePrompt() {
const input = document.getElementById('chat-input');
if (!input || !chatAttachments.length) return;
if (!input.value.trim()) {
input.value = CHAT_FILE_DEFAULT_PROMPT;
adjustTextareaHeight(input);
}
}
function readFileAsAttachment(file) {
return new Promise((resolve, reject) => {
const mimeType = file.type || '';
const isTextLike = /^text\//i.test(mimeType) || /^(application\/(json|xml|javascript)|image\/svg\+xml)/i.test(mimeType);
const reader = new FileReader();
reader.onload = () => {
let content = reader.result;
if (typeof content === 'string' && content.startsWith('data:')) {
content = content.replace(/^data:[^;]+;base64,/, '');
}
resolve({ fileName: file.name, content: content, mimeType: mimeType });
};
reader.onerror = () => reject(reader.error);
if (isTextLike) {
reader.readAsText(file, 'UTF-8');
} else {
reader.readAsDataURL(file);
}
});
}
function addFilesToChat(files) {
if (!files || !files.length) return;
const next = Array.from(files);
if (chatAttachments.length + next.length > MAX_CHAT_FILES) {
alert('最多同时上传 ' + MAX_CHAT_FILES + ' 个文件,当前已选 ' + chatAttachments.length + ' 个。');
return;
}
const addOne = (file) => {
return readFileAsAttachment(file).then((a) => {
chatAttachments.push(a);
renderChatFileChips();
appendChatFilePrompt();
}).catch(() => {
alert('读取文件失败:' + file.name);
});
};
let p = Promise.resolve();
next.forEach((file) => { p = p.then(() => addOne(file)); });
p.then(() => {});
}
function setupChatFileUpload() {
const inputEl = document.getElementById('chat-file-input');
const container = document.getElementById('chat-input-container') || document.querySelector('.chat-input-container');
if (!inputEl || !container) return;
inputEl.addEventListener('change', function () {
const files = this.files;
if (files && files.length) {
addFilesToChat(files);
}
this.value = '';
});
container.addEventListener('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
this.classList.add('drag-over');
});
container.addEventListener('dragleave', function (e) {
e.preventDefault();
e.stopPropagation();
if (!this.contains(e.relatedTarget)) {
this.classList.remove('drag-over');
}
});
container.addEventListener('drop', function (e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('drag-over');
const files = e.dataTransfer && e.dataTransfer.files;
if (files && files.length) addFilesToChat(files);
});
}
// 确保 chat-input-container 有 id(若模板未写)
function ensureChatInputContainerId() {
const c = document.querySelector('.chat-input-container');
if (c && !c.id) c.id = 'chat-input-container';
}
function setupMentionSupport() {
mentionSuggestionsEl = document.getElementById('mention-suggestions');
if (mentionSuggestionsEl) {
@@ -799,6 +947,8 @@ function initializeChatUI() {
}
activeTaskInterval = setInterval(() => loadActiveTasks(), ACTIVE_TASK_REFRESH_INTERVAL);
setupMentionSupport();
ensureChatInputContainerId();
setupChatFileUpload();
}
// 消息计数器,确保ID唯一