ai / assets /plugins /loader.js
hadadrjt's picture
ai: Bump script version.
2e0d32d
//
// SPDX-FileCopyrightText: Hadad <[email protected]>
// SPDX-License-Identifier: Apache-2.0
//
// Prism.
Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/';
// WebSocket URL helper.
function createWebSocket() {
return new WebSocket((window.location.protocol === "https:" ? "wss" : "ws") + "//" + window.location.host);
}
// UI elements.
const chatArea = document.getElementById('chatArea');
const chatBox = document.getElementById('chatBox');
const initialContent = document.getElementById('initialContent');
const form = document.getElementById('footerForm');
const input = document.getElementById('userInput');
const btn = document.getElementById('sendBtn');
const stopBtn = document.getElementById('stopBtn');
const promptItems = document.querySelectorAll('.prompt-item');
const mainHeader = document.getElementById('mainHeader');
const chatHeader = document.getElementById('chatHeader');
const homeBtn = document.getElementById('homeBtn');
const clearBtn = document.getElementById('clearBtn');
// Track state.
let socket = null;
let streamMsg = null;
let conversationHistory = [];
let currentAssistantText = "";
let isRequestActive = false;
let abortController = null;
// Render markdown content.
function renderMarkdown(el) {
const raw = el.dataset.text || "";
const html = marked.parse(raw, {
gfm: true,
breaks: true,
smartLists: true,
smartypants: false,
headerIds: false
});
el.innerHTML = '<div class="md-content">' + html + '</div>';
const wrapper = el.querySelector('.md-content');
// Wrap tables.
const tables = wrapper.querySelectorAll('table');
tables.forEach(t => {
if (t.parentNode && t.parentNode.classList && t.parentNode.classList.contains('table-wrapper')) return;
const div = document.createElement('div');
div.className = 'table-wrapper';
t.parentNode.insertBefore(div, t);
div.appendChild(t);
});
// Style horizontal rules.
const hrs = wrapper.querySelectorAll('hr');
hrs.forEach(h => {
if (!h.classList.contains('styled-hr')) {
h.classList.add('styled-hr');
}
});
// Highlight code.
Prism.highlightAllUnder(wrapper);
}
// Chat view.
function enterChatView() {
mainHeader.style.display = 'none';
chatHeader.style.display = 'flex';
chatHeader.setAttribute('aria-hidden', 'false');
chatBox.style.display = 'flex';
initialContent.style.display = 'none';
}
// Home view.
function leaveChatView() {
mainHeader.style.display = 'flex';
chatHeader.style.display = 'none';
chatHeader.setAttribute('aria-hidden', 'true');
chatBox.style.display = 'none';
initialContent.style.display = 'flex';
}
// Chat bubble.
function addMsg(who, text) {
const div = document.createElement('div');
div.className = 'bubble ' + (who === 'user' ? 'bubble-user' : 'bubble-assist');
div.dataset.text = text;
renderMarkdown(div);
chatBox.appendChild(div);
chatBox.style.display = 'flex';
chatBox.scrollTop = chatBox.scrollHeight;
return div;
}
// Clear all chat.
function clearAllMessages() {
stopStream(true);
conversationHistory = [];
currentAssistantText = "";
if (streamMsg) {
const loadingEl = streamMsg.querySelector('.loading');
if (loadingEl) loadingEl.remove();
streamMsg = null;
}
chatBox.innerHTML = "";
input.value = "";
btn.disabled = true;
stopBtn.style.display = 'none';
btn.style.display = 'inline-flex';
enterChatView();
}
// Reconnect WebSocket.
let reconnectAttempts = 0;
function setupWebSocket() {
if (socket) {
socket.onopen = null;
socket.onclose = null;
socket.onmessage = null;
socket.onerror = null;
socket.close();
socket = null;
}
socket = createWebSocket();
socket.onopen = () => {
reconnectAttempts = 0;
};
socket.onclose = () => {
reconnectAttempts++;
// Try reconnecting.
const delay = Math.min(30000, 1000 * Math.pow(2, reconnectAttempts)); // 30 seconds.
setTimeout(setupWebSocket, delay);
};
socket.onmessage = handleSocketMessage;
socket.onerror = () => {
socket.close();
};
}
// Handle incoming socket messages.
function handleSocketMessage(e) {
const data = JSON.parse(e.data);
if (data.type === 'chunk') {
if (streamMsg) {
const loadingEl = streamMsg.querySelector('.loading');
if (loadingEl) loadingEl.remove();
streamMsg.dataset.text += data.chunk;
currentAssistantText = streamMsg.dataset.text || "";
renderMarkdown(streamMsg);
chatBox.scrollTop = chatBox.scrollHeight;
}
} else if (data.type === 'end' || data.type === 'error') {
if (streamMsg) {
const loadingEl = streamMsg.querySelector('.loading');
if (loadingEl) loadingEl.remove();
streamMsg.dataset.done = '1';
if (data.type === 'error') {
streamMsg.dataset.text = data.error || 'An error occurred during the request.';
renderMarkdown(streamMsg);
} else {
conversationHistory.push({ role: 'assistant', content: streamMsg.dataset.text });
}
streamMsg = null;
isRequestActive = false;
abortController = null;
}
btn.style.display = 'inline-flex';
stopBtn.style.display = 'none';
stopBtn.style.pointerEvents = 'auto';
}
}
// Send user message.
async function submitMessage() {
const message = input.value.trim();
if (!message || isRequestActive) return;
enterChatView();
addMsg('user', message);
conversationHistory.push({ role: 'user', content: message });
streamMsg = addMsg('assistant', '');
const loadingEl = document.createElement('span');
loadingEl.className = 'loading';
streamMsg.appendChild(loadingEl);
stopBtn.style.display = 'inline-flex';
btn.style.display = 'none';
input.value = '';
btn.disabled = true;
isRequestActive = true;
// Stopping request.
abortController = new AbortController();
try {
socket.send(JSON.stringify({
type: 'ask',
message,
history: conversationHistory,
abortSignal: true
}));
} catch (error) {
if (streamMsg) {
const loadingEl = streamMsg.querySelector('.loading');
if (loadingEl) loadingEl.remove();
streamMsg.dataset.text = error.message || 'An error occurred during the request.';
renderMarkdown(streamMsg);
streamMsg.dataset.done = '1';
streamMsg = null;
isRequestActive = false;
abortController = null;
}
btn.style.display = 'inline-flex';
stopBtn.style.display = 'none';
}
}
// Stop streaming and cancel the ongoing request.
function stopStream(forceCancel = false) {
if (!isRequestActive) return;
isRequestActive = false;
if (abortController) {
abortController.abort();
abortController = null;
}
// Notify server to stop sending streams / processing.
try {
socket.send(JSON.stringify({ type: 'stop' }));
} catch {}
if (streamMsg && !forceCancel) {
const loadingEl = streamMsg.querySelector('.loading');
if (loadingEl) loadingEl.remove();
streamMsg.dataset.text += '';
renderMarkdown(streamMsg);
streamMsg.dataset.done = '1';
streamMsg = null;
}
stopBtn.style.display = 'none';
btn.style.display = 'inline-flex';
stopBtn.style.pointerEvents = 'auto';
}
// Wait for socket ready.
function sendWhenReady(msgFn) {
if (socket.readyState === WebSocket.OPEN) {
msgFn();
} else {
socket.addEventListener('open', function handler() {
msgFn();
socket.removeEventListener('open', handler);
});
}
}
// Prompts.
promptItems.forEach(p => {
p.addEventListener('click', () => {
input.value = p.dataset.prompt;
sendWhenReady(submitMessage);
});
});
// Submit.
form.addEventListener('submit', e => {
e.preventDefault();
submitMessage();
});
// Stop.
stopBtn.addEventListener('click', () => {
stopBtn.style.pointerEvents = 'none';
stopStream();
});
// Home.
homeBtn.addEventListener('click', () => {
leaveChatView();
});
// Clear messages.
clearBtn.addEventListener('click', () => {
clearAllMessages();
});
// Enable send button only if input has text.
input.addEventListener('input', () => {
btn.disabled = input.value.trim() === '';
});
// Animations.
document.addEventListener('DOMContentLoaded', function () {
AOS.init({
duration: 800,
easing: 'ease-out-cubic',
once: true,
offset: 50
});
});
// Initialize WebSocket connection.
setupWebSocket();