diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index a807ec7..25b8b69 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -13,6 +13,7 @@ public static class SessionEndpoint public static void MapSessionEndpoints(this IEndpointRouteBuilder app) { app.MapGet("/api/sessions", HandleList); + app.MapGet("/api/sessions/{sessionId}", HandleGet); app.MapPost("/api/sessions", HandleCreate); app.MapPut("/api/sessions/{sessionId}", HandleUpdate); } @@ -23,6 +24,23 @@ public static class SessionEndpoint await ctx.Response.WriteAsJsonAsync(sessions); } + private static async Task HandleGet(HttpContext ctx, DatabaseService db) + { + var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); + if (string.IsNullOrEmpty(sessionId)) + { + ctx.Response.StatusCode = 400; + return; + } + var session = await db.GetSession(sessionId); + if (session == null) + { + ctx.Response.StatusCode = 404; + return; + } + await ctx.Response.WriteAsJsonAsync(session); + } + private static async Task HandleCreate(HttpContext ctx, DatabaseService db) { var session = await db.CreateSession("Neue Session"); diff --git a/wwwroot/css/style.css b/wwwroot/css/style.css index 2befe50..8be8b6c 100644 --- a/wwwroot/css/style.css +++ b/wwwroot/css/style.css @@ -4,16 +4,54 @@ body { background: #222; color: #eee; display: flex; + flex-direction: column; height: 100vh; } -main { +#app { flex: 1; display: flex; flex-direction: column; - min-width: 0; + overflow: hidden; } +.page { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* ── Nav ── */ + +#nav { + padding: .75em 1.5em; + border-bottom: 1px solid #444; + display: flex; + align-items: center; + gap: 2em; + flex-shrink: 0; +} + +.nav-brand { + font-weight: bold; + font-size: 1.1em; +} + +.nav-links { + display: flex; + gap: 1em; +} + +.nav-link { + color: #888; + text-decoration: none; + padding: .25em .5em; +} + +.nav-link:hover { color: #eee; } +.nav-link.active { color: #eee; border-bottom: 2px solid #eee; } + body, input, select, button, textarea { font-family: monospace; font-size: 1em; @@ -53,66 +91,10 @@ 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 ── */ @@ -249,3 +231,108 @@ header h1 { display: block; margin-inline: auto; } + +/* ── Dashboard ── */ + +.dashboard { + padding: 2em; + overflow-y: auto; +} + +.dashboard section { + margin-bottom: 2em; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1em; +} + +.section-header h2 { + font-size: 1.1em; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1em; +} + +.card { + background: #333; + border: 1px solid #444; + padding: 1em; + display: flex; + flex-direction: column; + gap: .5em; +} + +.card.clickable { cursor: pointer; } +.card.clickable:hover { border-color: #888; } + +.card-icon { font-size: 2em; } +.card-title { font-weight: bold; } +.card-sub { color: #888; font-size: 0.85em; } + +.card.placeholder { + border-style: dashed; + color: #666; +} + +/* ── Session Page ── */ + +.session-page { + display: flex; + flex-direction: column; +} + +.session-header { + padding: .75em 1.5em; + border-bottom: 1px solid #444; + display: flex; + align-items: center; + gap: 1em; + flex-shrink: 0; +} + +.session-header h1 { + font-size: 1em; +} + +.back-btn { + background: none; + border: none; + font-size: 1.2em; + padding: .25em .5em; +} + +.header-btn { + font-size: 0.85em; +} + +.editable-title { + cursor: pointer; +} + +.editable-title:hover { + text-decoration: underline; + text-decoration-style: dotted; +} + +/* ── Placeholder Pages ── */ + +.placeholder-page { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1em; + color: #888; +} + +.placeholder-icon { + font-size: 4em; +} diff --git a/wwwroot/index.html b/wwwroot/index.html index 514388d..ad4c2d9 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -3,48 +3,109 @@ - Gott - Debug Console + Gott - + -
-
-

// gott

- - -
- - +
+ + + + + + + + + + + + + diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 18bda82..6804558 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -46,6 +46,12 @@ async function apiCreateSession() { return resp.json(); } +async function apiGetSession(sessionId) { + const resp = await fetch(`/api/sessions/${sessionId}`); + if (!resp.ok) return null; + return resp.json(); +} + async function apiUpdateSession(sessionId, data) { const resp = await fetch(`/api/sessions/${sessionId}`, { method: 'PUT', diff --git a/wwwroot/js/app.js b/wwwroot/js/app.js index 4f391b8..10fb05d 100644 --- a/wwwroot/js/app.js +++ b/wwwroot/js/app.js @@ -1,30 +1,21 @@ // ── App Init ── document.addEventListener('DOMContentLoaded', async () => { - // Session initialisieren (vor allem anderen) - await initSession(); - - // System Prompt Toggle - $('#showSystem').addEventListener('change', () => { - $('#systemPrompt').classList.toggle('visible', $('#showSystem').checked); + // Nav-Links + document.querySelectorAll('.nav-link[data-page]').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + navigateTo(link.dataset.page); + }); }); - // Buttons - $('#send').addEventListener('click', sendMessage); - $('#clearBtn').addEventListener('click', clearChat); - $('#newSessionBtn').addEventListener('click', createNewSession); + // Route aus Hash laden + const { page, id } = parseHash(); + await navigateTo(page, { id }); +}); - // Ctrl+Enter - $('#input').addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'Enter') sendMessage(); - }); - - // Auto-Resize Textarea - $('#input').addEventListener('input', function () { - this.style.height = 'auto'; - this.style.height = Math.min(this.scrollHeight, 150) + 'px'; - }); - - // History laden - await loadHistory(); +// Hash-Änderungen abfangen +window.addEventListener('hashchange', async () => { + const { page, id } = parseHash(); + await navigateTo(page, { id }); }); diff --git a/wwwroot/js/pages/dashboard.js b/wwwroot/js/pages/dashboard.js new file mode 100644 index 0000000..a1ec0e1 --- /dev/null +++ b/wwwroot/js/pages/dashboard.js @@ -0,0 +1,46 @@ +// ── Dashboard Page ── + +registerPage('dashboard', { + async init() { + const sessions = await apiGetSessions(); + const grid = document.getElementById('sessionCards'); + grid.innerHTML = ''; + + for (const s of sessions) { + const card = document.createElement('div'); + card.className = 'card clickable'; + card.innerHTML = ` +
${escHtml(s.name || 'Unbenannt')}
+
${formatDate(s.updatedAt)}
+ `; + card.addEventListener('click', () => { + navigateTo('session', { id: s.sessionId }); + }); + grid.appendChild(card); + } + + // Neue Session Button + const btn = document.getElementById('newSessionBtn'); + if (btn) { + btn.addEventListener('click', async () => { + const session = await apiCreateSession(); + navigateTo('session', { id: session.sessionId }); + }); + } + }, + destroy() {} +}); + +function escHtml(s) { + const d = document.createElement('div'); + d.textContent = s; + return d.innerHTML; +} + +function formatDate(iso) { + if (!iso) return ''; + return new Date(iso).toLocaleString('de-AT', { + day: '2-digit', month: '2-digit', year: '2-digit', + hour: '2-digit', minute: '2-digit' + }); +} diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js new file mode 100644 index 0000000..b7f3663 --- /dev/null +++ b/wwwroot/js/pages/session-page.js @@ -0,0 +1,100 @@ +// ── Session Page ── + +let _sessionPageCleanup = []; + +registerPage('session', { + async init(params) { + const sessionId = params.id; + if (!sessionId) return navigateTo('dashboard'); + + currentSessionId = sessionId; + + // Session-Name laden + const session = await apiGetSession(sessionId); + const titleEl = document.getElementById('sessionTitle'); + if (session && titleEl) { + titleEl.textContent = session.name || 'Unbenannt'; + titleEl.addEventListener('dblclick', () => editTitle(sessionId, titleEl)); + } + + // System Prompt Toggle + const showSystem = document.getElementById('showSystem'); + const systemPrompt = document.getElementById('systemPrompt'); + if (showSystem && systemPrompt) { + showSystem.addEventListener('change', () => { + systemPrompt.classList.toggle('visible', showSystem.checked); + }); + } + + // Send + const sendBtn = document.getElementById('send'); + if (sendBtn) sendBtn.addEventListener('click', sendMessage); + + // Ctrl+Enter + const inputEl = document.getElementById('input'); + if (inputEl) { + const keyHandler = (e) => { + if (e.ctrlKey && e.key === 'Enter') sendMessage(); + }; + inputEl.addEventListener('keydown', keyHandler); + + // Auto-Resize + inputEl.addEventListener('input', function () { + this.style.height = 'auto'; + this.style.height = Math.min(this.scrollHeight, 150) + 'px'; + }); + } + + // Archivieren + const archiveBtn = document.getElementById('archiveBtn'); + if (archiveBtn) archiveBtn.addEventListener('click', clearChat); + + // State zurücksetzen und History laden + messages.length = 0; + lastKnownMessageId = 0; + loadedArchiveCount = 0; + isStreaming = false; + + await loadHistory(); + }, + + destroy() { + stopHistoryPolling(); + messages.length = 0; + lastKnownMessageId = 0; + loadedArchiveCount = 0; + isStreaming = false; + } +}); + +function editTitle(sessionId, titleEl) { + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'session-rename'; + input.value = titleEl.textContent; + + titleEl.replaceWith(input); + input.focus(); + input.select(); + + const save = async () => { + const newName = input.value.trim(); + if (newName) { + await apiUpdateSession(sessionId, { name: newName }); + const newTitle = document.createElement('h1'); + newTitle.id = 'sessionTitle'; + newTitle.className = 'editable-title'; + newTitle.textContent = newName; + newTitle.addEventListener('dblclick', () => editTitle(sessionId, newTitle)); + input.replaceWith(newTitle); + } else { + input.replaceWith(titleEl); + } + }; + + input.addEventListener('blur', save); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') input.blur(); + if (e.key === 'Escape') { input.replaceWith(titleEl); } + }); +} diff --git a/wwwroot/js/router.js b/wwwroot/js/router.js new file mode 100644 index 0000000..0765bcd --- /dev/null +++ b/wwwroot/js/router.js @@ -0,0 +1,54 @@ +// ── SPA Router ── + +const routes = {}; +let currentPage = null; + +function registerPage(name, { init, destroy }) { + routes[name] = { init, destroy }; +} + +async function navigateTo(page, params = {}) { + const app = document.getElementById('app'); + + // Alte Page zerstören + if (currentPage && routes[currentPage] && routes[currentPage].destroy) { + routes[currentPage].destroy(); + } + + // Template klonen und einfügen + const tpl = document.getElementById(`tpl-${page}`); + if (!tpl) { + app.innerHTML = `

404

Seite "${page}" nicht gefunden.

`; + currentPage = null; + return; + } + + app.innerHTML = ''; + app.appendChild(tpl.content.cloneNode(true)); + currentPage = page; + + // Nav-Links aktualisieren + document.querySelectorAll('.nav-link').forEach(l => { + l.classList.toggle('active', l.dataset.page === page); + }); + + // URL Hash setzen + const hash = params.id ? `${page}/${params.id}` : page; + history.replaceState(null, '', `#${hash}`); + + // Page initialisieren + if (routes[page] && routes[page].init) { + await routes[page].init(params); + } + + // Back-Buttons verdrahten + document.querySelectorAll('.back-btn[data-page]').forEach(btn => { + btn.addEventListener('click', () => navigateTo(btn.dataset.page)); + }); +} + +function parseHash() { + const hash = location.hash.slice(1) || 'dashboard'; + const parts = hash.split('/'); + return { page: parts[0], id: parts[1] || null }; +} diff --git a/wwwroot/js/session.js b/wwwroot/js/session.js index e0f3135..601f976 100644 --- a/wwwroot/js/session.js +++ b/wwwroot/js/session.js @@ -5,104 +5,3 @@ 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(); } - }); -} diff --git a/wwwroot/prototype.html b/wwwroot/prototype.html new file mode 100644 index 0000000..514388d --- /dev/null +++ b/wwwroot/prototype.html @@ -0,0 +1,50 @@ + + + + + + Gott - Debug Console + + + + + + +
+
+

// gott

+ + +
+ + +
+
+ + +
+
+ +
+ + +
+
+ + + + + + + + +