Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SmartChild</title> | |
| <link rel="stylesheet" href="/xp.css"> | |
| <style> | |
| html, body { | |
| overscroll-behavior-y: none; | |
| touch-action: pan-x pan-y; | |
| } | |
| :root { | |
| --bg-color: #008080; | |
| --window-bg: #ece9d8; | |
| --accent: #39ff14; | |
| --ai-color: #ff0055; | |
| --user-color: #0066ff; | |
| } | |
| body { | |
| background: var(--bg-color); | |
| font-family: 'Tahoma', sans-serif; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| height: 85vh; | |
| margin: 0; | |
| padding: 20px; | |
| box-sizing: border-box; | |
| overflow: hidden; | |
| position: fixed; | |
| width: 100%; | |
| } | |
| /* Standard Window - Fixed Height to prevent "growing" */ | |
| .window { | |
| width: 90%; | |
| max-width: 440px; | |
| height: 480px; /* Locked height */ | |
| background: var(--window-bg) ; | |
| box-shadow: 4px 4px 10px rgba(0,0,0,0.3); | |
| box-sizing: border-box; | |
| display: flex; | |
| flex-direction: column; | |
| transition: all 0.2s ease-in-out; | |
| } | |
| /* Modal/Loading Windows - Reset so they aren't 520px tall */ | |
| #modal-overlay .window, | |
| #loading-overlay .window { | |
| height: auto ; | |
| min-height: 100px; | |
| } | |
| /* Fullscreen Mode */ | |
| .window.fullscreen { | |
| position: fixed; | |
| top: 0; left: 0; | |
| width: 100% ; | |
| height: 100% ; | |
| max-width: none ; | |
| z-index: 300; | |
| margin: 0; | |
| } | |
| .window-body { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 0; /* Critical for inner scrolling */ | |
| padding: 8px ; | |
| margin: 0; | |
| } | |
| .chat-box { | |
| flex: 1; /* This tells it to fill the available space */ | |
| min-height: 0; /* THE FIX: Allows the box to shrink smaller than its content */ | |
| height: auto ; /* Removes any old fixed heights */ | |
| max-height: none ; /* Prevents it from fighting the flexbox */ | |
| scroll-behavior: smooth; | |
| background: white; | |
| border: 2px inset #d5d5d5; | |
| overflow-y: auto; | |
| padding: 12px; | |
| padding-bottom: 2px ; | |
| margin-bottom: 10px; | |
| font-size: 16px; | |
| color: black; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .chat-box::after { | |
| content: ""; | |
| display: block; | |
| min-height: 1px; /* Extra "invisible" space after the last message */ | |
| width: 100%; | |
| flex-shrink: 0; | |
| } | |
| /* The Input Area */ | |
| #user-input { | |
| height: 30px; /* Increased from 26px to fit the bigger font */ | |
| box-sizing: border-box; | |
| border: 1px solid black; | |
| background: white; | |
| padding: 4px; /* Added a little padding so text isn't touching the walls */ | |
| overflow-y: auto; | |
| resize: none; | |
| width: 100%; | |
| font-family: 'Tahoma', sans-serif; | |
| font-size: 16px ; /* Using 18px is usually the "sweet spot" for mobile */ | |
| line-height: 1.2; | |
| transition: height 0.2s ease-in-out; | |
| } | |
| /* Expand Logic: Input grows, Chat Box stays flexible (shrinks) */ | |
| .window.input-expanded #user-input { | |
| height: 130px ; | |
| } | |
| /* Fullscreen tweaks to keep things proportional */ | |
| .window.fullscreen.input-expanded .chat-box { | |
| flex: 1 ; | |
| } | |
| .window.fullscreen.input-expanded #user-input { | |
| max-height: 50vh; | |
| } | |
| /* UI Spacing */ | |
| #expand-toggle { margin-left: 100px ; cursor: pointer; } | |
| label[for="expand-toggle"] { cursor: pointer; white-space: nowrap; } | |
| .message { margin-bottom: 10px; line-height: 1.4; } | |
| .user { color: var(--user-color); font-weight: bold; } | |
| .ai { color: var(--ai-color); font-weight: bold; } | |
| /* Buttons & Overlays */ | |
| #export-btn { | |
| background: #0066ff ; color: black ; | |
| width: 22px ; height: 22px ; | |
| display: flex; align-items: center; justify-content: center; | |
| border: 1px solid #808080 ; | |
| box-shadow: inset 1px 1px #fff, inset -1px -1px #808080 ; | |
| } | |
| #modal-overlay, #loading-overlay { | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| display: flex; justify-content: center; align-items: center; | |
| } | |
| #modal-overlay { background: rgba(0,0,0,0.3); z-index: 400; display: none; } | |
| #loading-overlay { background: rgba(0,0,0,0.8); z-index: 500; } | |
| .controls-row { display: flex; justify-content: space-between; margin-top: 10px; gap: 5px; } | |
| .status-bar { display: flex; gap: 10px; margin-top: 5px; } | |
| .status-bar-field { white-space: nowrap; margin: 0; font-size: 11px; } | |
| /* Code Block Styling */ | |
| .code-container { | |
| margin: 8px 0; background: #ece9d8; padding: 10px; | |
| border: 1px solid #808080; font-family: 'Courier New', monospace; | |
| position: relative; white-space: pre-wrap; font-size: 11px; word-break: break-all; | |
| } | |
| .copy-btn { | |
| position: absolute; top: 2px; right: 2px; font-size: 9px; | |
| padding: 4px 6px; cursor: pointer; background: white; border: 1px solid #808080; | |
| } | |
| .title-bar-text { | |
| color: white; | |
| font-weight: bold; | |
| letter-spacing: 0.5px; | |
| /* ADJUST THIS NUMBER TO TWEAK SIZE */ | |
| font-size: 16px; | |
| /* This makes sure it looks like classic Windows text */ | |
| text-shadow: 1px 1px #000; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="modal-overlay"> | |
| <div class="window" style="width: 300px;"> | |
| <div class="title-bar"> | |
| <div class="title-bar-text">Confirm Action</div> | |
| </div> | |
| <div class="window-body"> | |
| <p style="text-align: center; margin-bottom: 20px;">Are you sure you want to clear the chat?</p> | |
| <div style="display: flex; flex-direction: column; gap: 8px;"> | |
| <button id="modal-clear-save">Clear and Save</button> | |
| <button id="modal-clear">Clear</button> | |
| <button id="modal-cancel">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="loading-overlay"> | |
| <div class="window" style="width: 300px"> | |
| <div class="title-bar"><div class="title-bar-text">Connecting to the server...</div></div> | |
| <div class="window-body"> | |
| <p id="status-text">Uploading SmartChild's brain...</p> | |
| <progress id="load-progress" value="0" max="100"></progress> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="window" id="main-window"> | |
| <div class="title-bar"> | |
| <div class="title-bar-text">Instant Messenger</div> | |
| <div class="title-bar-controls" style="display: flex; align-items: center;"> | |
| <button id="export-btn" title="Export Chat">💾</button> | |
| <button aria-label="Minimize" id="minimize-btn"></button> | |
| <button aria-label="Maximize" id="maximize-btn"></button> | |
| <button aria-label="Close" id="close-trigger"></button> | |
| </div> | |
| </div> | |
| <div class="window-body"> | |
| <div class="chat-box" id="chat-box"> | |
| <div class="message"><span class="ai">SmartChild:</span> *static* ...lol! I'm back.<br><br> Use me offline or in airplane mode!</div> | |
| </div> | |
| <div class="field-row-stacked"> | |
| <textarea id="user-input" placeholder="Type a message..." autofocus style="width: 100%; font-family: 'Tahoma', sans-serif; font-size: 13px;"></textarea> | |
| </div> | |
| <div class="controls-row"> | |
| <div style="display: flex; align-items: center;"> | |
| <input type="checkbox" id="sound-toggle" checked> | |
| <label for="sound-toggle" style="font-size: 11px;">Sound</label> | |
| <div style="width: 22px; flex-shrink: 0;"></div> | |
| <input type="checkbox" id="expand-toggle"> | |
| <label for="expand-toggle" style="font-size: 11px; margin-left: 4px;">Expand</label> | |
| </div> | |
| <button id="send-btn" style="width: 80px; font-weight: bold;">Send</button> | |
| </div> | |
| </div> | |
| <div class="status-bar"> | |
| <p class="status-bar-field">Status: Online</p> | |
| <p class="status-bar-field" id="tps-display">Speed: 0 tps</p> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { Wllama } from './wllama.js'; | |
| // 1. Define your Space's direct address | |
| const SPACE_URL = "https://macwhisperer-smartchild.static.hf.space"; | |
| // 2. Use absolute URLs for everything | |
| const MODEL_URL = `${SPACE_URL}/tinyllama-1.1b-Q4_K_M.gguf`; | |
| const CONFIG = { | |
| "single-thread/wllama.wasm": `${SPACE_URL}/wasm/single-thread/wllama.wasm`, | |
| "multi-thread/wllama.wasm": `${SPACE_URL}/wasm/multi-thread/wllama.wasm`, | |
| }; | |
| const wllama = new Wllama(CONFIG); | |
| const chatBox = document.getElementById('chat-box'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const exportBtn = document.getElementById('export-btn'); | |
| const closeTrigger = document.getElementById('close-trigger'); | |
| const tpsDisplay = document.getElementById('tps-display'); | |
| const soundToggle = document.getElementById('sound-toggle'); | |
| const loadingOverlay = document.getElementById('loading-overlay'); | |
| const progressBar = document.getElementById('load-progress'); | |
| const modalOverlay = document.getElementById('modal-overlay'); | |
| const modalCancel = document.getElementById('modal-cancel'); | |
| const modalClear = document.getElementById('modal-clear'); | |
| const modalClearSave = document.getElementById('modal-clear-save'); | |
| const expandToggle = document.getElementById('expand-toggle'); | |
| // --- UI CONTROLS --- | |
| const mainWindow = document.getElementById('main-window'); | |
| const maximizeBtn = document.getElementById('maximize-btn'); | |
| const minimizeBtn = document.getElementById('minimize-btn'); | |
| // Maximize/Fullscreen Logic | |
| if (maximizeBtn) { | |
| maximizeBtn.addEventListener('click', () => { | |
| mainWindow.classList.toggle('fullscreen'); | |
| // Wait for resize, then snap chat to bottom | |
| setTimeout(() => { | |
| chatBox.scrollTo({ top: chatBox.scrollHeight, behavior: 'smooth' }); | |
| }, 300); | |
| }); | |
| } | |
| // Minimize Button | |
| if (minimizeBtn) { | |
| minimizeBtn.addEventListener('click', () => { | |
| mainWindow.classList.remove('fullscreen'); | |
| }); | |
| } | |
| // Expand Input Logic | |
| if (expandToggle) { | |
| expandToggle.addEventListener('change', () => { | |
| if (expandToggle.checked) { | |
| mainWindow.classList.add('input-expanded'); | |
| } else { | |
| mainWindow.classList.remove('input-expanded'); | |
| } | |
| // Wait for expand animation, then snap scroll & focus | |
| setTimeout(() => { | |
| chatBox.scrollTo({ top: chatBox.scrollHeight, behavior: 'smooth' }); | |
| if (expandToggle.checked) userInput.focus(); | |
| }, 300); | |
| }); | |
| } | |
| // iPad Keyboard Snap Logic | |
| if (userInput) { | |
| userInput.addEventListener('focus', () => { | |
| setTimeout(() => { | |
| userInput.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'center' | |
| }); | |
| }, 300); | |
| }); | |
| } | |
| const sounds = { | |
| open: new Audio('door_open.mp3'), | |
| receive: new Audio('msg_receive.mp3'), | |
| send: new Audio('msg_send.mp3') | |
| }; | |
| let abortController = null; | |
| function escapeHTML(str) { | |
| return str.replace(/[&<>"']/g, function(m) { | |
| return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]; | |
| }); | |
| } | |
| function formatText(text) { | |
| const parts = text.split('```'); | |
| let result = ""; | |
| for (let i = 0; i < parts.length; i++) { | |
| if (i % 2 === 0) { | |
| result += parts[i]; | |
| } else { | |
| let codeContent = parts[i].replace(/^[a-zA-Z]+\n/, ''); | |
| const isLastPart = (i === parts.length - 1); | |
| const cleanCode = codeContent.trim(); | |
| if (isLastPart) { | |
| result += `<div class="code-container"><code>${escapeHTML(cleanCode)}</code></div>`; | |
| } else { | |
| result += `<div class="code-container"><button class="copy-btn" onclick="copyCode(this)">Copy</button><code>${escapeHTML(cleanCode)}</code></div>`; | |
| } | |
| } | |
| } | |
| return result; | |
| } | |
| window.copyCode = (btn) => { | |
| const codeElement = btn.nextElementSibling; | |
| navigator.clipboard.writeText(codeElement.innerText); | |
| btn.innerText = "Copied!"; | |
| setTimeout(() => btn.innerText = "Copy", 1500); | |
| }; | |
| function exportChat() { | |
| let chatText = "--- SmartChild Chat Export ---\n\n"; | |
| const messages = chatBox.querySelectorAll('.message'); | |
| messages.forEach(msg => { | |
| const senderSpan = msg.querySelector('span:first-child'); | |
| const textSpan = msg.querySelector('span:last-child'); | |
| if (senderSpan && textSpan) { | |
| chatText += `${senderSpan.innerText} ${textSpan.innerText}\n\n`; | |
| } | |
| }); | |
| const blob = new Blob([chatText], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `SmartChild_Chat_${new Date().getTime()}.txt`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| function clearChat() { | |
| chatBox.innerHTML = `<div class="message"><span class="ai">SmartChild:</span> *static* ...lol! I'm back.<br><br> Use me offline or in airplane mode!</div>`; | |
| modalOverlay.style.display = 'none'; | |
| } | |
| exportBtn.addEventListener('click', exportChat); | |
| closeTrigger.addEventListener('click', () => modalOverlay.style.display = 'flex'); | |
| modalCancel.addEventListener('click', () => modalOverlay.style.display = 'none'); | |
| modalClear.addEventListener('click', clearChat); | |
| modalClearSave.addEventListener('click', () => { exportChat(); clearChat(); }); | |
| function playSound(name) { | |
| if (soundToggle.checked) sounds[name].play().catch(() => {}); | |
| } | |
| async function init() { | |
| try { | |
| await wllama.loadModelFromUrl(MODEL_URL, { | |
| n_ctx: 2048, | |
| progressCallback: ({ loaded, total }) => { | |
| progressBar.value = (loaded / total) * 100; | |
| } | |
| }); | |
| loadingOverlay.style.display = 'none'; | |
| playSound('open'); | |
| } catch (err) { | |
| document.getElementById('status-text').innerText = "Load Failed. Try a different browser or newer device."; | |
| console.error(err); | |
| } | |
| } | |
| async function sendMessage() { | |
| if (sendBtn.innerText === "End") { | |
| if (abortController) abortController.abort(); | |
| return; | |
| } | |
| const text = userInput.value.trim(); | |
| if (!text) return; | |
| appendMessage('user', 'You', text); | |
| userInput.value = ''; | |
| playSound('send'); | |
| const typingId = appendMessage('ai', 'SmartChild', '...'); | |
| const displaySpan = document.getElementById(typingId); | |
| sendBtn.innerText = "End"; | |
| sendBtn.style.color = "red"; | |
| abortController = new AbortController(); | |
| const prompt = `<|im_start|>system\nYou are SmartChild, an AIM bot from the 2000s, but alive in 2026. You are a helpful assistant who is silly and uses slang like lol. Keep responses shorter and fun.<|im_end|>\n<|im_start|>user\n${text}<|im_end|>\n<|im_start|>assistant\n`; | |
| let tokenCount = 0; | |
| const startTime = performance.now(); | |
| try { | |
| await wllama.createCompletion(prompt, { | |
| sampling: { temp: 0.6, top_p: 0.8, max_tokens: 500, stop: ["<|im_start|>", "<|im_end|>", "User:", "You:"] }, | |
| abortSignal: abortController.signal, | |
| onNewToken: (token, piece, currentText) => { | |
| tokenCount++; | |
| const seconds = (performance.now() - startTime) / 1000; | |
| tpsDisplay.innerText = `Speed: ${(tokenCount / seconds).toFixed(1)} tps`; | |
| let cleanText = currentText.replace(/<\|im_start\|>|<\|im_end\|>|user|assistant/gi, '').trimStart(); | |
| displaySpan.innerHTML = formatText(cleanText); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } | |
| }); | |
| playSound('receive'); | |
| } catch (e) { | |
| if (e.name === 'AbortError') displaySpan.innerText += " [Interrupted]"; | |
| } finally { | |
| sendBtn.innerText = "Send"; | |
| sendBtn.style.color = ""; | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| abortController = null; | |
| } | |
| } | |
| function appendMessage(senderClass, senderName, text) { | |
| const id = 'msg-' + Math.random().toString(36).substr(2, 9); | |
| const html = `<div class="message"><span class="${senderClass}">${senderName}:</span> <span id="${id}">${text}</span></div>`; | |
| chatBox.insertAdjacentHTML('beforeend', html); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| return id; | |
| } | |
| sendBtn.addEventListener('click', sendMessage); | |
| userInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); // Prevents a new line from being added | |
| sendMessage(); | |
| } | |
| }); | |
| init(); | |
| </script> | |
| </body> | |
| </html> |