|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | +<meta charset="UTF-8" /> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | +<title>dev_assistant</title> |
| 7 | +<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Syne:wght@400;600;800&display=swap" rel="stylesheet" /> |
| 8 | +<style> |
| 9 | +*{box-sizing:border-box;margin:0;padding:0} |
| 10 | +body{background:#0a0c10;font-family:'JetBrains Mono',monospace;height:100vh;overflow:hidden;color:#e2e8f0} |
| 11 | +body::before{content:'';position:fixed;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,255,136,0.01) 2px,rgba(0,255,136,0.01) 4px);pointer-events:none;z-index:0} |
| 12 | + |
| 13 | +#assistant{position:fixed;width:80px;height:80px;cursor:pointer;z-index:100;bottom:100px;right:80px;user-select:none;filter:drop-shadow(0 0 8px rgba(0,255,136,0.4));transition:filter .3s} |
| 14 | +#assistant:hover{filter:drop-shadow(0 0 20px rgba(0,255,136,0.9))} |
| 15 | +#assistant.idle{animation:idleFloat 3s ease-in-out infinite} |
| 16 | +#assistant.speaking{animation:speakBounce .35s ease infinite alternate} |
| 17 | +#assistant.thinking{animation:thinkSpin .8s linear infinite} |
| 18 | + |
| 19 | +@keyframes idleFloat{0%,100%{transform:translateY(0) rotate(0deg)}33%{transform:translateY(-8px) rotate(-2deg)}66%{transform:translateY(-4px) rotate(2deg)}} |
| 20 | +@keyframes speakBounce{0%{transform:scaleY(1) scaleX(1)}100%{transform:scaleY(1.06) scaleX(.97)}} |
| 21 | +@keyframes thinkSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} |
| 22 | +@keyframes slideUp{from{opacity:0;transform:translateY(12px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}} |
| 23 | +@keyframes blink{0%,90%,100%{transform:scaleY(1)}95%{transform:scaleY(0.1)}} |
| 24 | +@keyframes dotPulse{0%,80%,100%{opacity:.2;transform:scale(.7)}40%{opacity:1;transform:scale(1)}} |
| 25 | + |
| 26 | +#chat-bubble{position:fixed;z-index:101;background:#10141c;border:1px solid #00ff88;width:360px;display:none;flex-direction:column;box-shadow:0 0 40px rgba(0,255,136,0.12),0 8px 32px rgba(0,0,0,.5);animation:slideUp .2s ease} |
| 27 | +#chat-bubble.active{display:flex} |
| 28 | + |
| 29 | +.bubble-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #1e2535;background:#161b26} |
| 30 | +.bubble-name{display:flex;align-items:center;gap:8px} |
| 31 | +.bubble-dot{width:6px;height:6px;background:#00ff88;border-radius:50%;animation:dotPulse 2s infinite} |
| 32 | +.bubble-title{font-size:11px;color:#00ff88;letter-spacing:1.5px;text-transform:uppercase} |
| 33 | +.bubble-close{background:transparent;border:none;color:#64748b;font-size:18px;cursor:pointer;line-height:1;padding:0 2px;transition:color .2s} |
| 34 | +.bubble-close:hover{color:#ff4455} |
| 35 | + |
| 36 | +.bubble-messages{overflow-y:auto;padding:12px 14px;max-height:280px;display:flex;flex-direction:column;gap:8px} |
| 37 | +.bubble-messages::-webkit-scrollbar{width:2px} |
| 38 | +.bubble-messages::-webkit-scrollbar-thumb{background:#1e2535} |
| 39 | + |
| 40 | +.msg{font-size:12px;line-height:1.65;padding:8px 11px;max-width:88%;white-space:pre-wrap;word-break:break-word} |
| 41 | +.msg.assistant{background:#161b26;border:1px solid #1e2535;border-left:2px solid #00ff88;color:#e2e8f0;align-self:flex-start} |
| 42 | +.msg.user{background:rgba(0,255,136,.07);border:1px solid rgba(0,255,136,.2);color:#e2e8f0;align-self:flex-end;text-align:right} |
| 43 | +.msg.error{border-left-color:#ff4455;color:#ff4455} |
| 44 | + |
| 45 | +.typing-wrap{display:flex;gap:4px;align-items:center;padding:10px 11px;background:#161b26;border:1px solid #1e2535;border-left:2px solid #00ff88;align-self:flex-start;width:52px} |
| 46 | +.td{width:5px;height:5px;background:#00ff88;border-radius:50%;animation:dotPulse 1.2s infinite} |
| 47 | +.td:nth-child(2){animation-delay:.2s} |
| 48 | +.td:nth-child(3){animation-delay:.4s} |
| 49 | + |
| 50 | +.quick-prompts{display:flex;gap:5px;flex-wrap:wrap;padding:8px 14px;border-top:1px solid #1e2535;background:#0d1017} |
| 51 | +.qp{font-size:10px;padding:3px 9px;background:transparent;border:1px solid #1e2535;color:#64748b;font-family:'JetBrains Mono',monospace;cursor:pointer;letter-spacing:.5px;transition:all .2s;white-space:nowrap} |
| 52 | +.qp:hover{border-color:#00ff88;color:#00ff88} |
| 53 | + |
| 54 | +.input-row{display:flex;border-top:1px solid #1e2535} |
| 55 | +#chat-input{flex:1;background:#0a0c10;border:none;color:#e2e8f0;font-family:'JetBrains Mono',monospace;font-size:12px;padding:10px 12px;outline:none;min-width:0} |
| 56 | +#chat-input::placeholder{color:#2a3547} |
| 57 | +#send-btn{background:#00ff88;color:#000;border:none;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:700;padding:0 16px;cursor:pointer;letter-spacing:.5px;white-space:nowrap;transition:opacity .2s;flex-shrink:0} |
| 58 | +#send-btn:hover{opacity:.85} |
| 59 | +#send-btn:disabled{opacity:.35;cursor:not-allowed} |
| 60 | + |
| 61 | +#key-prompt{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:200;background:rgba(10,12,16,.95)} |
| 62 | +.kp-box{background:#10141c;border:1px solid #00ff88;padding:40px 48px;width:440px;position:relative} |
| 63 | +.kp-corner{position:absolute;width:12px;height:12px;border-color:#00ff88;border-style:solid} |
| 64 | +.kp-corner.tl{top:-1px;left:-1px;border-width:2px 0 0 2px} |
| 65 | +.kp-corner.tr{top:-1px;right:-1px;border-width:2px 2px 0 0} |
| 66 | +.kp-corner.bl{bottom:-1px;left:-1px;border-width:0 0 2px 2px} |
| 67 | +.kp-corner.br{bottom:-1px;right:-1px;border-width:0 2px 2px 0} |
| 68 | +.kp-title{font-family:'Syne',sans-serif;font-size:26px;font-weight:800;color:#00ff88;margin-bottom:4px} |
| 69 | +.kp-sub{font-size:11px;color:#64748b;margin-bottom:28px;line-height:1.7} |
| 70 | +.kp-label{font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:#64748b;margin-bottom:6px;margin-top:14px;display:block} |
| 71 | +.kp-select,.kp-input{width:100%;background:#161b26;border:1px solid #1e2535;color:#e2e8f0;font-family:'JetBrains Mono',monospace;font-size:12px;padding:9px 13px;outline:none;transition:border-color .2s;margin-bottom:0} |
| 72 | +.kp-select{cursor:pointer;color:#64748b} |
| 73 | +.kp-input:focus,.kp-select:focus{border-color:#00ff88} |
| 74 | +.kp-btn{width:100%;background:#00ff88;color:#000;border:none;font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;letter-spacing:1px;padding:12px;cursor:pointer;text-transform:uppercase;transition:opacity .2s;margin-top:20px} |
| 75 | +.kp-btn:hover{opacity:.85} |
| 76 | + |
| 77 | +#hint{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);font-size:10px;color:#1e2535;letter-spacing:1px;text-transform:uppercase;pointer-events:none} |
| 78 | +</style> |
| 79 | +</head> |
| 80 | +<body> |
| 81 | + |
| 82 | +<div id="key-prompt"> |
| 83 | + <div class="kp-box"> |
| 84 | + <div class="kp-corner tl"></div><div class="kp-corner tr"></div> |
| 85 | + <div class="kp-corner bl"></div><div class="kp-corner br"></div> |
| 86 | + <div class="kp-title">dev_assistant</div> |
| 87 | + <div class="kp-sub">Your roaming AI companion. Bring your own key.</div> |
| 88 | + <label class="kp-label">provider</label> |
| 89 | + <select class="kp-select" id="kp-provider"> |
| 90 | + <option value="openrouter">OpenRouter</option> |
| 91 | + <option value="gemini">Gemini</option> |
| 92 | + </select> |
| 93 | + <label class="kp-label">api key</label> |
| 94 | + <input class="kp-input" id="kp-key" type="password" placeholder="sk-or-v1-... or AIzaSy..." autocomplete="off" spellcheck="false" /> |
| 95 | + <button class="kp-btn" onclick="startAssistant()">[ launch ]</button> |
| 96 | + </div> |
| 97 | +</div> |
| 98 | + |
| 99 | +<div id="assistant" class="idle" onclick="toggleChat()" style="display:none"> |
| 100 | + <svg viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"> |
| 101 | + <defs> |
| 102 | + <filter id="glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter> |
| 103 | + </defs> |
| 104 | + <!-- Body --> |
| 105 | + <rect x="18" y="30" width="44" height="32" rx="7" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/> |
| 106 | + <!-- Head --> |
| 107 | + <rect x="22" y="8" width="36" height="26" rx="6" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/> |
| 108 | + <!-- Antenna --> |
| 109 | + <line x1="40" y1="8" x2="40" y2="1" stroke="#00ff88" stroke-width="1.5"/> |
| 110 | + <circle cx="40" cy="1" r="2.5" fill="#00ff88" filter="url(#glow)"/> |
| 111 | + <!-- Eyes --> |
| 112 | + <ellipse id="eye-l" cx="32" cy="20" rx="4" ry="4" fill="#00ff88" filter="url(#glow)"/> |
| 113 | + <ellipse id="eye-r" cx="48" cy="20" rx="4" ry="4" fill="#00ff88" filter="url(#glow)"/> |
| 114 | + <circle cx="33" cy="20" r="1.8" fill="#0a0c10"/> |
| 115 | + <circle cx="49" cy="20" r="1.8" fill="#0a0c10"/> |
| 116 | + <circle cx="33.8" cy="19.2" r=".7" fill="#fff" opacity=".8"/> |
| 117 | + <circle cx="49.8" cy="19.2" r=".7" fill="#fff" opacity=".8"/> |
| 118 | + <!-- Neck --> |
| 119 | + <rect x="35" y="33" width="10" height="5" rx="2" fill="#1e2535"/> |
| 120 | + <!-- Mouth --> |
| 121 | + <rect id="mouth" x="31" y="27" width="18" height="4" rx="2" fill="#00ff88" opacity=".5"/> |
| 122 | + <!-- Arms --> |
| 123 | + <rect x="5" y="35" width="13" height="6" rx="3" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/> |
| 124 | + <rect x="62" y="35" width="13" height="6" rx="3" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/> |
| 125 | + <!-- Legs --> |
| 126 | + <rect x="25" y="60" width="10" height="14" rx="4" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/> |
| 127 | + <rect x="45" y="60" width="10" height="14" rx="4" fill="#10141c" stroke="#00ff88" stroke-width="1.5"/> |
| 128 | + <!-- Chest panel --> |
| 129 | + <rect x="26" y="38" width="28" height="18" rx="3" fill="#161b26" stroke="#1e2535" stroke-width="1"/> |
| 130 | + <circle cx="33" cy="47" r="2.5" fill="#00ff88" opacity=".6"/> |
| 131 | + <circle cx="40" cy="47" r="2.5" fill="#4a9eff" opacity=".6"/> |
| 132 | + <circle cx="47" cy="47" r="2.5" fill="#f5a623" opacity=".6"/> |
| 133 | + <rect x="30" y="41" width="20" height="2" rx="1" fill="#1e2535"/> |
| 134 | + </svg> |
| 135 | +</div> |
| 136 | + |
| 137 | +<div id="chat-bubble"> |
| 138 | + <div class="bubble-header"> |
| 139 | + <div class="bubble-name"> |
| 140 | + <div class="bubble-dot"></div> |
| 141 | + <span class="bubble-title">dev_assistant</span> |
| 142 | + </div> |
| 143 | + <button class="bubble-close" onclick="toggleChat()">×</button> |
| 144 | + </div> |
| 145 | + <div class="bubble-messages" id="messages"></div> |
| 146 | + <div class="quick-prompts"> |
| 147 | + <button class="qp" onclick="quickPrompt('What should I focus on today?')">today?</button> |
| 148 | + <button class="qp" onclick="quickPrompt('Review my stack')">stack</button> |
| 149 | + <button class="qp" onclick="quickPrompt('Give me a quick coding challenge in Python')">challenge</button> |
| 150 | + <button class="qp" onclick="quickPrompt('Explain CORS simply')">cors?</button> |
| 151 | + <button class="qp" onclick="quickPrompt('Best practices for REST APIs')">rest api</button> |
| 152 | + </div> |
| 153 | + <div class="input-row"> |
| 154 | + <input id="chat-input" placeholder="ask anything..." autocomplete="off" spellcheck="false" /> |
| 155 | + <button id="send-btn" onclick="sendMessage()">send →</button> |
| 156 | + </div> |
| 157 | +</div> |
| 158 | + |
| 159 | +<div id="hint">click the robot to chat</div> |
| 160 | + |
| 161 | +<script> |
| 162 | +let API_KEY = '', PROVIDER = 'openrouter'; |
| 163 | +let chatOpen = false; |
| 164 | +let messages = []; |
| 165 | +let posX, posY; |
| 166 | +let roamTimer = null; |
| 167 | + |
| 168 | +const bot = document.getElementById('assistant'); |
| 169 | +const bubble = document.getElementById('chat-bubble'); |
| 170 | +const msgsEl = document.getElementById('messages'); |
| 171 | +const inp = document.getElementById('chat-input'); |
| 172 | +const sendBtn = document.getElementById('send-btn'); |
| 173 | +const hint = document.getElementById('hint'); |
| 174 | + |
| 175 | +// ── Init ────────────────────────────────────────────────────── |
| 176 | +window.addEventListener('DOMContentLoaded', () => { |
| 177 | + const k = localStorage.getItem('da_key'); |
| 178 | + const p = localStorage.getItem('da_provider'); |
| 179 | + if (k) { API_KEY = k; PROVIDER = p || 'openrouter'; launch(); } |
| 180 | + inp.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); }); |
| 181 | + document.getElementById('kp-key').addEventListener('keydown', e => { if (e.key === 'Enter') startAssistant(); }); |
| 182 | +}); |
| 183 | + |
| 184 | +function startAssistant() { |
| 185 | + const k = document.getElementById('kp-key').value.trim(); |
| 186 | + const p = document.getElementById('kp-provider').value; |
| 187 | + if (!k) { document.getElementById('kp-key').style.borderColor = '#ff4455'; return; } |
| 188 | + API_KEY = k; PROVIDER = p; |
| 189 | + localStorage.setItem('da_key', k); |
| 190 | + localStorage.setItem('da_provider', p); |
| 191 | + document.getElementById('key-prompt').style.display = 'none'; |
| 192 | + launch(); |
| 193 | +} |
| 194 | + |
| 195 | +function launch() { |
| 196 | + document.getElementById('key-prompt').style.display = 'none'; |
| 197 | + bot.style.display = 'block'; |
| 198 | + posX = window.innerWidth - 140; |
| 199 | + posY = window.innerHeight - 180; |
| 200 | + bot.style.left = posX + 'px'; |
| 201 | + bot.style.top = posY + 'px'; |
| 202 | + bot.style.bottom = 'auto'; |
| 203 | + bot.style.right = 'auto'; |
| 204 | + startRoaming(); |
| 205 | + setTimeout(greet, 600); |
| 206 | +} |
| 207 | + |
| 208 | +// ── Roaming ─────────────────────────────────────────────────── |
| 209 | +function startRoaming() { |
| 210 | + roamTimer = setInterval(() => { if (!chatOpen) roam(); }, 5000); |
| 211 | +} |
| 212 | + |
| 213 | +function roam() { |
| 214 | + const pw = window.innerWidth - 100, ph = window.innerHeight - 100; |
| 215 | + const dx = (Math.random() - 0.5) * 200, dy = (Math.random() - 0.5) * 140; |
| 216 | + posX = Math.max(10, Math.min(pw, posX + dx)); |
| 217 | + posY = Math.max(10, Math.min(ph, posY + dy)); |
| 218 | + bot.style.transition = 'left 2.5s cubic-bezier(.25,.46,.45,.94), top 2.5s cubic-bezier(.25,.46,.45,.94)'; |
| 219 | + bot.style.left = posX + 'px'; |
| 220 | + bot.style.top = posY + 'px'; |
| 221 | + syncBubble(); |
| 222 | +} |
| 223 | + |
| 224 | +function syncBubble() { |
| 225 | + const bw = 360, bh = 400; |
| 226 | + const vw = window.innerWidth, vh = window.innerHeight; |
| 227 | + let bx = posX + 90, by = posY - 60; |
| 228 | + if (bx + bw > vw - 8) bx = posX - bw - 8; |
| 229 | + if (by + bh > vh - 8) by = posY - bh + 80; |
| 230 | + if (by < 8) by = posY + 90; |
| 231 | + bubble.style.left = bx + 'px'; |
| 232 | + bubble.style.top = by + 'px'; |
| 233 | + bubble.style.bottom = 'auto'; |
| 234 | + bubble.style.right = 'auto'; |
| 235 | +} |
| 236 | + |
| 237 | +// ── Toggle chat ─────────────────────────────────────────────── |
| 238 | +function toggleChat() { |
| 239 | + chatOpen = !chatOpen; |
| 240 | + if (chatOpen) { |
| 241 | + syncBubble(); |
| 242 | + bubble.classList.add('active'); |
| 243 | + setAnim('speaking'); |
| 244 | + hint.style.display = 'none'; |
| 245 | + inp.focus(); |
| 246 | + } else { |
| 247 | + bubble.classList.remove('active'); |
| 248 | + setAnim('idle'); |
| 249 | + hint.style.display = 'block'; |
| 250 | + } |
| 251 | +} |
| 252 | + |
| 253 | +function setAnim(state) { |
| 254 | + bot.className = state; |
| 255 | +} |
| 256 | + |
| 257 | +// ── Messages ────────────────────────────────────────────────── |
| 258 | +function addMsg(role, text, extra = '') { |
| 259 | + messages.push({ role, content: text }); |
| 260 | + const el = document.createElement('div'); |
| 261 | + el.className = `msg ${role}${extra ? ' ' + extra : ''}`; |
| 262 | + el.textContent = text; |
| 263 | + msgsEl.appendChild(el); |
| 264 | + msgsEl.scrollTop = msgsEl.scrollHeight; |
| 265 | + return el; |
| 266 | +} |
| 267 | + |
| 268 | +function showTyping() { |
| 269 | + const el = document.createElement('div'); |
| 270 | + el.className = 'typing-wrap'; el.id = 'typing'; |
| 271 | + el.innerHTML = '<div class="td"></div><div class="td"></div><div class="td"></div>'; |
| 272 | + msgsEl.appendChild(el); |
| 273 | + msgsEl.scrollTop = msgsEl.scrollHeight; |
| 274 | +} |
| 275 | + |
| 276 | +function hideTyping() { |
| 277 | + const el = document.getElementById('typing'); |
| 278 | + if (el) el.remove(); |
| 279 | +} |
| 280 | + |
| 281 | +function greet() { |
| 282 | + const g = [ |
| 283 | + "Hey dev! I'm your assistant. Ask me anything — code, debugging, architecture, or just vibe.", |
| 284 | + "// online\n\nReady to help. What are we building today?", |
| 285 | + "Hello! I'm here whenever you need me. Ask away.", |
| 286 | + ]; |
| 287 | + addMsg('assistant', g[Math.floor(Math.random() * g.length)]); |
| 288 | +} |
| 289 | + |
| 290 | +function quickPrompt(t) { inp.value = t; sendMessage(); } |
| 291 | + |
| 292 | +// ── Send ────────────────────────────────────────────────────── |
| 293 | +async function sendMessage() { |
| 294 | + const text = inp.value.trim(); |
| 295 | + if (!text || sendBtn.disabled) return; |
| 296 | + inp.value = ''; |
| 297 | + addMsg('user', text); |
| 298 | + sendBtn.disabled = true; |
| 299 | + setAnim('thinking'); |
| 300 | + showTyping(); |
| 301 | + |
| 302 | + const sys = `You are dev_assistant — a concise, sharp AI coding assistant with a hacker aesthetic. |
| 303 | +Expertise: cybersecurity, IoT, Python, C/C++, Go, JavaScript/TypeScript, Linux, EdTech, DevOps. |
| 304 | +Be direct and practical. Use code blocks when relevant. Skip filler phrases. |
| 305 | +Keep replies under 200 words unless a detailed answer is genuinely needed.`; |
| 306 | + |
| 307 | + const history = messages.slice(-12).map(m => ({ |
| 308 | + role: m.role === 'assistant' ? 'assistant' : 'user', |
| 309 | + content: m.content |
| 310 | + })); |
| 311 | + |
| 312 | + try { |
| 313 | + let reply = ''; |
| 314 | + |
| 315 | + if (PROVIDER === 'openrouter') { |
| 316 | + const res = await fetch('https://openrouter.ai/api/v1/chat/completions', { |
| 317 | + method: 'POST', |
| 318 | + headers: { |
| 319 | + 'Authorization': `Bearer ${API_KEY}`, |
| 320 | + 'Content-Type': 'application/json', |
| 321 | + 'HTTP-Referer': window.location.href, |
| 322 | + 'X-Title': 'dev_assistant', |
| 323 | + }, |
| 324 | + body: JSON.stringify({ |
| 325 | + model: 'meta-llama/llama-3.3-70b-instruct', |
| 326 | + messages: [{ role: 'system', content: sys }, ...history], |
| 327 | + max_tokens: 600, |
| 328 | + temperature: 0.72, |
| 329 | + }), |
| 330 | + }); |
| 331 | + const d = await res.json(); |
| 332 | + if (d.error) throw new Error(d.error.message); |
| 333 | + reply = d.choices?.[0]?.message?.content || '...'; |
| 334 | + |
| 335 | + } else { |
| 336 | + const contents = history.map(m => ({ |
| 337 | + role: m.role === 'assistant' ? 'model' : 'user', |
| 338 | + parts: [{ text: m.content }] |
| 339 | + })); |
| 340 | + const res = await fetch( |
| 341 | + `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${API_KEY}`, |
| 342 | + { |
| 343 | + method: 'POST', |
| 344 | + headers: { 'Content-Type': 'application/json' }, |
| 345 | + body: JSON.stringify({ |
| 346 | + system_instruction: { parts: [{ text: sys }] }, |
| 347 | + contents, |
| 348 | + generationConfig: { maxOutputTokens: 600, temperature: 0.72 }, |
| 349 | + }), |
| 350 | + } |
| 351 | + ); |
| 352 | + const d = await res.json(); |
| 353 | + if (d.error) throw new Error(d.error.message); |
| 354 | + reply = d.candidates?.[0]?.content?.parts?.[0]?.text || '...'; |
| 355 | + } |
| 356 | + |
| 357 | + hideTyping(); |
| 358 | + addMsg('assistant', reply); |
| 359 | + setAnim('speaking'); |
| 360 | + setTimeout(() => { if (chatOpen) setAnim('idle'); }, 1800); |
| 361 | + |
| 362 | + } catch (e) { |
| 363 | + hideTyping(); |
| 364 | + addMsg('assistant', `// error: ${e.message}`, 'error'); |
| 365 | + setAnim('idle'); |
| 366 | + } |
| 367 | + |
| 368 | + sendBtn.disabled = false; |
| 369 | + inp.focus(); |
| 370 | +} |
| 371 | + |
| 372 | +// ── Eye blink ───────────────────────────────────────────────── |
| 373 | +setInterval(() => { |
| 374 | + if (Math.random() > 0.65) { |
| 375 | + ['eye-l','eye-r'].forEach(id => { |
| 376 | + const el = document.getElementById(id); |
| 377 | + if (!el) return; |
| 378 | + el.setAttribute('ry', '0.4'); |
| 379 | + setTimeout(() => el.setAttribute('ry', '4'), 110); |
| 380 | + }); |
| 381 | + } |
| 382 | +}, 2800); |
| 383 | +</script> |
| 384 | +</body> |
| 385 | +</html> |
0 commit comments