diff --git a/client/js/api.js b/client/js/api.js index c523bbc..7fc8770 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -20,5 +20,4 @@ createSession: (profileId) => request('POST', '/sessions', { profile_id: profileId }), deleteSession: (id) => request('DELETE', `/sessions/${id}`), pinSession: (id, pinned) => request('PATCH', `/sessions/${id}/pin`, { pinned }), - sendMessage: (id, text) => request('POST', `/sessions/${id}/messages`, { content: text }), }; diff --git a/client/js/app.js b/client/js/app.js index eb6fa79..0426962 100644 --- a/client/js/app.js +++ b/client/js/app.js @@ -44,8 +44,7 @@ [profiles, sessions] = await Promise.all([api.getProfiles(), api.getSessions()]); - // Attach preview text from message history - sessions = await Promise.all(sessions.map(enrichSession)); + sessions = sessions.map(enrichSession); renderProfiles(profileSelect, profiles); rerenderSidebar(); @@ -290,7 +289,7 @@ return profiles.find(p => p.id === profileId)?.name ?? profileId; } -async function enrichSession(s) { +function enrichSession(s) { return { ...s, profile_name: profileName(s.profile_id), preview: s.preview || '' }; } @@ -340,19 +339,19 @@ function renderPreviewStrip() { previewStrip.innerHTML = ''; - pendingImages.forEach((dataUrl, i) => { + pendingImages.forEach((dataUrl) => { const wrap = document.createElement('div'); wrap.className = 'img-thumb-wrap'; const img = document.createElement('img'); - img.src = dataUrl; // full data URL + img.src = dataUrl; img.className = 'img-thumb'; const btn = document.createElement('button'); btn.className = 'img-thumb-remove'; btn.textContent = '×'; btn.addEventListener('click', () => { - pendingImages.splice(i, 1); + pendingImages.splice(pendingImages.indexOf(dataUrl), 1); renderPreviewStrip(); }); diff --git a/client/js/chat.js b/client/js/chat.js index 74cfa09..5d2eb47 100644 --- a/client/js/chat.js +++ b/client/js/chat.js @@ -1,7 +1,8 @@ /** Chat area DOM helpers. */ -import { marked } from 'https://esm.sh/marked@12'; -import hljs from 'https://esm.sh/highlight.js@11'; +import { marked } from 'https://esm.sh/marked@12'; +import hljs from 'https://esm.sh/highlight.js@11'; +import { esc, timeLabel } from './utils.js'; // ── Markdown setup ──────────────────────────────────────────────────────────── @@ -30,25 +31,6 @@ image_view: '🖼️', }; -// ── Helpers ─────────────────────────────────────────────────────────────────── - -function esc(str) { - return String(str ?? '') - .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -} - -function timeLabel(iso) { - if (!iso) return ''; - const d = new Date(iso); - if (isNaN(d)) return ''; - const diff = Date.now() - d; - if (diff < 60_000) return 'just now'; - if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; - if (diff < 86_400_000) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - return d.toLocaleDateString(); -} - // ── Public API ──────────────────────────────────────────────────────────────── /** diff --git a/client/js/sidebar.js b/client/js/sidebar.js index abff932..4782f35 100644 --- a/client/js/sidebar.js +++ b/client/js/sidebar.js @@ -1,21 +1,6 @@ /** Sidebar DOM helpers. */ -function esc(str) { - return String(str ?? '') - .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -} - -function timeLabel(iso) { - if (!iso) return ''; - const d = new Date(iso); - if (isNaN(d)) return ''; - const diff = Date.now() - d; - if (diff < 60_000) return 'just now'; - if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; - if (diff < 86_400_000) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - return d.toLocaleDateString(); -} +import { esc, timeLabel } from './utils.js'; export function renderProfiles(selectEl, profiles) { selectEl.innerHTML = profiles diff --git a/client/js/utils.js b/client/js/utils.js new file mode 100644 index 0000000..edd62ba --- /dev/null +++ b/client/js/utils.js @@ -0,0 +1,18 @@ +/** Shared utilities. */ + +export function esc(str) { + return String(str ?? '') + .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +export function timeLabel(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d)) return ''; + const diff = Date.now() - d; + if (diff < 60_000) return 'just now'; + if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; + if (diff < 86_400_000) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + return d.toLocaleDateString(); +} diff --git a/client/js/ws.js b/client/js/ws.js index 6761259..91b51d7 100644 --- a/client/js/ws.js +++ b/client/js/ws.js @@ -2,12 +2,10 @@ export class WsClient { #ws = null; - #sessionId = null; #handlers = {}; connect(sessionId, handlers) { this.disconnect(); - this.#sessionId = sessionId; this.#handlers = handlers; const proto = location.protocol === 'https:' ? 'wss' : 'ws'; @@ -32,7 +30,6 @@ disconnect() { this.#ws?.close(); this.#ws = null; - this.#sessionId = null; } get ready() { diff --git a/client/style.css b/client/style.css index c0ce6a3..e1ab084 100644 --- a/client/style.css +++ b/client/style.css @@ -19,6 +19,10 @@ --error-bg: #1f0e0e; --error-border: #5c1a1a; --error-text: #f87171; + --thinking-bg: #111820; + --thinking-border: #1e3a5f; + --thinking-text: #6b9fd4; + --thinking-pre-text: #5a8ab0; --input-bg: #1e1e1e; --radius: 12px; --shadow: 0 1px 4px rgba(0,0,0,0.4); @@ -319,11 +323,11 @@ .thinking-card { align-self: flex-start; max-width: 84%; - background: #111820; - border: 1px solid #1e3a5f; + background: var(--thinking-bg); + border: 1px solid var(--thinking-border); border-radius: var(--radius); font-size: 12px; - color: #6b9fd4; + color: var(--thinking-text); } .thinking-header { @@ -343,7 +347,7 @@ .thinking-card.open .thinking-header::after { content: '‹'; font-size: 16px; opacity: 0.5; } .thinking-body { - border-top: 1px solid #1e3a5f; + border-top: 1px solid var(--thinking-border); padding: 8px 12px; display: none; } @@ -361,7 +365,7 @@ word-break: break-word; max-height: 320px; overflow-y: auto; - color: #5a8ab0; + color: var(--thinking-pre-text); } /* Typing indicator */