SmartChild / index.html
macwhisperer's picture
Update index.html
5cfc8ea verified
<!DOCTYPE html>
<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) !important;
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 !important;
min-height: 100px;
}
/* Fullscreen Mode */
.window.fullscreen {
position: fixed;
top: 0; left: 0;
width: 100% !important;
height: 100% !important;
max-width: none !important;
z-index: 300;
margin: 0;
}
.window-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; /* Critical for inner scrolling */
padding: 8px !important;
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 !important; /* Removes any old fixed heights */
max-height: none !important; /* Prevents it from fighting the flexbox */
scroll-behavior: smooth;
background: white;
border: 2px inset #d5d5d5;
overflow-y: auto;
padding: 12px;
padding-bottom: 2px !important;
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 !important; /* 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 !important;
}
/* Fullscreen tweaks to keep things proportional */
.window.fullscreen.input-expanded .chat-box {
flex: 1 !important;
}
.window.fullscreen.input-expanded #user-input {
max-height: 50vh;
}
/* UI Spacing */
#expand-toggle { margin-left: 100px !important; 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 !important; color: black !important;
width: 22px !important; height: 22px !important;
display: flex; align-items: center; justify-content: center;
border: 1px solid #808080 !important;
box-shadow: inset 1px 1px #fff, inset -1px -1px #808080 !important;
}
#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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[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>