Sessions: Sidebar, Session-Wechsel, Archivieren-Button, alles session-bezogen

This commit is contained in:
Meleki committed 2026-02-26 18:37:24 +01:00
1 parent 8ddee38032
commit 25f07caf83
15 files changed
+442 -78

No files matched your search

+12 -1
View File
@@ -50,16 +50,27 @@ public static class ChatEndpoint
res.Headers["Cache-Control"] = "no-cache"; res.Headers["Cache-Control"] = "no-cache";
res.Headers["Connection"] = "keep-alive"; res.Headers["Connection"] = "keep-alive";
var sessionId = body.SessionId;
if (string.IsNullOrEmpty(sessionId))
{
res.StatusCode = 400;
await res.WriteAsJsonAsync(new { error = "sessionId required" });
return;
}
var chainId = Guid.NewGuid().ToString("N"); var chainId = Guid.NewGuid().ToString("N");
// Chain zuerst anlegen (FK für messages)
await db.CreateChain(new ChainRecord await db.CreateChain(new ChainRecord
{ {
ChainId = chainId, ChainId = chainId,
SessionId = sessionId,
Model = modelId, Model = modelId,
SystemPrompt = body.SystemPrompt SystemPrompt = body.SystemPrompt
}); });
// Session-Timestamp aktualisieren
await db.UpdateSessionTimestamp(sessionId);
var messages = new List<object>(); var messages = new List<object>();
foreach (var msg in body.Messages) foreach (var msg in body.Messages)
messages.Add(new { role = msg.Role, content = msg.Content }); messages.Add(new { role = msg.Role, content = msg.Content });
+31 -8
View File
@@ -23,12 +23,14 @@ public static class HistoryEndpoint
private static async Task HandleHistory(HttpContext ctx, DatabaseService db) private static async Task HandleHistory(HttpContext ctx, DatabaseService db)
{ {
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault(); var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
await ctx.Response.WriteAsJsonAsync(new { error = "sessionId required" });
return;
}
IEnumerable<ChainRecord> chains; var chains = await db.GetActiveChains(sessionId);
if (string.IsNullOrEmpty(sessionId) || sessionId == "null")
chains = await db.GetActiveChains();
else
chains = await db.GetSessionChains(sessionId);
var result = new List<object>(); var result = new List<object>();
foreach (var chain in chains) foreach (var chain in chains)
@@ -42,16 +44,30 @@ public static class HistoryEndpoint
private static async Task HandlePoll(HttpContext ctx, DatabaseService db) private static async Task HandlePoll(HttpContext ctx, DatabaseService db)
{ {
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
var afterStr = ctx.Request.Query["afterId"].FirstOrDefault(); var afterStr = ctx.Request.Query["afterId"].FirstOrDefault();
var afterId = long.TryParse(afterStr, out var a) ? a : 0; var afterId = long.TryParse(afterStr, out var a) ? a : 0;
var messages = await db.GetActiveMessagesAfterId(afterId); if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var messages = await db.GetActiveMessagesAfterId(sessionId, afterId);
await ctx.Response.WriteAsJsonAsync(messages); await ctx.Response.WriteAsJsonAsync(messages);
} }
private static async Task HandleArchive(HttpContext ctx, DatabaseService db) private static async Task HandleArchive(HttpContext ctx, DatabaseService db)
{ {
var archive = await db.CreateArchive(); var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var archive = await db.CreateArchive(sessionId);
if (archive == null) if (archive == null)
{ {
ctx.Response.StatusCode = 204; ctx.Response.StatusCode = 204;
@@ -62,7 +78,14 @@ public static class HistoryEndpoint
private static async Task HandleListArchives(HttpContext ctx, DatabaseService db) private static async Task HandleListArchives(HttpContext ctx, DatabaseService db)
{ {
var archives = await db.GetArchives(); var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var archives = await db.GetArchives(sessionId);
await ctx.Response.WriteAsJsonAsync(archives); await ctx.Response.WriteAsJsonAsync(archives);
} }
+50
View File
@@ -0,0 +1,50 @@
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Gott.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Gott.Endpoints;
public static class SessionEndpoint
{
public static void MapSessionEndpoints(this IEndpointRouteBuilder app)
{
app.MapGet("/api/sessions", HandleList);
app.MapPost("/api/sessions", HandleCreate);
app.MapPut("/api/sessions/{sessionId}", HandleUpdate);
}
private static async Task HandleList(HttpContext ctx, DatabaseService db)
{
var sessions = await db.GetSessions();
await ctx.Response.WriteAsJsonAsync(sessions);
}
private static async Task HandleCreate(HttpContext ctx, DatabaseService db)
{
var session = await db.CreateSession("Neue Session");
await ctx.Response.WriteAsJsonAsync(session);
}
private static async Task HandleUpdate(HttpContext ctx, DatabaseService db)
{
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
if (string.IsNullOrEmpty(sessionId))
{
ctx.Response.StatusCode = 400;
return;
}
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
if (body.TryGetProperty("name", out var nameEl))
{
await db.UpdateSessionName(sessionId, nameEl.GetString());
}
var session = await db.GetSession(sessionId);
await ctx.Response.WriteAsJsonAsync(session);
}
}
+1
View File
@@ -4,6 +4,7 @@ public class ArchiveRecord
{ {
public long Id { get; set; } public long Id { get; set; }
public string ArchiveId { get; set; } public string ArchiveId { get; set; }
public string SessionId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public int ChainCount { get; set; } public int ChainCount { get; set; }
public int MessageCount { get; set; } public int MessageCount { get; set; }
+3
View File
@@ -22,4 +22,7 @@ public class ChatRequest
[JsonPropertyName("systemPrompt")] [JsonPropertyName("systemPrompt")]
public string SystemPrompt { get; set; } = ""; public string SystemPrompt { get; set; } = "";
[JsonPropertyName("sessionId")]
public string SessionId { get; set; } = "";
} }
+10
View File
@@ -0,0 +1,10 @@
namespace Gott.Models;
public class SessionRecord
{
public long Id { get; set; }
public string SessionId { get; set; }
public string Name { get; set; }
public string CreatedAt { get; set; }
public string UpdatedAt { get; set; }
}
+1
View File
@@ -21,5 +21,6 @@ app.UseStaticFiles();
app.MapChatEndpoint(); app.MapChatEndpoint();
app.MapHistoryEndpoint(); app.MapHistoryEndpoint();
app.MapSessionEndpoints();
app.Run("http://0.0.0.0:19000"); app.Run("http://0.0.0.0:19000");
+76 -22
View File
@@ -31,9 +31,18 @@ public class DatabaseService
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
conn.Execute(@" conn.Execute(@"
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL UNIQUE,
name TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archives ( CREATE TABLE IF NOT EXISTS archives (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
archive_id TEXT NOT NULL UNIQUE, archive_id TEXT NOT NULL UNIQUE,
session_id TEXT NOT NULL,
name TEXT, name TEXT,
chain_count INTEGER NOT NULL DEFAULT 0, chain_count INTEGER NOT NULL DEFAULT 0,
message_count INTEGER NOT NULL DEFAULT 0, message_count INTEGER NOT NULL DEFAULT 0,
@@ -75,24 +84,76 @@ public class DatabaseService
"); ");
} }
// ── Sessions ──
public async Task<SessionRecord> CreateSession(string name = null)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
var session = new SessionRecord
{
SessionId = Guid.NewGuid().ToString("N"),
Name = name,
CreatedAt = now,
UpdatedAt = now
};
await conn.ExecuteAsync(@"
INSERT INTO sessions (session_id, name, created_at, updated_at)
VALUES (@SessionId, @Name, @CreatedAt, @UpdatedAt)
", session);
return session;
}
public async Task<IEnumerable<SessionRecord>> GetSessions()
{
using var conn = CreateConnection();
return await conn.QueryAsync<SessionRecord>(
"SELECT * FROM sessions ORDER BY updated_at DESC");
}
public async Task<SessionRecord> GetSession(string sessionId)
{
using var conn = CreateConnection();
return await conn.QuerySingleOrDefaultAsync<SessionRecord>(
"SELECT * FROM sessions WHERE session_id = @sessionId", new { sessionId });
}
public async Task UpdateSessionTimestamp(string sessionId)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE sessions SET updated_at = @now WHERE session_id = @sessionId",
new { sessionId, now = DateTime.UtcNow.ToString("o") });
}
public async Task UpdateSessionName(string sessionId, string name)
{
using var conn = CreateConnection();
await conn.ExecuteAsync(
"UPDATE sessions SET name = @name, updated_at = @now WHERE session_id = @sessionId",
new { sessionId, name, now = DateTime.UtcNow.ToString("o") });
}
// ── Archives ── // ── Archives ──
public async Task<ArchiveRecord> CreateArchive(string name = null) public async Task<ArchiveRecord> CreateArchive(string sessionId, string name = null)
{ {
using var conn = CreateConnection(); using var conn = CreateConnection();
// Zähle aktive Chains und Messages
var chainCount = await conn.ExecuteScalarAsync<int>( var chainCount = await conn.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM chains WHERE archive_id IS NULL"); "SELECT COUNT(*) FROM chains WHERE archive_id IS NULL AND session_id = @sessionId",
new { sessionId });
if (chainCount == 0) return null; if (chainCount == 0) return null;
var messageCount = await conn.ExecuteScalarAsync<int>(@" var messageCount = await conn.ExecuteScalarAsync<int>(@"
SELECT COUNT(*) FROM messages SELECT COUNT(*) FROM messages
WHERE chain_id IN (SELECT chain_id FROM chains WHERE archive_id IS NULL)"); WHERE chain_id IN (SELECT chain_id FROM chains WHERE archive_id IS NULL AND session_id = @sessionId)",
new { sessionId });
var archive = new ArchiveRecord var archive = new ArchiveRecord
{ {
ArchiveId = Guid.NewGuid().ToString("N"), ArchiveId = Guid.NewGuid().ToString("N"),
SessionId = sessionId,
Name = name, Name = name,
ChainCount = chainCount, ChainCount = chainCount,
MessageCount = messageCount, MessageCount = messageCount,
@@ -100,23 +161,23 @@ public class DatabaseService
}; };
await conn.ExecuteAsync(@" await conn.ExecuteAsync(@"
INSERT INTO archives (archive_id, name, chain_count, message_count, created_at) INSERT INTO archives (archive_id, session_id, name, chain_count, message_count, created_at)
VALUES (@ArchiveId, @Name, @ChainCount, @MessageCount, @CreatedAt) VALUES (@ArchiveId, @SessionId, @Name, @ChainCount, @MessageCount, @CreatedAt)
", archive); ", archive);
// Alle aktiven Chains diesem Archiv zuweisen
await conn.ExecuteAsync( await conn.ExecuteAsync(
"UPDATE chains SET archive_id = @archiveId WHERE archive_id IS NULL", "UPDATE chains SET archive_id = @archiveId WHERE archive_id IS NULL AND session_id = @sessionId",
new { archiveId = archive.ArchiveId }); new { archiveId = archive.ArchiveId, sessionId });
return archive; return archive;
} }
public async Task<IEnumerable<ArchiveRecord>> GetArchives(int limit = 20) public async Task<IEnumerable<ArchiveRecord>> GetArchives(string sessionId)
{ {
using var conn = CreateConnection(); using var conn = CreateConnection();
return await conn.QueryAsync<ArchiveRecord>( return await conn.QueryAsync<ArchiveRecord>(
"SELECT * FROM archives ORDER BY id DESC LIMIT @limit", new { limit }); "SELECT * FROM archives WHERE session_id = @sessionId ORDER BY id DESC",
new { sessionId });
} }
public async Task<IEnumerable<ChainRecord>> GetArchiveChains(string archiveId) public async Task<IEnumerable<ChainRecord>> GetArchiveChains(string archiveId)
@@ -161,14 +222,7 @@ public class DatabaseService
); );
} }
public async Task<IEnumerable<ChainRecord>> GetActiveChains() public async Task<IEnumerable<ChainRecord>> GetActiveChains(string sessionId)
{
using var conn = CreateConnection();
return await conn.QueryAsync<ChainRecord>(
"SELECT * FROM chains WHERE archive_id IS NULL ORDER BY id DESC");
}
public async Task<IEnumerable<ChainRecord>> GetSessionChains(string sessionId)
{ {
using var conn = CreateConnection(); using var conn = CreateConnection();
return await conn.QueryAsync<ChainRecord>( return await conn.QueryAsync<ChainRecord>(
@@ -197,14 +251,14 @@ public class DatabaseService
); );
} }
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(long afterId, int limit = 200) public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200)
{ {
using var conn = CreateConnection(); using var conn = CreateConnection();
return await conn.QueryAsync<MessageRecord>(@" return await conn.QueryAsync<MessageRecord>(@"
SELECT m.* FROM messages m SELECT m.* FROM messages m
JOIN chains c ON m.chain_id = c.chain_id JOIN chains c ON m.chain_id = c.chain_id
WHERE m.id > @afterId AND c.archive_id IS NULL WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId
ORDER BY m.id LIMIT @limit", ORDER BY m.id LIMIT @limit",
new { afterId, limit }); new { sessionId, afterId, limit });
} }
} }
+71 -2
View File
@@ -4,10 +4,16 @@ body {
background: #222; background: #222;
color: #eee; color: #eee;
display: flex; display: flex;
flex-direction: column;
height: 100vh; height: 100vh;
} }
main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
body, input, select, button, textarea { body, input, select, button, textarea {
font-family: monospace; font-family: monospace;
font-size: 1em; font-size: 1em;
@@ -28,6 +34,7 @@ button:hover, select:hover, input:hover, textarea:hover {
button { button {
cursor: pointer; cursor: pointer;
line-height: 1.25;
} }
textarea { textarea {
@@ -46,6 +53,68 @@ textarea {
cursor: pointer; cursor: pointer;
} }
/* ── Sidebar ── */
#sidebar {
width: 260px;
background: #1a1a1a;
border-right: 1px solid #444;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
}
.sidebar-header {
padding: 1em;
border-bottom: 1px solid #444;
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-header h2 {
font-size: 1em;
}
#sessionList {
flex: 1;
overflow-y: auto;
}
.session-item {
padding: 1em;
cursor: pointer;
display: flex;
flex-direction: column;
border-left: 3px solid transparent;
}
.session-item:hover { background: #333; }
.session-item.active {
background: #333;
border-left-color: #eee;
}
.session-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 1px solid transparent;
}
.session-date {
color: #888;
}
.session-rename {
background: #222;
border: 1px solid #888;
outline: none;
width: 100%;
}
/* ── Header ── */ /* ── Header ── */
header { header {
@@ -79,7 +148,7 @@ header h1 {
background: #111; background: #111;
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
flex-shrink: 0; flex-shrink: 0;
min-height: 2em; min-height: 2.5em;
} }
/* ── Chat ── */ /* ── Chat ── */
+29 -18
View File
@@ -8,30 +8,41 @@
</head> </head>
<body> <body>
<header> <aside id="sidebar">
<h1>// gott</h1> <div class="sidebar-header">
<select id="model"> <h2>Sessions</h2>
<option value="sonnet">Sonnet 4.6</option> <button id="newSessionBtn" title="Neue Session">+</button>
<option value="opus">Opus 4.6</option>
</select>
<button class="clear-btn" id="clearBtn">Clear</button>
<div class="toggle">
<input type="checkbox" id="showSystem">
<label for="showSystem">System Prompt</label>
</div> </div>
</header> <div id="sessionList"></div>
</aside>
<textarea id="systemPrompt" placeholder="System Prompt (SOUL.md Inhalt etc.)"></textarea> <main>
<div class="meta-bar" id="meta"></div> <header>
<div id="chat"></div> <h1>// gott</h1>
<select id="model">
<option value="sonnet">Sonnet 4.6</option>
<option value="opus">Opus 4.6</option>
</select>
<button class="clear-btn" id="clearBtn">Archivieren</button>
<div class="toggle">
<input type="checkbox" id="showSystem">
<label for="showSystem">System Prompt</label>
</div>
</header>
<div class="input-area"> <textarea id="systemPrompt" placeholder="System Prompt (SOUL.md Inhalt etc.)"></textarea>
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter)" rows="2"></textarea> <div class="meta-bar" id="meta"></div>
<button id="send">Senden</button> <div id="chat"></div>
</div>
<div class="input-area">
<textarea id="input" placeholder="Nachricht eingeben... (Ctrl+Enter)" rows="2"></textarea>
<button id="send">Senden</button>
</div>
</main>
<script src="js/ui.js"></script> <script src="js/ui.js"></script>
<script src="js/api.js"></script> <script src="js/api.js"></script>
<script src="js/session.js"></script>
<script src="js/chat.js"></script> <script src="js/chat.js"></script>
<script src="js/history.js"></script> <script src="js/history.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
+30 -9
View File
@@ -1,31 +1,31 @@
// ── API Calls ── // ── API Calls ──
async function apiChat(model, messages, systemPrompt) { async function apiChat(model, messages, systemPrompt, sessionId) {
return fetch('/api/chat', { return fetch('/api/chat', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, messages, systemPrompt }) body: JSON.stringify({ model, messages, systemPrompt, sessionId })
}); });
} }
async function apiHistory(sessionId = 'null') { async function apiHistory(sessionId) {
const resp = await fetch(`/api/history?sessionId=${sessionId}`); const resp = await fetch(`/api/history?sessionId=${sessionId}`);
return resp.json(); return resp.json();
} }
async function apiHistoryPoll(afterId) { async function apiHistoryPoll(sessionId, afterId) {
const resp = await fetch(`/api/history/poll?afterId=${afterId}`); const resp = await fetch(`/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`);
return resp.json(); return resp.json();
} }
async function apiArchive() { async function apiArchive(sessionId) {
const resp = await fetch('/api/archive', { method: 'POST' }); const resp = await fetch(`/api/archive?sessionId=${sessionId}`, { method: 'POST' });
if (resp.status === 204) return null; if (resp.status === 204) return null;
return resp.json(); return resp.json();
} }
async function apiListArchives() { async function apiListArchives(sessionId) {
const resp = await fetch('/api/archives'); const resp = await fetch(`/api/archives?sessionId=${sessionId}`);
return resp.json(); return resp.json();
} }
@@ -33,3 +33,24 @@ async function apiGetArchive(archiveId) {
const resp = await fetch(`/api/archive/${archiveId}`); const resp = await fetch(`/api/archive/${archiveId}`);
return resp.json(); return resp.json();
} }
// ── Session API ──
async function apiGetSessions() {
const resp = await fetch('/api/sessions');
return resp.json();
}
async function apiCreateSession() {
const resp = await fetch('/api/sessions', { method: 'POST' });
return resp.json();
}
async function apiUpdateSession(sessionId, data) {
const resp = await fetch(`/api/sessions/${sessionId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return resp.json();
}
+8 -6
View File
@@ -1,13 +1,18 @@
// ── App Init ── // ── App Init ──
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
// Session initialisieren (vor allem anderen)
await initSession();
// System Prompt Toggle // System Prompt Toggle
$('#showSystem').addEventListener('change', () => { $('#showSystem').addEventListener('change', () => {
$('#systemPrompt').classList.toggle('visible', $('#showSystem').checked); $('#systemPrompt').classList.toggle('visible', $('#showSystem').checked);
}); });
// Send Button // Buttons
$('#send').addEventListener('click', sendMessage); $('#send').addEventListener('click', sendMessage);
$('#clearBtn').addEventListener('click', clearChat);
$('#newSessionBtn').addEventListener('click', createNewSession);
// Ctrl+Enter // Ctrl+Enter
$('#input').addEventListener('keydown', (e) => { $('#input').addEventListener('keydown', (e) => {
@@ -20,9 +25,6 @@ document.addEventListener('DOMContentLoaded', () => {
this.style.height = Math.min(this.scrollHeight, 150) + 'px'; this.style.height = Math.min(this.scrollHeight, 150) + 'px';
}); });
// Clear Button
$('#clearBtn').addEventListener('click', clearChat);
// History laden // History laden
loadHistory(); await loadHistory();
}); });
+7 -8
View File
@@ -4,17 +4,16 @@ const messages = [];
let isStreaming = false; let isStreaming = false;
async function clearChat() { async function clearChat() {
const archive = await apiArchive(); const archive = await apiArchive(getCurrentSessionId());
if (!archive) return;
messages.length = 0; messages.length = 0;
chatEl().innerHTML = ''; chatEl().innerHTML = '';
lastKnownMessageId = 0; lastKnownMessageId = 0;
loadedArchiveCount = 0;
if (archive) { setMeta(`Archiviert: ${archive.chainCount} Chains, ${archive.messageCount} Messages`);
setMeta(`Archiviert: ${archive.chainCount} Chains, ${archive.messageCount} Messages`); showArchiveLoadButton();
showArchiveLoadButton();
} else {
setMeta('');
}
} }
// ── Send Message ── // ── Send Message ──
@@ -40,7 +39,7 @@ async function sendMessage() {
let currentToolBox = null; let currentToolBox = null;
try { try {
const resp = await apiChat(model, messages, systemPrompt); const resp = await apiChat(model, messages, systemPrompt, getCurrentSessionId());
const reader = resp.body.getReader(); const reader = resp.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ''; let buffer = '';
+5 -4
View File
@@ -6,12 +6,13 @@ let loadedArchiveCount = 0;
async function loadHistory() { async function loadHistory() {
try { try {
const archives = await apiListArchives(); const sid = getCurrentSessionId();
const archives = await apiListArchives(sid);
if (archives.length > 0) { if (archives.length > 0) {
showArchiveLoadButton(); showArchiveLoadButton();
} }
const data = await apiHistory('null'); const data = await apiHistory(sid);
data.reverse(); data.reverse();
for (const entry of data) { for (const entry of data) {
@@ -82,7 +83,7 @@ function getOrCreateArchiveDivider() {
async function loadLatestArchive() { async function loadLatestArchive() {
try { try {
const archives = await apiListArchives(); const archives = await apiListArchives(getCurrentSessionId());
if (archives.length <= loadedArchiveCount) return; if (archives.length <= loadedArchiveCount) return;
const latest = archives[loadedArchiveCount]; const latest = archives[loadedArchiveCount];
@@ -163,7 +164,7 @@ async function pollHistory() {
if (isStreaming) return; if (isStreaming) return;
try { try {
const newMessages = await apiHistoryPoll(lastKnownMessageId); const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId);
if (newMessages.length > 0) { if (newMessages.length > 0) {
renderMessages(newMessages, true); renderMessages(newMessages, true);
scrollDown(); scrollDown();
+108
View File
@@ -0,0 +1,108 @@
// ── Session State ──
let currentSessionId = null;
function getCurrentSessionId() {
return currentSessionId;
}
async function initSession() {
// SessionId aus localStorage oder neue erstellen
const stored = localStorage.getItem('sessionId');
if (stored) {
currentSessionId = stored;
} else {
const session = await apiCreateSession();
currentSessionId = session.sessionId;
localStorage.setItem('sessionId', currentSessionId);
}
await refreshSessionList();
}
async function createNewSession() {
const session = await apiCreateSession();
currentSessionId = session.sessionId;
localStorage.setItem('sessionId', currentSessionId);
// Chat leeren und neu laden
messages.length = 0;
chatEl().innerHTML = '';
lastKnownMessageId = 0;
loadedArchiveCount = 0;
setMeta('');
await refreshSessionList();
await loadHistory();
}
async function switchSession(sessionId) {
if (sessionId === currentSessionId) return;
currentSessionId = sessionId;
localStorage.setItem('sessionId', currentSessionId);
// Chat leeren und Session laden
messages.length = 0;
chatEl().innerHTML = '';
lastKnownMessageId = 0;
loadedArchiveCount = 0;
setMeta('');
await refreshSessionList();
await loadHistory();
}
async function refreshSessionList() {
const sessions = await apiGetSessions();
const list = $('#sessionList');
list.innerHTML = '';
for (const s of sessions) {
const div = document.createElement('div');
div.className = 'session-item' + (s.sessionId === currentSessionId ? ' active' : '');
div.dataset.sessionId = s.sessionId;
const name = document.createElement('span');
name.className = 'session-name';
name.textContent = s.name || 'Unbenannt';
name.addEventListener('dblclick', () => renameSession(s.sessionId, name));
const date = document.createElement('span');
date.className = 'session-date';
date.textContent = new Date(s.updatedAt).toLocaleString('de-AT', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
});
div.appendChild(name);
div.appendChild(date);
div.addEventListener('click', () => switchSession(s.sessionId));
list.appendChild(div);
}
}
async function renameSession(sessionId, nameEl) {
const input = document.createElement('input');
input.type = 'text';
input.className = 'session-rename';
input.value = nameEl.textContent;
nameEl.replaceWith(input);
input.focus();
input.select();
const save = async () => {
const newName = input.value.trim();
if (newName) {
await apiUpdateSession(sessionId, { name: newName });
}
await refreshSessionList();
};
input.addEventListener('blur', save);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') input.blur();
if (e.key === 'Escape') { input.value = nameEl.textContent; input.blur(); }
});
}