modularize frontend and implement new css and themes

This commit is contained in:
2026-08-24 13:10:37 +01:00
parent 13116899db
commit 02b7ba317c
14 changed files with 1572 additions and 1768 deletions
+19
View File
@@ -0,0 +1,19 @@
(function () {
function currentSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'macchiato' : 'light';
}
var btn = document.getElementById('theme-toggle');
if (btn) {
btn.addEventListener('click', function () {
var current = document.documentElement.getAttribute('data-theme') || 'light';
var next = current === 'light' ? 'macchiato' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('poc-theme', next);
});
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
if (!localStorage.getItem('poc-theme')) {
document.documentElement.setAttribute('data-theme', currentSystemTheme());
}
});
})();
+187
View File
@@ -0,0 +1,187 @@
var activeSessionId = null;
var supporterWs = null;
var lastTypingSessionId = null;
var supporterTypingActive = false;
function setAppHeight() {
var height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
document.documentElement.style.setProperty('--app-height', height + 'px');
}
setAppHeight();
window.addEventListener('resize', setAppHeight);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', setAppHeight);
window.visualViewport.addEventListener('scroll', setAppHeight);
}
function openSidebar() {
document.getElementById('sidebar').classList.add('open');
document.getElementById('drawer-backdrop').classList.add('visible');
}
function closeSidebar() {
document.getElementById('sidebar').classList.remove('open');
document.getElementById('drawer-backdrop').classList.remove('visible');
}
function selectSession(button) {
document.querySelectorAll('.session-item').forEach(function (b) { b.classList.remove('selected'); });
button.classList.add('selected');
setActiveSession(button.dataset.sessionId, button.dataset.sessionName);
closeSidebar();
}
function setActiveSession(sessionId, name) {
stopSupporterTyping(lastTypingSessionId);
activeSessionId = sessionId;
document.getElementById('active-session-name').textContent = name;
document.getElementById('active-session-name').style.color = '';
document.getElementById('active-session-meta').textContent = 'Session #' + sessionId.slice(0, 8);
document.getElementById('reply-area').classList.add('visible');
document.getElementById('reply-input').value = '';
hideUserTyping();
scrollSupporterMessages();
}
function resetSupporterDashboard() {
stopSupporterTyping(lastTypingSessionId);
activeSessionId = null;
document.getElementById('active-session-name').textContent = 'Select a conversation';
document.getElementById('active-session-name').style.color = 'var(--ink-dim)';
document.getElementById('active-session-meta').textContent = '';
document.getElementById('reply-area').classList.remove('visible');
hideUserTyping();
closeSidebar();
document.getElementById('supporter-messages').innerHTML =
'<div class="empty-state"><p>Chat history was cleared.</p><p>New conversations will appear in the chat list.</p></div>';
htmx.trigger('#session-list', 'refresh');
}
function scrollSupporterMessages() {
var el = document.getElementById('supporter-messages');
if (el) el.scrollTop = el.scrollHeight;
}
function showUserTyping() {
var indicator = document.getElementById('supporter-peer-typing');
if (!indicator) return;
indicator.classList.add('visible');
scrollSupporterMessages();
}
function hideUserTyping() {
var indicator = document.getElementById('supporter-peer-typing');
if (indicator) indicator.classList.remove('visible');
}
function sendSupporterTypingState(state, sessionId) {
sessionId = sessionId || activeSessionId;
if (!sessionId || !supporterWs || supporterWs.readyState !== WebSocket.OPEN) return;
supporterWs.send('typing:supporter:' + state + ':' + sessionId);
}
function startSupporterTyping() {
var input = document.getElementById('reply-input');
if (!activeSessionId || !input || !input.value.trim()) {
stopSupporterTyping(lastTypingSessionId || activeSessionId);
return;
}
if (supporterTypingActive && lastTypingSessionId === activeSessionId) return;
supporterTypingActive = true;
lastTypingSessionId = activeSessionId;
sendSupporterTypingState('start', activeSessionId);
}
function stopSupporterTyping(sessionId) {
sessionId = sessionId || activeSessionId;
if (!supporterTypingActive && !sessionId) return;
var targetSession = sessionId || lastTypingSessionId;
if (targetSession) sendSupporterTypingState('stop', targetSession);
supporterTypingActive = false;
lastTypingSessionId = null;
}
function handleReplyInput() {
var input = document.getElementById('reply-input');
if (input && input.value.trim()) startSupporterTyping();
else stopSupporterTyping(lastTypingSessionId || activeSessionId);
}
document.addEventListener('htmx:afterSwap', function (e) {
if (e.detail.target.id === 'supporter-messages') scrollSupporterMessages();
});
document.getElementById('reply-btn').addEventListener('click', function () {
if (!activeSessionId) return;
var input = document.getElementById('reply-input');
var content = input.value.trim();
if (!content) return;
stopSupporterTyping(activeSessionId);
fetch('/supporter/send/' + activeSessionId, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'content=' + encodeURIComponent(content)
})
.then(function (r) { return r.text(); })
.then(function (html) {
var msgs = document.getElementById('supporter-messages');
msgs.insertAdjacentHTML('beforeend', html);
input.value = '';
scrollSupporterMessages();
setAppHeight();
});
});
document.getElementById('reply-input').addEventListener('keydown', function (e) {
if (e.key === 'Enter') document.getElementById('reply-btn').click();
});
document.getElementById('reply-input').addEventListener('input', handleReplyInput);
document.getElementById('reply-input').addEventListener('focus', function () { setTimeout(setAppHeight, 80); });
document.getElementById('reply-input').addEventListener('blur', function () { setTimeout(setAppHeight, 80); });
function connectSupporterWS() {
var wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
supporterWs = new WebSocket(wsProtocol + '://' + location.host + '/ws/supporter/dashboard');
supporterWs.onmessage = function (e) {
var data = e.data;
if (data === '__reset__') {
resetSupporterDashboard();
return;
}
if (data.indexOf('typing:user:start:') === 0) {
var sessionId = data.replace('typing:user:start:', '');
if (sessionId === activeSessionId) showUserTyping();
return;
}
if (data.indexOf('typing:user:stop:') === 0) {
var sessionId2 = data.replace('typing:user:stop:', '');
if (sessionId2 === activeSessionId) hideUserTyping();
return;
}
if (data.indexOf('typing:supporter:') === 0) return;
htmx.trigger('#session-list', 'refresh');
if (data === activeSessionId) {
hideUserTyping();
fetch('/supporter/messages/' + activeSessionId)
.then(function (r) { return r.text(); })
.then(function (html) {
document.getElementById('supporter-messages').innerHTML = html;
scrollSupporterMessages();
});
}
};
supporterWs.onopen = function () {
if (supporterTypingActive && lastTypingSessionId) sendSupporterTypingState('start', lastTypingSessionId);
};
supporterWs.onclose = function () { setTimeout(connectSupporterWS, 2000); };
}
connectSupporterWS();
+113
View File
@@ -0,0 +1,113 @@
var ws = null;
var userTypingActive = false;
function setAppHeight() {
var height = window.visualViewport ? window.visualViewport.height : window.innerHeight;
document.documentElement.style.setProperty('--app-height', height + 'px');
}
setAppHeight();
window.addEventListener('resize', setAppHeight);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', setAppHeight);
window.visualViewport.addEventListener('scroll', setAppHeight);
}
function scrollMessages() {
var el = document.getElementById('messages');
if (el) el.scrollTop = el.scrollHeight;
}
function showSupporterTyping() {
var indicator = document.getElementById('peer-typing');
if (!indicator) return;
indicator.classList.add('visible');
scrollMessages();
}
function hideSupporterTyping() {
var indicator = document.getElementById('peer-typing');
if (indicator) indicator.classList.remove('visible');
}
function sendUserTypingState(state) {
if (!ws || ws.readyState !== WebSocket.OPEN || !window.__sessionId) return;
ws.send('typing:user:' + state + ':' + window.__sessionId);
}
function startUserTyping() {
if (userTypingActive) return;
userTypingActive = true;
sendUserTypingState('start');
}
function stopUserTyping() {
if (!userTypingActive) return;
sendUserTypingState('stop');
userTypingActive = false;
}
function handleUserTypingInput() {
var input = document.getElementById('chat-input');
if (input && input.value.trim()) startUserTyping();
else stopUserTyping();
}
document.addEventListener('htmx:afterSwap', function (e) {
if (e.detail.target.id === 'messages') scrollMessages();
});
document.addEventListener('htmx:beforeRequest', function (e) {
if (e.detail.elt && e.detail.elt.id === 'chat-form') stopUserTyping();
});
function resetChat() {
stopUserTyping();
var oldWs = ws;
ws = null;
window.__sessionId = null;
userTypingActive = false;
if (oldWs) oldWs.close();
location.reload();
}
function initChat(sessionId) {
window.__sessionId = sessionId;
document.getElementById('chat-session-id').textContent = 'Session #' + sessionId.slice(0, 8);
var wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(wsProtocol + '://' + location.host + '/ws/user/' + sessionId);
ws.onmessage = function (message) {
if (message.data === '__reset__') {
resetChat();
return;
}
if (message.data.startsWith('typing:supporter:start:')) {
showSupporterTyping();
return;
}
if (message.data.startsWith('typing:supporter:stop:')) {
hideSupporterTyping();
return;
}
if (message.data.startsWith('typing:user:')) return;
hideSupporterTyping();
htmx.trigger('#messages', 'refresh');
};
ws.onopen = function () {
htmx.trigger('#messages', 'refresh');
};
ws.onerror = function () {
console.warn('WebSocket error; polling will continue.');
};
var chatInput = document.getElementById('chat-input');
if (chatInput) {
chatInput.addEventListener('focus', function () { setTimeout(setAppHeight, 80); });
chatInput.addEventListener('blur', function () { setTimeout(setAppHeight, 80); });
}
setAppHeight();
}