Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>help</title><link rel="icon" href="https://aesthetic.computer/icon/128x128/prompt.png" type="image/png" /><link rel="stylesheet" href="https://aesthetic.computer/type/webfonts/berkeley-mono-variable.css" /><style>:root { --bg: #111; --bg2: #1a1a1a; --fg: #ccc; --fg2: #888; --pink: #ff6b9d; --green: #6bcb77; --cyan: #4ecdc4; --border: #333;}[data-theme="light"] { --bg: #f4f4f4; --bg2: #fff; --fg: #222; --fg2: #777; --pink: rgb(205, 92, 155); --green: #059669; --cyan: #0891b2; --border: #ccc;}* { margin: 0; padding: 0; box-sizing: border-box; }::-webkit-scrollbar { width: 4px; }::-webkit-scrollbar-track { background: transparent; }::-webkit-scrollbar-thumb { background: var(--border); }html, body { height: 100%; }body { font-family: 'Berkeley Mono Variable', monospace; font-size: 14px; line-height: 1.6; background: var(--bg); color: var(--fg); cursor: url('https://aesthetic.computer/aesthetic.computer/cursors/precise.svg') 12 12, auto; -webkit-text-size-adjust: none;}
/* ---- LANDING ---- */#landing { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 2em; gap: 1.5em; text-align: center;}.logo { font-size: 24px; font-weight: normal; letter-spacing: 3px; }.logo span { color: var(--pink); }.subtitle { font-size: 12px; color: var(--fg2); line-height: 1.7; max-width: 380px; }.subtitle a { color: var(--cyan); text-decoration: none; }.subtitle a:hover { text-decoration: underline; }.divider { width: 40px; height: 1px; background: var(--border); }.steps { text-align: left; max-width: 380px; width: 100%; display: flex; flex-direction: column; gap: 0.8em; }.step { font-size: 12px; color: var(--fg2); padding-left: 1.5em; position: relative; }.step::before { content: attr(data-n); position: absolute; left: 0; color: var(--pink); font-size: 11px; }.btn { font-family: inherit; font-size: 13px; letter-spacing: 1px; padding: 8px 24px; background: var(--bg2); color: var(--fg); border: 1px solid var(--border); cursor: pointer; transition: border-color 0.2s, color 0.2s;}.btn:hover { border-color: var(--pink); color: var(--pink); }#authStatus { font-size: 11px; color: var(--fg2); }
/* ---- CHAT APP ---- */#app { display: none; flex-direction: column; height: 100%;}#app.visible { display: flex; }
/* header */#header { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0;}#header .logo { font-size: 16px; letter-spacing: 2px; }.header-right { display: flex; align-items: center; gap: 12px; }#usageBar { font-size: 11px; color: var(--fg2); }#handleBadge { font-size: 11px; color: var(--pink); }.sign-out-btn { font-family: inherit; font-size: 11px; background: none; border: none; color: var(--fg2); cursor: pointer; padding: 0;}.sign-out-btn:hover { color: var(--fg); }
/* messages */#messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px;}.msg { display: flex; flex-direction: column; gap: 4px; max-width: 680px; }.msg.user { align-self: flex-end; align-items: flex-end; }.msg.assistant { align-self: flex-start; align-items: flex-start; }.msg-role { font-size: 10px; color: var(--fg2); letter-spacing: 1px; text-transform: uppercase; }.msg.user .msg-role { color: var(--pink); }.msg-body { font-size: 13px; line-height: 1.65; color: var(--fg); background: var(--bg2); border: 1px solid var(--border); padding: 10px 14px; white-space: pre-wrap; word-break: break-word;}
/* typing indicator */#typing { display: none; align-self: flex-start; }#typing.visible { display: flex; }#typing .msg-body { color: var(--fg2); font-style: italic; }
/* input area */#inputArea { border-top: 1px solid var(--border); padding: 12px 16px; display: flex; gap: 8px; align-items: flex-end; flex-shrink: 0;}#msgInput { font-family: inherit; font-size: 13px; flex: 1; padding: 8px 12px; resize: none; background: var(--bg2); color: var(--fg); border: 1px solid var(--border); min-height: 38px; max-height: 160px; overflow-y: auto; line-height: 1.5;}#msgInput:focus { outline: none; border-color: var(--cyan); }#msgInput::placeholder { color: var(--fg2); }#sendBtn { font-family: inherit; font-size: 12px; letter-spacing: 1px; padding: 8px 16px; height: 38px; background: var(--bg2); color: var(--cyan); border: 1px solid var(--cyan); cursor: pointer; flex-shrink: 0;}#sendBtn:hover { background: var(--cyan); color: var(--bg); }#sendBtn:disabled { opacity: 0.4; cursor: default; background: var(--bg2); color: var(--fg2); border-color: var(--border); }
/* empty state */#emptyState { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: var(--fg2); font-size: 12px; text-align: center; padding: 2em;}#emptyState .hint { font-size: 11px; color: var(--border); }
@media (max-width: 500px) { #header { padding: 8px 12px; } #messages { padding: 12px; } #inputArea { padding: 8px 12px; }}</style></head><body>
<!-- Landing --><div id="landing"> <div class="logo"><span>help</span>.aesthetic.computer</div> <div class="divider"></div> <div class="subtitle"> AI assistant for <a href="https://aesthetic.computer" target="_blank">Aesthetic Computer</a>. Ask anything about pieces, KidLisp, or creative computing. </div> <div class="steps"> <div class="step" data-n="1.">Sign in with your <a href="https://aesthetic.computer/handle" target="_blank" style="color:var(--cyan)">@handle</a></div> <div class="step" data-n="2.">Ask anything about AC</div> </div> <button class="btn" id="signInBtn">sign in</button> <div id="authStatus"></div></div>
<!-- Chat App --><div id="app"> <div id="header"> <div class="logo"><span>help</span>.aesthetic.computer</div> <div class="header-right"> <span id="usageBar"></span> <span id="handleBadge"></span> <button class="sign-out-btn" id="signOutBtn">sign out</button> </div> </div>
<div id="messages"> <div id="emptyState"> <div>ask me anything about aesthetic.computer</div> <div class="hint">pieces · kidlisp · creative computing</div> </div> </div>
<div id="typing" class="msg assistant"> <div class="msg-body">...</div> </div>
<div id="inputArea"> <textarea id="msgInput" rows="1" placeholder="ask something..."></textarea> <button id="sendBtn">send</button> </div></div>
<script>let auth0Client = null;let accessToken = null;const chatHistory = [];
async function authFetch(url, opts = {}) { const headers = { ...opts.headers }; if (accessToken) headers.Authorization = 'Bearer ' + accessToken; const resp = await fetch(url, { ...opts, headers }); if (resp.status === 401 && auth0Client) { try { accessToken = await auth0Client.getTokenSilently({ cacheMode: 'off' }); headers.Authorization = 'Bearer ' + accessToken; return fetch(url, { ...opts, headers }); } catch { auth0Client.loginWithRedirect(); } } return resp;}
async function init() { document.getElementById('authStatus').textContent = 'loading...'; try { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://cdn.auth0.com/js/auth0-spa-js/2.1/auth0-spa-js.production.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); });
const config = await fetch('/auth/config').then(r => r.json()); auth0Client = await window.auth0.createAuth0Client({ domain: config.domain, clientId: config.clientId, cacheLocation: 'localstorage', useRefreshTokens: true, authorizationParams: { redirect_uri: location.origin + location.pathname }, });
if (location.search.includes('state=') && location.search.includes('code=')) { await auth0Client.handleRedirectCallback(); window.history.replaceState({}, '', location.pathname); }
if (await auth0Client.isAuthenticated()) { accessToken = await auth0Client.getTokenSilently(); const me = await authFetch('/auth/me').then(r => r.json()); showApp(me); } else { document.getElementById('authStatus').textContent = ''; document.getElementById('signInBtn').onclick = () => auth0Client.loginWithRedirect(); } } catch (err) { document.getElementById('authStatus').textContent = 'error: ' + err.message; }}
async function refreshUsage() { try { const { used, limit } = await authFetch('/api/usage').then(r => r.json()); document.getElementById('usageBar').textContent = `${used}/${limit} today`; } catch {}}
function showApp(me) { document.getElementById('landing').style.display = 'none'; document.getElementById('app').classList.add('visible'); document.getElementById('handleBadge').textContent = '@' + (me.handle || '?'); document.getElementById('signOutBtn').onclick = () => auth0Client.logout({ logoutParams: { returnTo: location.origin } }); refreshUsage(); setupChat();}
function addMessage(role, content) { const empty = document.getElementById('emptyState'); if (empty) empty.remove(); const msgs = document.getElementById('messages'); const div = document.createElement('div'); div.className = `msg ${role}`; div.innerHTML = `<div class="msg-role">${role === 'user' ? 'you' : 'help'}</div><div class="msg-body"></div>`; div.querySelector('.msg-body').textContent = content; msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div.querySelector('.msg-body');}
function setupChat() { const input = document.getElementById('msgInput'); const sendBtn = document.getElementById('sendBtn'); const typing = document.getElementById('typing'); const msgs = document.getElementById('messages');
input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 160) + 'px'; }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }); sendBtn.onclick = send;
async function send() { const text = input.value.trim(); if (!text || sendBtn.disabled) return;
input.value = ''; input.style.height = 'auto'; sendBtn.disabled = true;
addMessage('user', text); chatHistory.push({ role: 'user', content: text });
typing.classList.add('visible'); msgs.appendChild(typing); msgs.scrollTop = msgs.scrollHeight;
let fullText = ''; let assistantBody = null;
try { const resp = await authFetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: chatHistory }), });
typing.classList.remove('visible');
if (!resp.ok) { const err = await resp.json(); addMessage('assistant', err.error || 'something went wrong'); sendBtn.disabled = false; return; }
const used = resp.headers.get('X-Rate-Limit-Used'); const limit = resp.headers.get('X-Rate-Limit-Total'); if (used && limit) document.getElementById('usageBar').textContent = `${used}/${limit} today`;
assistantBody = addMessage('assistant', '');
const reader = resp.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; fullText += decoder.decode(value, { stream: true }); assistantBody.textContent = fullText; msgs.scrollTop = msgs.scrollHeight; }
chatHistory.push({ role: 'assistant', content: fullText }); } catch (err) { typing.classList.remove('visible'); addMessage('assistant', 'error: ' + err.message); }
sendBtn.disabled = false; input.focus(); }}
// Themeif (window.matchMedia('(prefers-color-scheme: light)').matches) document.documentElement.setAttribute('data-theme', 'light');window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => { document.documentElement.setAttribute('data-theme', e.matches ? 'light' : '');});
init();</script></body></html>