Multi-File Upload, Dateinamen in History, kein Auto-Scroll bei Attachment-Klick
This commit is contained in:
1 parent
0b2c9a08c6
commit
663f2c2a9a
7 files changed
+66
-40
No files matched your search
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 = '';
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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 = '';
|
||||||
|
|||||||
Reference in new issue
Block a user