Sessions: Sidebar, Session-Wechsel, Archivieren-Button, alles session-bezogen
This commit is contained in:
1 parent
8ddee38032
commit
25f07caf83
15 files changed
+423
-59
No files matched your search
@@ -50,16 +50,27 @@ public static class ChatEndpoint
|
||||
res.Headers["Cache-Control"] = "no-cache";
|
||||
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");
|
||||
|
||||
// Chain zuerst anlegen (FK für messages)
|
||||
await db.CreateChain(new ChainRecord
|
||||
{
|
||||
ChainId = chainId,
|
||||
SessionId = sessionId,
|
||||
Model = modelId,
|
||||
SystemPrompt = body.SystemPrompt
|
||||
});
|
||||
|
||||
// Session-Timestamp aktualisieren
|
||||
await db.UpdateSessionTimestamp(sessionId);
|
||||
|
||||
var messages = new List<object>();
|
||||
foreach (var msg in body.Messages)
|
||||
messages.Add(new { role = msg.Role, content = msg.Content });
|
||||
|
||||
@@ -23,12 +23,14 @@ public static class HistoryEndpoint
|
||||
private static async Task HandleHistory(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
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;
|
||||
if (string.IsNullOrEmpty(sessionId) || sessionId == "null")
|
||||
chains = await db.GetActiveChains();
|
||||
else
|
||||
chains = await db.GetSessionChains(sessionId);
|
||||
var chains = await db.GetActiveChains(sessionId);
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var chain in chains)
|
||||
@@ -42,16 +44,30 @@ public static class HistoryEndpoint
|
||||
|
||||
private static async Task HandlePoll(HttpContext ctx, DatabaseService db)
|
||||
{
|
||||
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 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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
ctx.Response.StatusCode = 204;
|
||||
@@ -62,7 +78,14 @@ public static class HistoryEndpoint
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public class ArchiveRecord
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string ArchiveId { get; set; }
|
||||
public string SessionId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int ChainCount { get; set; }
|
||||
public int MessageCount { get; set; }
|
||||
|
||||
@@ -22,4 +22,7 @@ public class ChatRequest
|
||||
|
||||
[JsonPropertyName("systemPrompt")]
|
||||
public string SystemPrompt { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("sessionId")]
|
||||
public string SessionId { get; set; } = "";
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -21,5 +21,6 @@ app.UseStaticFiles();
|
||||
|
||||
app.MapChatEndpoint();
|
||||
app.MapHistoryEndpoint();
|
||||
app.MapSessionEndpoints();
|
||||
|
||||
app.Run("http://0.0.0.0:19000");
|
||||
+76
-22
@@ -31,9 +31,18 @@ public class DatabaseService
|
||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
archive_id TEXT NOT NULL UNIQUE,
|
||||
session_id TEXT NOT NULL,
|
||||
name TEXT,
|
||||
chain_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 ──
|
||||
|
||||
public async Task<ArchiveRecord> CreateArchive(string name = null)
|
||||
public async Task<ArchiveRecord> CreateArchive(string sessionId, string name = null)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
|
||||
// Zähle aktive Chains und Messages
|
||||
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;
|
||||
|
||||
var messageCount = await conn.ExecuteScalarAsync<int>(@"
|
||||
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
|
||||
{
|
||||
ArchiveId = Guid.NewGuid().ToString("N"),
|
||||
SessionId = sessionId,
|
||||
Name = name,
|
||||
ChainCount = chainCount,
|
||||
MessageCount = messageCount,
|
||||
@@ -100,23 +161,23 @@ public class DatabaseService
|
||||
};
|
||||
|
||||
await conn.ExecuteAsync(@"
|
||||
INSERT INTO archives (archive_id, name, chain_count, message_count, created_at)
|
||||
VALUES (@ArchiveId, @Name, @ChainCount, @MessageCount, @CreatedAt)
|
||||
INSERT INTO archives (archive_id, session_id, name, chain_count, message_count, created_at)
|
||||
VALUES (@ArchiveId, @SessionId, @Name, @ChainCount, @MessageCount, @CreatedAt)
|
||||
", archive);
|
||||
|
||||
// Alle aktiven Chains diesem Archiv zuweisen
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE chains SET archive_id = @archiveId WHERE archive_id IS NULL",
|
||||
new { archiveId = archive.ArchiveId });
|
||||
"UPDATE chains SET archive_id = @archiveId WHERE archive_id IS NULL AND session_id = @sessionId",
|
||||
new { archiveId = archive.ArchiveId, sessionId });
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ArchiveRecord>> GetArchives(int limit = 20)
|
||||
public async Task<IEnumerable<ArchiveRecord>> GetArchives(string sessionId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
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)
|
||||
@@ -161,14 +222,7 @@ public class DatabaseService
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ChainRecord>> GetActiveChains()
|
||||
{
|
||||
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)
|
||||
public async Task<IEnumerable<ChainRecord>> GetActiveChains(string sessionId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
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();
|
||||
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
|
||||
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId
|
||||
ORDER BY m.id LIMIT @limit",
|
||||
new { afterId, limit });
|
||||
new { sessionId, afterId, limit });
|
||||
}
|
||||
}
|
||||
+71
-2
@@ -4,10 +4,16 @@ body {
|
||||
background: #222;
|
||||
color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body, input, select, button, textarea {
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
@@ -28,6 +34,7 @@ button:hover, select:hover, input:hover, textarea:hover {
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@@ -46,6 +53,68 @@ textarea {
|
||||
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 {
|
||||
@@ -79,7 +148,7 @@ header h1 {
|
||||
background: #111;
|
||||
border-bottom: 1px solid #333;
|
||||
flex-shrink: 0;
|
||||
min-height: 2em;
|
||||
min-height: 2.5em;
|
||||
}
|
||||
|
||||
/* ── Chat ── */
|
||||
|
||||
+12
-1
@@ -8,13 +8,22 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<aside id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>Sessions</h2>
|
||||
<button id="newSessionBtn" title="Neue Session">+</button>
|
||||
</div>
|
||||
<div id="sessionList"></div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<header>
|
||||
<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">Clear</button>
|
||||
<button class="clear-btn" id="clearBtn">Archivieren</button>
|
||||
<div class="toggle">
|
||||
<input type="checkbox" id="showSystem">
|
||||
<label for="showSystem">System Prompt</label>
|
||||
@@ -29,9 +38,11 @@
|
||||
<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/api.js"></script>
|
||||
<script src="js/session.js"></script>
|
||||
<script src="js/chat.js"></script>
|
||||
<script src="js/history.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
+30
-9
@@ -1,31 +1,31 @@
|
||||
// ── API Calls ──
|
||||
|
||||
async function apiChat(model, messages, systemPrompt) {
|
||||
async function apiChat(model, messages, systemPrompt, sessionId) {
|
||||
return fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
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}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiHistoryPoll(afterId) {
|
||||
const resp = await fetch(`/api/history/poll?afterId=${afterId}`);
|
||||
async function apiHistoryPoll(sessionId, afterId) {
|
||||
const resp = await fetch(`/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiArchive() {
|
||||
const resp = await fetch('/api/archive', { method: 'POST' });
|
||||
async function apiArchive(sessionId) {
|
||||
const resp = await fetch(`/api/archive?sessionId=${sessionId}`, { method: 'POST' });
|
||||
if (resp.status === 204) return null;
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
async function apiListArchives() {
|
||||
const resp = await fetch('/api/archives');
|
||||
async function apiListArchives(sessionId) {
|
||||
const resp = await fetch(`/api/archives?sessionId=${sessionId}`);
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
@@ -33,3 +33,24 @@ async function apiGetArchive(archiveId) {
|
||||
const resp = await fetch(`/api/archive/${archiveId}`);
|
||||
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
@@ -1,13 +1,18 @@
|
||||
// ── App Init ──
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Session initialisieren (vor allem anderen)
|
||||
await initSession();
|
||||
|
||||
// System Prompt Toggle
|
||||
$('#showSystem').addEventListener('change', () => {
|
||||
$('#systemPrompt').classList.toggle('visible', $('#showSystem').checked);
|
||||
});
|
||||
|
||||
// Send Button
|
||||
// Buttons
|
||||
$('#send').addEventListener('click', sendMessage);
|
||||
$('#clearBtn').addEventListener('click', clearChat);
|
||||
$('#newSessionBtn').addEventListener('click', createNewSession);
|
||||
|
||||
// Ctrl+Enter
|
||||
$('#input').addEventListener('keydown', (e) => {
|
||||
@@ -20,9 +25,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
|
||||
});
|
||||
|
||||
// Clear Button
|
||||
$('#clearBtn').addEventListener('click', clearChat);
|
||||
|
||||
// History laden
|
||||
loadHistory();
|
||||
await loadHistory();
|
||||
});
|
||||
+5
-6
@@ -4,17 +4,16 @@ const messages = [];
|
||||
let isStreaming = false;
|
||||
|
||||
async function clearChat() {
|
||||
const archive = await apiArchive();
|
||||
const archive = await apiArchive(getCurrentSessionId());
|
||||
if (!archive) return;
|
||||
|
||||
messages.length = 0;
|
||||
chatEl().innerHTML = '';
|
||||
lastKnownMessageId = 0;
|
||||
loadedArchiveCount = 0;
|
||||
|
||||
if (archive) {
|
||||
setMeta(`Archiviert: ${archive.chainCount} Chains, ${archive.messageCount} Messages`);
|
||||
showArchiveLoadButton();
|
||||
} else {
|
||||
setMeta('');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Send Message ──
|
||||
@@ -40,7 +39,7 @@ async function sendMessage() {
|
||||
let currentToolBox = null;
|
||||
|
||||
try {
|
||||
const resp = await apiChat(model, messages, systemPrompt);
|
||||
const resp = await apiChat(model, messages, systemPrompt, getCurrentSessionId());
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
@@ -6,12 +6,13 @@ let loadedArchiveCount = 0;
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const archives = await apiListArchives();
|
||||
const sid = getCurrentSessionId();
|
||||
const archives = await apiListArchives(sid);
|
||||
if (archives.length > 0) {
|
||||
showArchiveLoadButton();
|
||||
}
|
||||
|
||||
const data = await apiHistory('null');
|
||||
const data = await apiHistory(sid);
|
||||
data.reverse();
|
||||
|
||||
for (const entry of data) {
|
||||
@@ -82,7 +83,7 @@ function getOrCreateArchiveDivider() {
|
||||
|
||||
async function loadLatestArchive() {
|
||||
try {
|
||||
const archives = await apiListArchives();
|
||||
const archives = await apiListArchives(getCurrentSessionId());
|
||||
if (archives.length <= loadedArchiveCount) return;
|
||||
|
||||
const latest = archives[loadedArchiveCount];
|
||||
@@ -163,7 +164,7 @@ async function pollHistory() {
|
||||
if (isStreaming) return;
|
||||
|
||||
try {
|
||||
const newMessages = await apiHistoryPoll(lastKnownMessageId);
|
||||
const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId);
|
||||
if (newMessages.length > 0) {
|
||||
renderMessages(newMessages, true);
|
||||
scrollDown();
|
||||
|
||||
@@ -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(); }
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user