Multi-File Upload, Dateinamen in History, kein Auto-Scroll bei Attachment-Klick

This commit is contained in:
Meleki committed 2026-02-27 08:28:59 +01:00
1 parent 0b2c9a08c6
commit 663f2c2a9a
7 files changed
+66 -40

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ public class MessageRecord
public bool IsError { get; set; } public bool IsError { get; set; }
public int Iteration { get; set; } // welche Loop-Iteration public int Iteration { get; set; } // welche Loop-Iteration
public int AttachmentCount { get; set; } // Anzahl angehängter Dateien public int AttachmentCount { get; set; } // Anzahl angehängter Dateien
public string AttachmentNames { get; set; } // JSON-Array mit Dateinamen
public string CreatedAt { get; set; } public string CreatedAt { get; set; }
} }
+9 -2
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper; using Dapper;
using Gott.Models; using Gott.Models;
@@ -121,6 +122,7 @@ public class DatabaseService
TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0"); TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "chains", "agent_id", "TEXT"); TryAddColumn(conn, "chains", "agent_id", "TEXT");
TryAddColumn(conn, "messages", "attachment_count", "INTEGER NOT NULL DEFAULT 0"); 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) 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.CreatedAt = DateTime.UtcNow.ToString("o");
record.AttachmentCount = attachments?.Count ?? 0; 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<long>(@" var messageId = await conn.ExecuteScalarAsync<long>(@"
INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, attachment_count, created_at) 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, @CreatedAt); VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @AttachmentCount, @AttachmentNames, @CreatedAt);
SELECT last_insert_rowid(); SELECT last_insert_rowid();
", record, tx); ", record, tx);
+1 -1
View File
@@ -124,7 +124,7 @@
<div id="chat"></div> <div id="chat"></div>
<div id="filePreview" class="file-preview" hidden></div> <div id="filePreview" class="file-preview" hidden></div>
<div class="input-area"> <div class="input-area">
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf" hidden> <input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf" multiple hidden>
<button id="fileBtn" class="file-btn" title="Datei anhängen">📎</button> <button id="fileBtn" class="file-btn" title="Datei anhängen">📎</button>
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter)" rows="2"></textarea> <textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter)" rows="2"></textarea>
<button id="send">Senden</button> <button id="send">Senden</button>
+43 -30
View File
@@ -2,7 +2,7 @@
const messages = []; const messages = [];
let isStreaming = false; let isStreaming = false;
let pendingFile = null; // { name, type, base64 } let pendingFiles = []; // [{ name, type, base64 }]
async function clearChat() { async function clearChat() {
const archive = await apiArchive(getCurrentSessionId()); const archive = await apiArchive(getCurrentSessionId());
@@ -23,23 +23,27 @@ async function sendMessage() {
const input = $('#input'); const input = $('#input');
const btn = $('#send'); const btn = $('#send');
const text = input.value.trim(); const text = input.value.trim();
if (!text && !pendingFile) return; if (!text && pendingFiles.length === 0) return;
// Content bauen: string oder array mit file-blocks // Content bauen: string oder array mit file-blocks
let content; let content;
let bubbleText = text; let bubbleText = text;
if (pendingFile) { if (pendingFiles.length > 0) {
content = []; content = [];
const blockType = pendingFile.type.startsWith('image/') ? 'image' : 'document'; const labels = [];
content.push({ for (const f of pendingFiles) {
type: blockType, const blockType = f.type.startsWith('image/') ? 'image' : 'document';
source: { type: 'base64', media_type: pendingFile.type, data: pendingFile.base64 }, content.push({
_file_name: pendingFile.name 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 }); if (text) content.push({ type: 'text', text });
bubbleText = (blockType === 'image' ? '📷 ' : '📄 ') + pendingFile.name + (text ? '\n' + text : ''); bubbleText = labels.join('\n') + (text ? '\n' + text : '');
clearPendingFile(); clearPendingFiles();
} else { } else {
content = text; content = text;
} }
@@ -160,34 +164,43 @@ function handleFileSelect(file) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
const base64 = reader.result.split(',')[1]; const base64 = reader.result.split(',')[1];
pendingFile = { name: file.name, type: file.type, base64 }; pendingFiles.push({ name: file.name, type: file.type, base64 });
showFilePreview(); renderFilePreview();
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
function showFilePreview() { function handleMultipleFiles(files) {
const preview = $('#filePreview'); for (const file of files) handleFileSelect(file);
if (!preview || !pendingFile) return; }
const isImage = pendingFile.type.startsWith('image/'); function renderFilePreview() {
preview.innerHTML = ` const preview = $('#filePreview');
<div class="file-preview-inner"> if (!preview || pendingFiles.length === 0) {
${isImage ? '<img src="data:' + pendingFile.type + ';base64,' + pendingFile.base64 + '" class="file-thumb">' : '<span class="file-icon">📄</span>'} if (preview) { preview.innerHTML = ''; preview.hidden = true; }
<span class="file-name">${esc(pendingFile.name)}</span> return;
<button class="file-remove" onclick="clearPendingFile()">✕</button> }
</div>
`; preview.innerHTML = pendingFiles.map((f, i) => {
const isImage = f.type.startsWith('image/');
return `<div class="file-preview-inner">
${isImage ? '<img src="data:' + f.type + ';base64,' + f.base64 + '" class="file-thumb">' : '<span class="file-icon">📄</span>'}
<span class="file-name">${esc(f.name)}</span>
<button class="file-remove" onclick="removePendingFile(${i})">✕</button>
</div>`;
}).join('');
preview.hidden = false; preview.hidden = false;
} }
function clearPendingFile() { function removePendingFile(index) {
pendingFile = null; pendingFiles.splice(index, 1);
renderFilePreview();
}
function clearPendingFiles() {
pendingFiles.length = 0;
const preview = $('#filePreview'); const preview = $('#filePreview');
if (preview) { if (preview) { preview.innerHTML = ''; preview.hidden = true; }
preview.innerHTML = '';
preview.hidden = true;
}
const fileInput = $('#fileInput'); const fileInput = $('#fileInput');
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
} }
+1 -1
View File
@@ -34,7 +34,7 @@ function renderMessages(msgs, addToConversation) {
case 'text': case 'text':
if (msg.role === 'user') { if (msg.role === 'user') {
addUserBubble(msg.content); 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 }); if (addToConversation) messages.push({ role: 'user', content: msg.content });
} else if (msg.role === 'assistant') { } else if (msg.role === 'assistant') {
addAssistantBubble(msg.content); addAssistantBubble(msg.content);
+3 -3
View File
@@ -49,7 +49,7 @@ registerPage('chat', {
if (fileBtn && fileInput) { if (fileBtn && fileInput) {
fileBtn.addEventListener('click', () => fileInput.click()); fileBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => { 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) => { chatContainer.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); 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; lastKnownMessageId = 0;
loadedArchiveCount = 0; loadedArchiveCount = 0;
isStreaming = false; isStreaming = false;
clearPendingFile(); clearPendingFiles();
} }
}); });
+8 -3
View File
@@ -124,15 +124,21 @@ function removeStreamedElements() {
// ── Attachment Indicator ── // ── Attachment Indicator ──
function addAttachmentIndicator(messageId, count) { function addAttachmentIndicator(messageId, count, namesJson) {
const chat = chatEl(); const chat = chatEl();
const lastMsg = chat.lastElementChild; const lastMsg = chat.lastElementChild;
if (!lastMsg) return; if (!lastMsg) return;
let names = [];
try { if (namesJson) names = JSON.parse(namesJson); } catch {}
const indicator = document.createElement('div'); const indicator = document.createElement('div');
indicator.className = 'attachment-indicator'; 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.style.cursor = 'pointer';
indicator.title = 'Klicken zum Anzeigen';
indicator.addEventListener('click', () => loadAndShowAttachments(messageId, indicator)); indicator.addEventListener('click', () => loadAndShowAttachments(messageId, indicator));
lastMsg.appendChild(indicator); lastMsg.appendChild(indicator);
} }
@@ -168,7 +174,6 @@ async function loadAndShowAttachments(messageId, indicator) {
indicator.textContent = `📎 ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`; indicator.textContent = `📎 ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`;
indicator.parentElement.appendChild(container); indicator.parentElement.appendChild(container);
scrollDown();
} catch (e) { } catch (e) {
indicator.textContent = '📎 Fehler beim Laden'; indicator.textContent = '📎 Fehler beim Laden';
indicator.dataset.loaded = ''; indicator.dataset.loaded = '';