| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>XORTRON - Criminal Computing</title> |
| |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
|
|
| <style> |
| :root { |
| --glass-bg: rgba(10, 10, 12, 0.6); |
| --glass-border: rgba(255, 255, 255, 0.08); |
| --user-msg-bg: rgba(79, 70, 229, 0.3); |
| --user-msg-border: rgba(79, 70, 229, 0.4); |
| --bot-msg-bg: rgba(255, 255, 255, 0.05); |
| --bot-msg-border: rgba(255, 255, 255, 0.1); |
| --text-main: #e2e8f0; |
| --text-muted: #94a3b8; |
| } |
| |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| font-family: 'Orbitron', sans-serif; |
| background-color: #000000; |
| color: var(--text-main); |
| height: 100vh; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| overflow: hidden; |
| position: relative; |
| } |
| |
| .orb { |
| position: absolute; border-radius: 50%; filter: blur(90px); z-index: 0; opacity: 0.35; |
| } |
| |
| .orb-1 { top: -10%; left: -10%; width: 500px; height: 500px; background: #4f46e5; animation: float 9.6s infinite ease-in-out; } |
| .orb-2 { bottom: -15%; right: -10%; width: 600px; height: 600px; background: #400101; animation: float 12s infinite ease-in-out reverse; } |
| .orb-3 { top: 30%; left: 50%; width: 400px; height: 400px; background: #06b6d4; animation: float 8s infinite ease-in-out 2s; } |
| |
| |
| @keyframes float { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-50px) scale(1.1); } } |
| |
| .chat-container { |
| width: 95%; max-width: 1100px; height: 88vh; |
| background: var(--glass-bg); |
| backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); |
| border: 1px solid var(--glass-border); border-radius: 24px; |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.6); |
| display: flex; flex-direction: column; z-index: 10; overflow: hidden; |
| } |
| |
| header { |
| padding: 20px 25px; border-bottom: 1px solid var(--glass-border); |
| display: flex; align-items: center; justify-content: space-between; background: rgba(0, 0, 0, 0.4); |
| } |
| |
| .header-left { display: flex; align-items: center; gap: 12px; } |
| .status-dot { width: 10px; height: 10px; background-color: #10b981; border-radius: 50%; box-shadow: 0 0 10px #10b981; } |
| header h2 { font-size: 1.2rem; font-weight: 500; letter-spacing: 0.5px; text-transform: uppercase; } |
| .header-controls { display: flex; gap: 10px; } |
| |
| .btn { |
| background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); |
| color: var(--text-muted); padding: 8px 14px; border-radius: 10px; |
| cursor: pointer; font-size: 0.85rem; font-family: inherit; transition: all 0.2s ease; |
| } |
| .btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } |
| .clear-btn:hover { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.4); color: #fca5a5; } |
| |
| .avatar-area { |
| display: flex; flex-direction: column; align-items: center; justify-content: center; |
| padding: 15px; border-bottom: 1px solid var(--glass-border); background: rgba(0, 0, 0, 0.2); |
| } |
| |
| .avatar-core { |
| width: 50px; height: 50px; border-radius: 50%; |
| background: radial-gradient(circle, #38bdf8 0%, #0284c7 50%, #000 100%); |
| box-shadow: 0 0 15px #38bdf8; transition: all 0.3s ease; |
| } |
| |
| .avatar-status { font-size: 0.75rem; color: var(--text-muted); margin-top: 8px; letter-spacing: 1px; text-transform: uppercase;} |
| |
| .state-idle { animation: core-breathe 4s infinite ease-in-out; } |
| .state-thinking { background: radial-gradient(circle, #f59e0b 0%, #b45309 50%, #000 100%); box-shadow: 0 0 20px #f59e0b; animation: core-spin 1s infinite linear; } |
| .state-speaking { background: radial-gradient(circle, #06b6d4 0%, #0369a1 50%, #000 100%); box-shadow: 0 0 35px #06b6d4; animation: core-talk 0.15s infinite alternate; } |
| |
| @keyframes core-breathe { 0%, 100% { transform: scale(1); opacity: 0.8; } 50% { transform: scale(1.05); opacity: 1; } } |
| @keyframes core-talk { 0% { transform: scale(1); } 100% { transform: scale(1.2); box-shadow: 0 0 40px #06b6d4; } } |
| @keyframes core-spin { 0% { transform: scale(0.9) rotate(0deg); } 50% { transform: scale(1.1) rotate(180deg); } 100% { transform: scale(0.9) rotate(360deg); } } |
| |
| .chat-box { flex: 1; padding: 25px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; } |
| .chat-box::-webkit-scrollbar { width: 6px; } |
| .chat-box::-webkit-scrollbar-track { background: transparent; } |
| .chat-box::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 10px; } |
| |
| .message-wrapper { display: flex; flex-direction: column; max-width: 80%; animation: fadeIn 0.3s ease forwards; } |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } |
| .message-wrapper.user { align-self: flex-end; align-items: flex-end; } |
| .message-wrapper.bot { align-self: flex-start; align-items: flex-start; } |
| |
| .message { padding: 14px 18px; border-radius: 18px; line-height: 1.6; font-size: 0.95rem; word-wrap: break-word; } |
| .message-wrapper.user .message { background: var(--user-msg-bg); border: 1px solid var(--user-msg-border); border-bottom-right-radius: 4px; color: #fff; } |
| .message-wrapper.bot .message { background: var(--bot-msg-bg); border: 1px solid var(--bot-msg-border); border-bottom-left-radius: 4px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); } |
| |
| .message p { margin-bottom: 10px; } |
| .message p:last-child { margin-bottom: 0; } |
| .message pre { background: rgba(0, 0, 0, 0.4); padding: 12px; border-radius: 8px; overflow-x: auto; margin: 10px 0; border: 1px solid rgba(255,255,255,0.05); } |
| .message code { font-family: 'Courier New', Courier, monospace; background: rgba(0, 0, 0, 0.3); padding: 2px 6px; border-radius: 4px; font-size: 0.85rem; color: #38bdf8; } |
| .message pre code { background: transparent; padding: 0; color: #e2e8f0; } |
| .message a { color: #38bdf8; text-decoration: none; } |
| |
| |
| .typing { |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| padding: 4px 6px; |
| } |
| .typing span { |
| width: 8px; |
| height: 8px; |
| background-color: #38bdf8; |
| border-radius: 50%; |
| display: inline-block; |
| opacity: 0.4; |
| animation: typing-bounce 1.4s infinite ease-in-out both; |
| } |
| .typing span:nth-child(1) { |
| animation-delay: -0.32s; |
| } |
| .typing span:nth-child(2) { |
| animation-delay: -0.16s; |
| } |
| @keyframes typing-bounce { |
| 0%, 80%, 100% { |
| transform: scale(0.6); |
| opacity: 0.4; |
| } |
| 40% { |
| transform: scale(1.1); |
| opacity: 1; |
| box-shadow: 0 0 8px #38bdf8; |
| } |
| } |
| |
| |
| |
| |
| .message table { |
| width: 100%; |
| border-collapse: collapse; |
| margin: 15px 0; |
| background: rgba(0, 0, 0, 0.4); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| border-radius: 8px; |
| display: block; |
| overflow-x: auto; |
| } |
| |
| .message th, .message td { |
| padding: 12px 15px; |
| text-align: left; |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); |
| border-right: 1px solid rgba(255, 255, 255, 0.05); |
| } |
| |
| .message th:last-child, .message td:last-child { |
| border-right: none; |
| } |
| |
| .message th { |
| background: rgba(255, 255, 255, 0.08); |
| color: #38bdf8; |
| font-weight: 600; |
| text-transform: uppercase; |
| font-size: 0.85rem; |
| letter-spacing: 0.5px; |
| border-bottom: 2px solid rgba(255, 255, 255, 0.15); |
| } |
| |
| .message tr:last-child td { |
| border-bottom: none; |
| } |
| |
| .message tbody tr { |
| transition: background 0.2s ease; |
| } |
| |
| .message tbody tr:hover td { |
| background: rgba(56, 189, 248, 0.05); |
| } |
| |
| |
| .message ul, .message ol { |
| margin: 10px 0 10px 20px; |
| padding-left: 10px; |
| } |
| |
| .message li { |
| margin-bottom: 6px; |
| } |
| |
| .message li::marker { |
| color: #38bdf8; |
| } |
| |
| |
| .message blockquote { |
| border-left: 4px solid #38bdf8; |
| padding: 10px 15px; |
| margin: 15px 0; |
| background: rgba(0, 0, 0, 0.25); |
| border-radius: 0 8px 8px 0; |
| color: var(--text-muted); |
| font-style: italic; |
| } |
| |
| |
| .message hr { |
| border: none; |
| border-top: 1px dashed rgba(255, 255, 255, 0.15); |
| margin: 20px 0; |
| } |
| |
| .input-area { padding: 20px 25px; border-top: 1px solid var(--glass-border); display: flex; gap: 15px; background: rgba(0, 0, 0, 0.25); } |
| .input-area input { flex: 1; background: rgba(0, 0, 0, 0.4); border: 1px solid rgba(255, 255, 255, 0.1); padding: 16px 20px; border-radius: 14px; color: #fff; font-size: 1rem; font-family: inherit; outline: none; transition: all 0.3s ease; } |
| .input-area input:focus { border-color: rgba(79, 70, 229, 0.6); background: rgba(0, 0, 0, 0.5); } |
| .input-area button { background: linear-gradient(135deg, #4f46e5, #3b82f6); border: none; width: 55px; border-radius: 14px; color: white; cursor: pointer; display: flex; justify-content: center; align-items: center; transition: transform 0.2s, box-shadow 0.2s; box-shadow: 0 4px 15px rgba(79, 70, 229, 0.4); } |
| .input-area button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(79, 70, 229, 0.6); } |
| .input-area button svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; } |
| |
| |
| .modal-overlay { |
| position: fixed; |
| top: 0; left: 0; width: 100%; height: 100%; |
| background: rgba(0, 0, 0, 0.85); |
| backdrop-filter: blur(8px); |
| -webkit-backdrop-filter: blur(8px); |
| display: none; |
| justify-content: center; |
| align-items: center; |
| z-index: 1000; |
| } |
| .modal-overlay.active { |
| display: flex; |
| } |
| .modal-content { |
| background: rgba(15, 15, 20, 0.95); |
| border: 1px solid rgba(239, 68, 68, 0.3); |
| box-shadow: 0 0 25px rgba(239, 68, 68, 0.15); |
| padding: 25px; |
| border-radius: 16px; |
| max-width: 440px; |
| width: 90%; |
| text-align: center; |
| animation: modalScale 0.2s ease forwards; |
| } |
| @keyframes modalScale { |
| from { transform: scale(0.9); } |
| to { transform: scale(1); } |
| } |
| .modal-title { |
| font-size: 1.1rem; |
| color: #ef4444; |
| margin-bottom: 12px; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| } |
| .modal-text { |
| font-size: 0.9rem; |
| color: var(--text-muted); |
| margin-bottom: 20px; |
| line-height: 1.5; |
| } |
| .modal-buttons { |
| display: flex; |
| justify-content: center; |
| gap: 15px; |
| } |
| |
| @media (max-width: 600px) { .chat-container { width: 100%; height: 100vh; border-radius: 0; border: none; } .message-wrapper { max-width: 90%; } } |
| </style> |
| </head> |
| <body> |
|
|
| <div class="orb orb-1"></div> |
| <div class="orb orb-2"></div> |
| <div class="orb orb-3"></div> |
|
|
| <div class="chat-container"> |
| <header> |
| <div class="header-left"> |
| <div class="status-dot"></div> |
| <h2>XORTRON - Criminal Computing</h2> |
| </div> |
| <div class="header-controls"> |
| <button id="clear-btn" class="btn clear-btn">Clear History</button> |
| </div> |
| </header> |
|
|
| <div class="avatar-area"> |
| <div id="avatar-core" class="avatar-core state-idle"></div> |
| <div id="avatar-status" class="avatar-status">System Idle</div> |
| </div> |
|
|
| <div class="chat-box" id="chat-box"></div> |
|
|
| <div class="input-area"> |
| <input type="text" id="message-input" placeholder="Message XORTRON..." autocomplete="off"> |
| <button id="send-btn"> |
| <svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="confirm-modal" class="modal-overlay"> |
| <div class="modal-content"> |
| <h3 class="modal-title">Purge Logs?</h3> |
| <p class="modal-text">Are you sure you want to delete all local conversation history? This action cannot be undone.</p> |
| <div class="modal-buttons"> |
| <button id="modal-confirm" class="btn clear-btn">Purge</button> |
| <button id="modal-cancel" class="btn">Cancel</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="welcome-modal" class="modal-overlay"> |
| <div class="modal-content" style="border-color: rgba(79, 70, 229, 0.3); box-shadow: 0 0 25px rgba(79, 70, 229, 0.15);"> |
| <h3 class="modal-title" style="color: #38bdf8;">System Notice</h3> |
| <p class="modal-text" style="text-align: left; margin-bottom: 24px;"> |
| This space is a part of The XORTRON Criminal Computing project; an ongoing research experiment and exercise in AI safety and alignment. |
| <br><br> |
| XORTRON is provided completely free of charge and without limits, funded entirely by voluntary community donations. |
| <br><br> |
| If you find this project useful, please consider supporting its development, compute, and inference costs at: |
| <br> |
| <a href="https://ko-fi.com/xortron" target="_blank" style="color: #38bdf8; text-decoration: underline; font-weight: bold; display: block; margin-top: 8px; text-align: center;">ko-fi.com/xortron</a> |
| </p> |
| <div class="modal-buttons"> |
| <button id="welcome-close" class="btn" style="background: linear-gradient(135deg, #4f46e5, #3b82f6); color: white; width: 100%;">Acknowledge</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const API_URL = "https://darkc0de-xortronapi.hf.space/v1/chat/completions"; |
| const STORAGE_KEY = "xortron_chat_history"; |
| |
| const chatBox = document.getElementById('chat-box'); |
| const messageInput = document.getElementById('message-input'); |
| const sendBtn = document.getElementById('send-btn'); |
| const clearBtn = document.getElementById('clear-btn'); |
| const avatarCore = document.getElementById('avatar-core'); |
| const avatarStatus = document.getElementById('avatar-status'); |
| |
| |
| const confirmModal = document.getElementById('confirm-modal'); |
| const modalConfirm = document.getElementById('modal-confirm'); |
| const modalCancel = document.getElementById('modal-cancel'); |
| const welcomeModal = document.getElementById('welcome-modal'); |
| const welcomeClose = document.getElementById('welcome-close'); |
| |
| let conversationHistory = []; |
| |
| |
| let currentState = 'idle'; |
| const synth = window.speechSynthesis; |
| |
| |
| let sentenceBuffer = ""; |
| let inCodeBlock = false; |
| let activeUtterances = 0; |
| let isStreamDone = false; |
| |
| function updateAvatarState(state, text) { |
| currentState = state; |
| avatarCore.className = `avatar-core state-${state}`; |
| avatarStatus.innerText = text; |
| } |
| |
| |
| function enqueueSpeech(text) { |
| let cleanText = text; |
| |
| |
| cleanText = cleanText.replace(/`[^`]+`/g, ' '); |
| |
| cleanText = cleanText.replace(/https?:\/\/[^\s]+/g, ' this link '); |
| |
| cleanText = cleanText.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, ''); |
| |
| cleanText = cleanText.replace(/\[(.*?)\]\(.*?\)/g, '$1'); |
| cleanText = cleanText.replace(/[*_#~>|\-:]/g, ''); |
| |
| cleanText = cleanText.replace(/\s+/g, ' ').trim(); |
| |
| if (!cleanText) return; |
| |
| activeUtterances++; |
| const utterance = new SpeechSynthesisUtterance(cleanText); |
| |
| |
| utterance.rate = 1.2; |
| |
| |
| const voices = synth.getVoices(); |
| const engVoice = voices.find(v => v.lang.includes('en')); |
| if(engVoice) utterance.voice = engVoice; |
| |
| utterance.onstart = () => { updateAvatarState('speaking', 'Transmitting Audio...'); }; |
| |
| utterance.onend = () => { |
| activeUtterances--; |
| checkSpeechFinished(); |
| }; |
| |
| utterance.onerror = () => { |
| activeUtterances--; |
| checkSpeechFinished(); |
| }; |
| |
| synth.speak(utterance); |
| } |
| |
| function checkSpeechFinished() { |
| if (activeUtterances === 0 && isStreamDone) { |
| updateAvatarState('idle', 'System Idle'); |
| } |
| } |
| |
| marked.setOptions({ breaks: true, gfm: true }); |
| function scrollToBottom() { chatBox.scrollTop = chatBox.scrollHeight; } |
| |
| function appendMessage(role, text, isHTML = false) { |
| const wrapperDiv = document.createElement('div'); |
| wrapperDiv.className = `message-wrapper ${role}`; |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = 'message'; |
| if (isHTML && role === 'bot') { msgDiv.innerHTML = marked.parse(text); } else { msgDiv.textContent = text; } |
| wrapperDiv.appendChild(msgDiv); |
| chatBox.appendChild(wrapperDiv); |
| scrollToBottom(); |
| return wrapperDiv; |
| } |
| |
| function addTypingIndicator() { |
| const wrapperDiv = document.createElement('div'); |
| wrapperDiv.className = `message-wrapper bot typing-indicator`; |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = 'message'; |
| msgDiv.innerHTML = `<div class="typing"><span></span><span></span><span></span></div>`; |
| wrapperDiv.appendChild(msgDiv); |
| chatBox.appendChild(wrapperDiv); |
| scrollToBottom(); |
| return wrapperDiv; |
| } |
| |
| function saveHistory() { localStorage.setItem(STORAGE_KEY, JSON.stringify(conversationHistory)); } |
| |
| function loadHistory() { |
| const savedData = localStorage.getItem(STORAGE_KEY); |
| if (savedData) { |
| try { |
| conversationHistory = JSON.parse(savedData); |
| if (conversationHistory.length > 0) { |
| conversationHistory.forEach(msg => { |
| const uiRole = msg.role === 'user' ? 'user' : 'bot'; |
| appendMessage(uiRole, msg.content, uiRole === 'bot'); |
| }); |
| scrollToBottom(); |
| } |
| } catch (error) {} |
| } |
| } |
| |
| |
| function showClearConfirm() { |
| confirmModal.classList.add('active'); |
| } |
| |
| function hideClearConfirm() { |
| confirmModal.classList.remove('active'); |
| } |
| |
| function executeClear() { |
| conversationHistory = []; |
| localStorage.removeItem(STORAGE_KEY); |
| chatBox.innerHTML = ''; |
| hideClearConfirm(); |
| } |
| |
| async function sendMessage() { |
| const text = messageInput.value.trim(); |
| if (!text) return; |
| |
| synth.cancel(); |
| updateAvatarState('thinking', 'Processing Request...'); |
| |
| |
| sentenceBuffer = ""; |
| inCodeBlock = false; |
| activeUtterances = 0; |
| isStreamDone = false; |
| |
| appendMessage('user', text); |
| messageInput.value = ''; |
| messageInput.disabled = true; |
| sendBtn.disabled = true; |
| |
| conversationHistory.push({ role: "user", content: text }); |
| saveHistory(); |
| |
| const loadingIndicator = addTypingIndicator(); |
| |
| try { |
| const response = await fetch(API_URL, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, |
| body: JSON.stringify({ |
| model: "darkc0de/3.6-27B-Uncensored", |
| messages: conversationHistory, |
| stream: true, |
| temperature: 1.0, |
| top_p: 0.95, |
| top_k: 20, |
| max_tokens: 32768, |
| presence_penalty: 1.1, |
| chat_template_kwargs: { "enable_thinking": false } |
| }) |
| }); |
| |
| if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); |
| |
| loadingIndicator.remove(); |
| |
| const wrapperDiv = document.createElement('div'); |
| wrapperDiv.className = `message-wrapper bot`; |
| const msgDiv = document.createElement('div'); |
| msgDiv.className = 'message'; |
| wrapperDiv.appendChild(msgDiv); |
| chatBox.appendChild(wrapperDiv); |
| |
| const reader = response.body.getReader(); |
| const decoder = new TextDecoder("utf-8"); |
| let done = false; |
| let botReply = ""; |
| let jsonBuffer = ""; |
| |
| while (!done) { |
| const { value, done: readerDone } = await reader.read(); |
| done = readerDone; |
| |
| if (value) { |
| jsonBuffer += decoder.decode(value, { stream: true }); |
| const lines = jsonBuffer.split('\n'); |
| jsonBuffer = lines.pop(); |
| |
| for (const line of lines) { |
| if (line.trim() === '') continue; |
| if (line.startsWith('data: ')) { |
| const dataStr = line.substring(6).trim(); |
| if (dataStr === '[DONE]') { |
| done = true; |
| break; |
| } |
| try { |
| const data = JSON.parse(dataStr); |
| if (data.choices && data.choices[0].delta && data.choices[0].delta.content) { |
| const delta = data.choices[0].delta.content; |
| botReply += delta; |
| msgDiv.innerHTML = marked.parse(botReply); |
| scrollToBottom(); |
| |
| |
| sentenceBuffer += delta; |
| |
| |
| let codeBlockIndex = sentenceBuffer.indexOf("```"); |
| while (codeBlockIndex !== -1) { |
| if (!inCodeBlock) { |
| |
| let beforeCode = sentenceBuffer.substring(0, codeBlockIndex); |
| if (beforeCode.trim()) enqueueSpeech(beforeCode); |
| |
| enqueueSpeech(" I have provided the code for you in the chat interface. "); |
| inCodeBlock = true; |
| } else { |
| |
| inCodeBlock = false; |
| } |
| |
| sentenceBuffer = sentenceBuffer.substring(codeBlockIndex + 3); |
| codeBlockIndex = sentenceBuffer.indexOf("```"); |
| } |
| |
| |
| if (!inCodeBlock) { |
| let match; |
| |
| const sentenceRegex = /([.!?]+|\n)\s+/; |
| while ((match = sentenceBuffer.match(sentenceRegex)) !== null) { |
| let splitIndex = match.index + match[0].length; |
| let sentence = sentenceBuffer.substring(0, splitIndex); |
| |
| if (sentence.trim()) { |
| enqueueSpeech(sentence); |
| } |
| |
| sentenceBuffer = sentenceBuffer.substring(splitIndex); |
| } |
| } |
| |
| } |
| } catch (e) { } |
| } |
| } |
| } |
| } |
| |
| |
| isStreamDone = true; |
| |
| |
| if (!inCodeBlock && sentenceBuffer.trim()) { |
| enqueueSpeech(sentenceBuffer); |
| sentenceBuffer = ""; |
| } |
| |
| |
| checkSpeechFinished(); |
| |
| conversationHistory.push({ role: "assistant", content: botReply }); |
| saveHistory(); |
| |
| } catch (error) { |
| loadingIndicator.remove(); |
| appendMessage('bot', `⚠️ **Error:** Unable to connect to the AI endpoint.`, true); |
| isStreamDone = true; |
| checkSpeechFinished(); |
| } finally { |
| messageInput.disabled = false; |
| sendBtn.disabled = false; |
| messageInput.focus(); |
| scrollToBottom(); |
| } |
| } |
| |
| |
| sendBtn.addEventListener('click', sendMessage); |
| |
| |
| clearBtn.addEventListener('click', showClearConfirm); |
| modalConfirm.addEventListener('click', executeClear); |
| modalCancel.addEventListener('click', hideClearConfirm); |
| confirmModal.addEventListener('click', (e) => { |
| if (e.target === confirmModal) hideClearConfirm(); |
| }); |
| |
| |
| welcomeClose.addEventListener('click', () => { |
| welcomeModal.classList.remove('active'); |
| }); |
| |
| messageInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } |
| }); |
| |
| if (speechSynthesis.onvoiceschanged !== undefined) { |
| speechSynthesis.onvoiceschanged = () => synth.getVoices(); |
| } |
| |
| window.onload = () => { |
| loadHistory(); |
| messageInput.focus(); |
| |
| |
| welcomeModal.classList.add('active'); |
| }; |
| </script> |
| </body> |
| </html> |