feat: Nachrichten als gelöscht markieren
- DB: deleted_on TEXT Spalte in messages (Migration)
- Backend: ToggleMessageDelete(), includeDeleted-Parameter in allen History-Queries
- Neuer Endpoint: PUT /api/messages/{id}/delete
- Frontend: Trash-Button zwischen Reply- und Mark-Button
- Gelöschte Nachrichten: durchgestrichen + roter Rand (oder versteckt je nach Einstellung)
- Einstellungen: Toggle-Switch 'Gelöschte Nachrichten anzeigen'
- KI-Payload: gelöschte Nachrichten werden nicht mitgeschickt
This commit is contained in:
1 parent
493dfcda1c
commit
a14a991a0f
11 files changed
+248
-39
No files matched your search
@@ -21,6 +21,7 @@ public static class HistoryEndpoint
|
||||
app.MapGet("/api/archive/{archiveId}", HandleGetArchive);
|
||||
app.MapGet("/api/attachments/{messageId}", HandleGetAttachments);
|
||||
app.MapPut("/api/messages/{messageId}/mark", HandleToggleMark);
|
||||
app.MapPut("/api/messages/{messageId}/delete", HandleToggleDelete);
|
||||
app.MapGet("/api/chains/{chainId}", HandleGetChain);
|
||||
}
|
||||
|
||||
@@ -44,23 +45,24 @@ public static class HistoryEndpoint
|
||||
const int messageLimit = 500;
|
||||
var beforeStr = ctx.Request.Query["beforeId"].FirstOrDefault();
|
||||
var beforeId = long.TryParse(beforeStr, out var b) ? b : 0;
|
||||
var includeDeleted = ctx.Request.Query["includeDeleted"].FirstOrDefault() == "true";
|
||||
|
||||
var totalCount = await db.GetActiveMessageCount(sessionId);
|
||||
var totalCount = await db.GetActiveMessageCount(sessionId, includeDeleted);
|
||||
|
||||
List<MessageRecord> messages;
|
||||
if (beforeId > 0)
|
||||
{
|
||||
messages = await db.GetActiveMessagesBeforeId(sessionId, beforeId, messageLimit);
|
||||
messages = await db.GetActiveMessagesBeforeId(sessionId, beforeId, messageLimit, includeDeleted);
|
||||
}
|
||||
else
|
||||
{
|
||||
messages = await db.GetLatestActiveMessages(sessionId, messageLimit);
|
||||
messages = await db.GetLatestActiveMessages(sessionId, messageLimit, includeDeleted);
|
||||
}
|
||||
|
||||
// Zähle wie viele Nachrichten es VOR der ältesten geladenen noch gibt
|
||||
var oldestLoadedId = messages.Count > 0 ? messages[0].Id : 0;
|
||||
var remainingOlder = oldestLoadedId > 0
|
||||
? await db.GetActiveMessageCountBeforeId(sessionId, oldestLoadedId)
|
||||
? await db.GetActiveMessageCountBeforeId(sessionId, oldestLoadedId, includeDeleted)
|
||||
: 0;
|
||||
|
||||
// Nach Chain gruppieren (Reihenfolge beibehalten)
|
||||
@@ -91,6 +93,7 @@ public static class HistoryEndpoint
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var afterStr = ctx.Request.Query["afterId"].FirstOrDefault();
|
||||
var afterId = long.TryParse(afterStr, out var a) ? a : 0;
|
||||
var includeDeleted = ctx.Request.Query["includeDeleted"].FirstOrDefault() == "true";
|
||||
|
||||
if (string.IsNullOrEmpty(sessionId) || !await VerifySessionOwner(ctx, db, sessionId))
|
||||
{
|
||||
@@ -98,7 +101,7 @@ public static class HistoryEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var messages = await db.GetActiveMessagesAfterId(sessionId, afterId);
|
||||
var messages = await db.GetActiveMessagesAfterId(sessionId, afterId, includeDeleted: includeDeleted);
|
||||
await ctx.Response.WriteAsJsonAsync(messages);
|
||||
}
|
||||
|
||||
@@ -174,6 +177,19 @@ public static class HistoryEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(new { messageId, isMarked });
|
||||
}
|
||||
|
||||
private static async Task HandleToggleDelete(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
var messageIdStr = ctx.Request.RouteValues["messageId"]?.ToString();
|
||||
if (!long.TryParse(messageIdStr, out var messageId))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var deletedOn = await db.ToggleMessageDelete(messageId);
|
||||
await ctx.Response.WriteAsJsonAsync(new { messageId, deletedOn });
|
||||
}
|
||||
|
||||
private static async Task HandleGetChain(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
var chainId = ctx.Request.RouteValues["chainId"]?.ToString();
|
||||
|
||||
@@ -14,6 +14,7 @@ public class MessageRecord
|
||||
public int AttachmentCount { get; set; } // Anzahl angehängter Dateien
|
||||
public string AttachmentNames { get; set; } // JSON-Array mit Dateinamen
|
||||
public bool IsMarked { get; set; } // als wichtig markiert
|
||||
public string DeletedOn { get; set; } // null = aktiv, sonst Zeitstempel der Löschung
|
||||
public string CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
|
||||
+46
-15
@@ -146,6 +146,7 @@ public class DatabaseService
|
||||
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
|
||||
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
|
||||
|
||||
TryAddColumn(conn, "messages", "deleted_on", "TEXT");
|
||||
TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0");
|
||||
TryAddColumn(conn, "agents", "web_search_version", "TEXT DEFAULT ''");
|
||||
|
||||
@@ -560,57 +561,62 @@ public class DatabaseService
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<int> GetActiveMessageCount(string sessionId)
|
||||
public async Task<int> GetActiveMessageCount(string sessionId, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.ExecuteScalarAsync<int>(@"
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
return await conn.ExecuteScalarAsync<int>($@"
|
||||
SELECT COUNT(*) FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE c.archive_id IS NULL AND c.session_id = @sessionId",
|
||||
WHERE c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}",
|
||||
new { sessionId });
|
||||
}
|
||||
|
||||
public async Task<List<MessageRecord>> GetLatestActiveMessages(string sessionId, int limit)
|
||||
public async Task<List<MessageRecord>> GetLatestActiveMessages(string sessionId, int limit, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var messages = await conn.QueryAsync<MessageRecord>(@"
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
var messages = await conn.QueryAsync<MessageRecord>($@"
|
||||
SELECT m.* FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE c.archive_id IS NULL AND c.session_id = @sessionId
|
||||
WHERE c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY m.id DESC LIMIT @limit",
|
||||
new { sessionId, limit });
|
||||
return messages.Reverse().ToList();
|
||||
}
|
||||
|
||||
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId)
|
||||
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.ExecuteScalarAsync<int>(@"
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
return await conn.ExecuteScalarAsync<int>($@"
|
||||
SELECT COUNT(*) FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId",
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}",
|
||||
new { sessionId, beforeId });
|
||||
}
|
||||
|
||||
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit)
|
||||
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var messages = await conn.QueryAsync<MessageRecord>(@"
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
var messages = await conn.QueryAsync<MessageRecord>($@"
|
||||
SELECT m.* FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId
|
||||
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY m.id DESC LIMIT @limit",
|
||||
new { sessionId, beforeId, limit });
|
||||
return messages.Reverse().ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200)
|
||||
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200, bool includeDeleted = false)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QueryAsync<MessageRecord>(@"
|
||||
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
|
||||
return await conn.QueryAsync<MessageRecord>($@"
|
||||
SELECT m.* FROM messages m
|
||||
JOIN chains c ON m.chain_id = c.chain_id
|
||||
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId
|
||||
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
|
||||
ORDER BY m.id LIMIT @limit",
|
||||
new { sessionId, afterId, limit });
|
||||
}
|
||||
@@ -628,6 +634,31 @@ public class DatabaseService
|
||||
new { messageId });
|
||||
}
|
||||
|
||||
// ── Message Deletion ──
|
||||
|
||||
public async Task<string> ToggleMessageDelete(long messageId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var current = await conn.ExecuteScalarAsync<string>(
|
||||
"SELECT deleted_on FROM messages WHERE id = @messageId", new { messageId });
|
||||
string newValue;
|
||||
if (current == null)
|
||||
{
|
||||
newValue = DateTime.UtcNow.ToString("o");
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE messages SET deleted_on = @newValue WHERE id = @messageId",
|
||||
new { messageId, newValue });
|
||||
}
|
||||
else
|
||||
{
|
||||
newValue = null;
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE messages SET deleted_on = NULL WHERE id = @messageId",
|
||||
new { messageId });
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MessageRecord>> GetMarkedMessages(string sessionId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
- [x] Agents-Formular (nur beim Bearbeiten von vorhandenen Agent): Button der das File-Explorer Modal öffnet bei dem ich den Agent-Ordner sehe.
|
||||
- [x] Chat: Maximal 500 Nachrichten laden. Dauert sonst zu lange und der Browser wird langsam
|
||||
- [x] User-Management: Tabelle in Datenbank mit username / password. Wenn kein User vorhanden: dann admin mit password "admin" anlegen
|
||||
- [ ] Neuer Message-Tag: `[canvas:<title>:<height (optional)>]...[/canvas]`
|
||||
- [x] Websearch Tool
|
||||
- [x] Anfrage/Stream abbrechen
|
||||
- [x] Telegram-Intergration
|
||||
- [x] Refactoring: Agent/Session-Einstellungen, die den System-Prompt bzw. das Tool Use bearbeiten
|
||||
- [ ] Cronjobs: Planung, ob cron von Debian/Linux verwendet werden soll, oder ein eigener Worker/Service in der App selbst, der das übernimmt
|
||||
- [x] Weiteres Modell hinzufügen: "Haiku 4.5"
|
||||
- [ ] Weitere Tools: Glob? Search?
|
||||
- [ ] Weitere Tools: Glob? Search? Todo List?
|
||||
|
||||
## Bugs
|
||||
- [x] Wenn gerade gestreamt wird und man refreshed die Seite, dann verliert das Frontend den Status "Streaming..." und ich kann eine Nachricht schreiben, obwohl das nicht möglich sein sollte
|
||||
@@ -33,7 +32,15 @@
|
||||
- [x] File Explorer: CodeMirror lässt sich horizontal nicht scroll
|
||||
- [ ] Chat: Datum und Uhrzeit bei den Nachrichten anzeigen (Format: dd.MM.yyyy HH:mm:ss)
|
||||
- [x] Dashboard: Menu-Dropdown bei Cards sind abgeschnitten wenn die Card-Höhe nicht ausreicht (logisch, weil das Menu-Dropdown ein Child von der Card ist)
|
||||
- [ ] Auto-scrolling deaktivieren, wenn man weiterhoch gescrollt hat, und er wieder fortsetzen wenn man ganz nach unten gescrollt hat
|
||||
|
||||
## Ideas / Backlog
|
||||
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
|
||||
- [ ] Subagents: Möglichkeit Subagents zu starten und zu orchestrieren
|
||||
|
||||
## Features aus Devy übernehmen
|
||||
- [ ] 💬 Typing-Indicator - Animierter Indikator statt statischem "denkt nach..."
|
||||
- [ ] 🎨 Chat-Hintergründe - Benutzerdefinierte Hintergründe pro Session
|
||||
- [ ] 👤 User-Management - Admin-only: Benutzer CRUD, Login-as-User, Default-Pfade
|
||||
- [ ] 🖥️ Windows Desktop App - WPF + WebView2 Wrapper mit konfigurierbarer URL
|
||||
- [ ] 🎨 Canvas - HTML/CSS/JS inline rendern in sandboxed iframes mit Resize und Source-View (Message-Tag: `[canvas:<title>:<height (optional)>]...[/canvas]`)
|
||||
@@ -280,6 +280,7 @@
|
||||
}
|
||||
|
||||
.msg-reply-btn,
|
||||
.msg-delete-btn,
|
||||
.msg-mark-btn {
|
||||
position: absolute;
|
||||
top: var(--badge-spacing);
|
||||
@@ -294,6 +295,10 @@
|
||||
}
|
||||
|
||||
.msg-reply-btn {
|
||||
right: calc(var(--badge-spacing) + 4em + var(--badge-spacing) * 2);
|
||||
}
|
||||
|
||||
.msg-delete-btn {
|
||||
right: calc(var(--badge-spacing) + 2em + var(--badge-spacing));
|
||||
}
|
||||
|
||||
@@ -302,11 +307,13 @@
|
||||
}
|
||||
|
||||
.msg:hover .msg-reply-btn,
|
||||
.msg:hover .msg-delete-btn,
|
||||
.msg:hover .msg-mark-btn {
|
||||
opacity: var(--semi-transparent);
|
||||
}
|
||||
|
||||
.msg-reply-btn:hover,
|
||||
.msg-delete-btn:hover,
|
||||
.msg-mark-btn:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
@@ -316,12 +323,30 @@
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.msg-delete-btn.deleted {
|
||||
opacity: 1 !important;
|
||||
color: var(--dangerous);
|
||||
}
|
||||
|
||||
/* Markierte Nachrichten */
|
||||
|
||||
.msg.marked {
|
||||
border-left: 3px solid var(--warning);
|
||||
}
|
||||
|
||||
/* Gelöschte Nachrichten */
|
||||
|
||||
.msg.msg-deleted {
|
||||
opacity: 0.45;
|
||||
text-decoration: line-through;
|
||||
text-decoration-color: var(--dangerous);
|
||||
border-left: 3px solid var(--dangerous);
|
||||
}
|
||||
|
||||
.msg.msg-deleted-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Reply Quote in User Bubble */
|
||||
|
||||
.reply-quote {
|
||||
|
||||
@@ -228,3 +228,48 @@ label.skill-item {
|
||||
background: var(--dangerous);
|
||||
color: var(--f1-bg);
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2.4em;
|
||||
height: 1.4em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--f2-border);
|
||||
border-radius: 1em;
|
||||
transition: background var(--transition-duration);
|
||||
}
|
||||
|
||||
.toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
left: .2em;
|
||||
top: .2em;
|
||||
background: var(--f1-bg);
|
||||
border-radius: 50%;
|
||||
transition: transform var(--transition-duration);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider::before {
|
||||
transform: translateX(1em);
|
||||
}
|
||||
|
||||
@@ -273,6 +273,18 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>Gelöschte Nachrichten</label>
|
||||
<span class="setting-hint">Gelöschte Nachrichten durchgestrichen anzeigen (werden nicht an die KI gesendet)</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="settingShowDeletedMessages">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="settingsBackupSection" class="settings-section" style="display:none">
|
||||
<h2>Daten</h2>
|
||||
|
||||
+11
-3
@@ -85,15 +85,18 @@ async function apiDeleteAgent(agentId) {
|
||||
return fetch(`/api/agents/${agentId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
async function apiHistory(sessionId, beforeId) {
|
||||
async function apiHistory(sessionId, beforeId, includeDeleted = false) {
|
||||
let url = `/api/history?sessionId=${sessionId}`;
|
||||
if (beforeId) url += `&beforeId=${beforeId}`;
|
||||
if (includeDeleted) url += `&includeDeleted=true`;
|
||||
const resp = await fetch(url);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiHistoryPoll(sessionId, afterId) {
|
||||
const resp = await fetch(`/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`);
|
||||
async function apiHistoryPoll(sessionId, afterId, includeDeleted = false) {
|
||||
let url = `/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`;
|
||||
if (includeDeleted) url += `&includeDeleted=true`;
|
||||
const resp = await fetch(url);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
@@ -186,6 +189,11 @@ async function apiToggleMessageMark(messageId) {
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiToggleMessageDelete(messageId) {
|
||||
const resp = await fetch(`/api/messages/${messageId}/delete`, { method: 'PUT' });
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiGitStatus(sessionId) {
|
||||
const resp = await fetch(`/api/sessions/${sessionId}/git-status`);
|
||||
if (!resp.ok) return null;
|
||||
|
||||
+11
-7
@@ -10,7 +10,7 @@ async function loadHistory() {
|
||||
try {
|
||||
const sid = getCurrentSessionId();
|
||||
|
||||
const data = await apiHistory(sid);
|
||||
const data = await apiHistory(sid, null, isDeletedVisible());
|
||||
const entries = data.entries || [];
|
||||
|
||||
// Älteste geladene Message-ID merken für Nachladen
|
||||
@@ -51,7 +51,7 @@ async function loadOlderMessages() {
|
||||
|
||||
try {
|
||||
const sid = getCurrentSessionId();
|
||||
const data = await apiHistory(sid, _oldestLoadedMessageId);
|
||||
const data = await apiHistory(sid, _oldestLoadedMessageId, isDeletedVisible());
|
||||
const entries = data.entries || [];
|
||||
|
||||
_hasMoreOlderMessages = data.hasMore;
|
||||
@@ -146,11 +146,13 @@ function _renderMessagesIntoFragment(fragment, msgs) {
|
||||
case 'text':
|
||||
if (msg.role === 'user') {
|
||||
div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
renderUserContent(div, msg.content);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
fragment.appendChild(div);
|
||||
if (msg.attachmentCount > 0) {
|
||||
@@ -167,12 +169,14 @@ function _renderMessagesIntoFragment(fragment, msgs) {
|
||||
}
|
||||
} else if (msg.role === 'assistant') {
|
||||
div.className = 'msg assistant markdown' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
fragment.appendChild(div);
|
||||
}
|
||||
@@ -215,12 +219,12 @@ function renderMessages(msgs, addToConversation) {
|
||||
switch (msg.type) {
|
||||
case 'text':
|
||||
if (msg.role === 'user') {
|
||||
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked);
|
||||
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
if (msg.attachmentCount > 0) addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames);
|
||||
if (addToConversation) messages.push({ role: 'user', content: msg.content });
|
||||
if (addToConversation && !msg.deletedOn) messages.push({ role: 'user', content: msg.content });
|
||||
} else if (msg.role === 'assistant') {
|
||||
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked);
|
||||
if (addToConversation) messages.push({ role: 'assistant', content: msg.content });
|
||||
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
if (addToConversation && !msg.deletedOn) messages.push({ role: 'assistant', content: msg.content });
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -388,7 +392,7 @@ async function pollHistory(force = false) {
|
||||
_pollLock = true;
|
||||
|
||||
try {
|
||||
const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId);
|
||||
const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId, isDeletedVisible());
|
||||
if (newMessages.length > 0) {
|
||||
renderMessages(newMessages, true);
|
||||
scrollDown();
|
||||
|
||||
@@ -5,7 +5,8 @@ const SETTINGS_KEY = 'settings';
|
||||
const SETTINGS_DEFAULTS = {
|
||||
pageWidth: 900,
|
||||
toolCallDisplay: 'modifications', // 'all' | 'modifications' | 'hidden'
|
||||
chainMetaDisplay: 'sometimes' // 'always' | 'sometimes' | 'never'
|
||||
chainMetaDisplay: 'sometimes', // 'always' | 'sometimes' | 'never'
|
||||
showDeletedMessages: false
|
||||
};
|
||||
|
||||
function loadSettings() {
|
||||
@@ -38,6 +39,7 @@ registerPage('settings', {
|
||||
const display = document.getElementById('settingPageWidthValue');
|
||||
const toolCallSelect = document.getElementById('settingToolCallDisplay');
|
||||
const chainMetaSelect = document.getElementById('settingChainMetaDisplay');
|
||||
const showDeletedToggle = document.getElementById('settingShowDeletedMessages');
|
||||
const resetBtn = document.getElementById('settingsResetBtn');
|
||||
const backupSection = document.getElementById('settingsBackupSection');
|
||||
const telegramSection = document.getElementById('settingsTelegramSection');
|
||||
@@ -53,6 +55,7 @@ registerPage('settings', {
|
||||
display.textContent = s.pageWidth + 'px';
|
||||
toolCallSelect.value = s.toolCallDisplay;
|
||||
chainMetaSelect.value = s.chainMetaDisplay;
|
||||
showDeletedToggle.checked = s.showDeletedMessages;
|
||||
|
||||
slider.addEventListener('input', () => {
|
||||
display.textContent = slider.value + 'px';
|
||||
@@ -77,6 +80,12 @@ registerPage('settings', {
|
||||
saveSettings(s);
|
||||
});
|
||||
|
||||
showDeletedToggle.addEventListener('change', () => {
|
||||
const s = loadSettings();
|
||||
s.showDeletedMessages = showDeletedToggle.checked;
|
||||
saveSettings(s);
|
||||
});
|
||||
|
||||
const backupBtn = document.getElementById('settingsBackupBtn');
|
||||
if (backupBtn) backupBtn.addEventListener('click', async () => {
|
||||
backupBtn.disabled = true;
|
||||
@@ -100,6 +109,7 @@ registerPage('settings', {
|
||||
display.textContent = SETTINGS_DEFAULTS.pageWidth + 'px';
|
||||
toolCallSelect.value = SETTINGS_DEFAULTS.toolCallDisplay;
|
||||
chainMetaSelect.value = SETTINGS_DEFAULTS.chainMetaDisplay;
|
||||
showDeletedToggle.checked = SETTINGS_DEFAULTS.showDeletedMessages;
|
||||
});
|
||||
|
||||
function initTelegramSection() {
|
||||
|
||||
+56
-6
@@ -115,6 +115,46 @@ function truncate(s, max) {
|
||||
return s && s.length > max ? s.substring(0, max) + '...' : (s || '');
|
||||
}
|
||||
|
||||
// ── Delete Button ──
|
||||
|
||||
function _addDeleteButton(div, messageId, deletedOn) {
|
||||
if (!messageId) return;
|
||||
const isDeleted = !!deletedOn;
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'msg-delete-btn icon-btn' + (isDeleted ? ' deleted' : '');
|
||||
btn.innerHTML = '<i class="fa fa-trash-o"></i>';
|
||||
btn.title = isDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
const result = await apiToggleMessageDelete(messageId);
|
||||
const nowDeleted = !!result.deletedOn;
|
||||
btn.classList.toggle('deleted', nowDeleted);
|
||||
btn.title = nowDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
|
||||
if (nowDeleted && !isDeletedVisible()) {
|
||||
// Nachricht ausblenden
|
||||
div.classList.add('msg-deleted-hidden');
|
||||
} else {
|
||||
div.classList.remove('msg-deleted-hidden');
|
||||
div.classList.toggle('msg-deleted', nowDeleted);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Delete toggle failed:', err);
|
||||
}
|
||||
});
|
||||
// Nach dem Reply-Button einfügen
|
||||
const replyBtn = div.querySelector('.msg-reply-btn');
|
||||
if (replyBtn) {
|
||||
replyBtn.after(btn);
|
||||
} else {
|
||||
div.prepend(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function isDeletedVisible() {
|
||||
return loadSettings().showDeletedMessages === true;
|
||||
}
|
||||
|
||||
// ── Mark Button ──
|
||||
|
||||
function _addMarkButton(div, messageId, isMarked) {
|
||||
@@ -134,11 +174,17 @@ function _addMarkButton(div, messageId, isMarked) {
|
||||
console.error('Mark toggle failed:', err);
|
||||
}
|
||||
});
|
||||
const replyBtn = div.querySelector('.msg-reply-btn');
|
||||
if (replyBtn) {
|
||||
replyBtn.after(btn);
|
||||
// Nach dem Delete-Button einfügen (oder nach Reply-Button, oder am Anfang)
|
||||
const deleteBtn = div.querySelector('.msg-delete-btn');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.after(btn);
|
||||
} else {
|
||||
div.prepend(btn);
|
||||
const replyBtn = div.querySelector('.msg-reply-btn');
|
||||
if (replyBtn) {
|
||||
replyBtn.after(btn);
|
||||
} else {
|
||||
div.prepend(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,23 +284,26 @@ function _renderUserTextContent(container, raw) {
|
||||
parseFluentEmojis(wrap);
|
||||
}
|
||||
|
||||
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false) {
|
||||
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false, deletedOn = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg user markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
if (streamed) div.classList.add('streamed');
|
||||
renderUserContent(div, text);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false) {
|
||||
function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false, deletedOn = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg assistant markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
@@ -262,6 +311,7 @@ function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = f
|
||||
parseFluentEmojis(div);
|
||||
wrapKreaGalleries(div);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
|
||||
Reference in new issue
Block a user