From 663f2c2a9a4b2de44a1a2a55c07eda737dd2bd73 Mon Sep 17 00:00:00 2001 From: Meleki Date: Fri, 27 Feb 2026 08:28:59 +0100 Subject: [PATCH] Multi-File Upload, Dateinamen in History, kein Auto-Scroll bei Attachment-Klick --- Models/MessageRecord.cs | 1 + Services/DatabaseService.cs | 11 +++++- wwwroot/index.html | 2 +- wwwroot/js/chat.js | 73 +++++++++++++++++++++-------------- wwwroot/js/history.js | 2 +- wwwroot/js/pages/chat-page.js | 6 +-- wwwroot/js/ui.js | 11 ++++-- 7 files changed, 66 insertions(+), 40 deletions(-) diff --git a/Models/MessageRecord.cs b/Models/MessageRecord.cs index 93464bb..0f153d8 100644 --- a/Models/MessageRecord.cs +++ b/Models/MessageRecord.cs @@ -12,6 +12,7 @@ public class MessageRecord public bool IsError { get; set; } public int Iteration { get; set; } // welche Loop-Iteration public int AttachmentCount { get; set; } // Anzahl angehΓ€ngter Dateien + public string AttachmentNames { get; set; } // JSON-Array mit Dateinamen public string CreatedAt { get; set; } } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index ca5cdfc..37720e3 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Dapper; using Gott.Models; @@ -121,6 +122,7 @@ public class DatabaseService TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0"); TryAddColumn(conn, "chains", "agent_id", "TEXT"); TryAddColumn(conn, "messages", "attachment_count", "INTEGER NOT NULL DEFAULT 0"); + TryAddColumn(conn, "messages", "attachment_names", "TEXT"); } private void TryAddColumn(SqliteConnection conn, string table, string column, string type) @@ -370,9 +372,14 @@ public class DatabaseService record.CreatedAt = DateTime.UtcNow.ToString("o"); record.AttachmentCount = attachments?.Count ?? 0; + if (attachments != null && attachments.Count > 0) + { + var names = attachments.Select(a => a.FileName ?? "").ToArray(); + record.AttachmentNames = System.Text.Json.JsonSerializer.Serialize(names); + } var messageId = await conn.ExecuteScalarAsync(@" - INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, attachment_count, created_at) - VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @AttachmentCount, @CreatedAt); + INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, attachment_count, attachment_names, created_at) + VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @AttachmentCount, @AttachmentNames, @CreatedAt); SELECT last_insert_rowid(); ", record, tx); diff --git a/wwwroot/index.html b/wwwroot/index.html index 9486eb7..ef9f303 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -124,7 +124,7 @@
- + diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index 816da83..ba9bb17 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -2,7 +2,7 @@ const messages = []; let isStreaming = false; -let pendingFile = null; // { name, type, base64 } +let pendingFiles = []; // [{ name, type, base64 }] async function clearChat() { const archive = await apiArchive(getCurrentSessionId()); @@ -23,23 +23,27 @@ async function sendMessage() { const input = $('#input'); const btn = $('#send'); const text = input.value.trim(); - if (!text && !pendingFile) return; + if (!text && pendingFiles.length === 0) return; // Content bauen: string oder array mit file-blocks let content; let bubbleText = text; - if (pendingFile) { + if (pendingFiles.length > 0) { content = []; - const blockType = pendingFile.type.startsWith('image/') ? 'image' : 'document'; - content.push({ - type: blockType, - source: { type: 'base64', media_type: pendingFile.type, data: pendingFile.base64 }, - _file_name: pendingFile.name - }); + const labels = []; + for (const f of pendingFiles) { + const blockType = f.type.startsWith('image/') ? 'image' : 'document'; + content.push({ + type: blockType, + source: { type: 'base64', media_type: f.type, data: f.base64 }, + _file_name: f.name + }); + labels.push((blockType === 'image' ? 'πŸ“· ' : 'πŸ“„ ') + f.name); + } if (text) content.push({ type: 'text', text }); - bubbleText = (blockType === 'image' ? 'πŸ“· ' : 'πŸ“„ ') + pendingFile.name + (text ? '\n' + text : ''); - clearPendingFile(); + bubbleText = labels.join('\n') + (text ? '\n' + text : ''); + clearPendingFiles(); } else { content = text; } @@ -160,34 +164,43 @@ function handleFileSelect(file) { const reader = new FileReader(); reader.onload = () => { const base64 = reader.result.split(',')[1]; - pendingFile = { name: file.name, type: file.type, base64 }; - showFilePreview(); + pendingFiles.push({ name: file.name, type: file.type, base64 }); + renderFilePreview(); }; reader.readAsDataURL(file); } -function showFilePreview() { - const preview = $('#filePreview'); - if (!preview || !pendingFile) return; +function handleMultipleFiles(files) { + for (const file of files) handleFileSelect(file); +} - const isImage = pendingFile.type.startsWith('image/'); - preview.innerHTML = ` -
- ${isImage ? '' : 'πŸ“„'} - ${esc(pendingFile.name)} - -
- `; +function renderFilePreview() { + const preview = $('#filePreview'); + if (!preview || pendingFiles.length === 0) { + if (preview) { preview.innerHTML = ''; preview.hidden = true; } + return; + } + + preview.innerHTML = pendingFiles.map((f, i) => { + const isImage = f.type.startsWith('image/'); + return `
+ ${isImage ? '' : 'πŸ“„'} + ${esc(f.name)} + +
`; + }).join(''); preview.hidden = false; } -function clearPendingFile() { - pendingFile = null; +function removePendingFile(index) { + pendingFiles.splice(index, 1); + renderFilePreview(); +} + +function clearPendingFiles() { + pendingFiles.length = 0; const preview = $('#filePreview'); - if (preview) { - preview.innerHTML = ''; - preview.hidden = true; - } + if (preview) { preview.innerHTML = ''; preview.hidden = true; } const fileInput = $('#fileInput'); if (fileInput) fileInput.value = ''; } diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index b4bc712..300aa39 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -34,7 +34,7 @@ function renderMessages(msgs, addToConversation) { case 'text': if (msg.role === 'user') { addUserBubble(msg.content); - if (msg.attachmentCount > 0) addAttachmentIndicator(msg.id, msg.attachmentCount); + if (msg.attachmentCount > 0) addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames); if (addToConversation) messages.push({ role: 'user', content: msg.content }); } else if (msg.role === 'assistant') { addAssistantBubble(msg.content); diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index bacff76..d93b615 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -49,7 +49,7 @@ registerPage('chat', { if (fileBtn && fileInput) { fileBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', (e) => { - if (e.target.files[0]) handleFileSelect(e.target.files[0]); + if (e.target.files.length > 0) handleMultipleFiles(e.target.files); }); } @@ -60,7 +60,7 @@ registerPage('chat', { chatContainer.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); - if (e.dataTransfer.files[0]) handleFileSelect(e.dataTransfer.files[0]); + if (e.dataTransfer.files.length > 0) handleMultipleFiles(e.dataTransfer.files); }); } @@ -98,6 +98,6 @@ registerPage('chat', { lastKnownMessageId = 0; loadedArchiveCount = 0; isStreaming = false; - clearPendingFile(); + clearPendingFiles(); } }); diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 0af7e22..51d2614 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -124,15 +124,21 @@ function removeStreamedElements() { // ── Attachment Indicator ── -function addAttachmentIndicator(messageId, count) { +function addAttachmentIndicator(messageId, count, namesJson) { const chat = chatEl(); const lastMsg = chat.lastElementChild; if (!lastMsg) return; + let names = []; + try { if (namesJson) names = JSON.parse(namesJson); } catch {} + const indicator = document.createElement('div'); indicator.className = 'attachment-indicator'; - indicator.textContent = `πŸ“Ž ${count} Datei${count > 1 ? 'en' : ''}`; + + const nameList = names.filter(n => n).join(', '); + indicator.textContent = `πŸ“Ž ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${nameList}` : ''); indicator.style.cursor = 'pointer'; + indicator.title = 'Klicken zum Anzeigen'; indicator.addEventListener('click', () => loadAndShowAttachments(messageId, indicator)); lastMsg.appendChild(indicator); } @@ -168,7 +174,6 @@ async function loadAndShowAttachments(messageId, indicator) { indicator.textContent = `πŸ“Ž ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`; indicator.parentElement.appendChild(container); - scrollDown(); } catch (e) { indicator.textContent = 'πŸ“Ž Fehler beim Laden'; indicator.dataset.loaded = '';