Add animated talking face: blinks idly, looks around while thinking, mouth moves while speaking
Browse files- index.html +41 -4
index.html
CHANGED
|
@@ -21,6 +21,19 @@
|
|
| 21 |
border-bottom:1px solid var(--border); }
|
| 22 |
header .logo { width:34px; height:34px; border-radius:10px; display:flex; align-items:center; justify-content:center;
|
| 23 |
font-size:20px; background:linear-gradient(135deg,var(--accent),var(--accent2)); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
header h1 { font-size:18px; margin:0; }
|
| 25 |
header .tag { font-size:11px; color:var(--muted); border:1px solid var(--border); border-radius:20px; padding:3px 10px; }
|
| 26 |
header .spacer { flex:1; }
|
|
@@ -99,7 +112,17 @@
|
|
| 99 |
</head>
|
| 100 |
<body>
|
| 101 |
<header>
|
| 102 |
-
<div class="logo">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
<h1>ArtieAI</h1>
|
| 104 |
<span class="tag">free · no sign-up</span>
|
| 105 |
<span class="tag" id="usageTag">…</span>
|
|
@@ -291,7 +314,7 @@ async function runTurn(userText, opts){
|
|
| 291 |
const via = opts.via || ask;
|
| 292 |
const doneRe = opts.doneMarker ? new RegExp(opts.doneMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) : null;
|
| 293 |
addMsg("user", md(userText));
|
| 294 |
-
sendBtn.disabled = true; typing.style.display = "block";
|
| 295 |
try{
|
| 296 |
let contextMsg = userText;
|
| 297 |
if(FACTUAL.test(userText)){
|
|
@@ -353,7 +376,7 @@ async function runTurn(userText, opts){
|
|
| 353 |
else if(m.includes("upstream")) addMsg("system", "⏳ The free AI lane is busy right now — try again in a bit. (This didn't use any of today's chats.)");
|
| 354 |
else addMsg("system", "⚠️ The free AI services are both having trouble right now. Try again in a bit — it's not your connection. (" + esc(m.slice(0,160)) + ")");
|
| 355 |
}
|
| 356 |
-
typing.style.display = "none"; sendBtn.disabled = false;
|
| 357 |
updateUsage();
|
| 358 |
chat.scrollTop = chat.scrollHeight;
|
| 359 |
}
|
|
@@ -453,14 +476,16 @@ function speak(text){
|
|
| 453 |
if(!clean.trim()) return;
|
| 454 |
const u = new SpeechSynthesisUtterance(clean);
|
| 455 |
u.rate = 1; u.pitch = 1;
|
|
|
|
| 456 |
synth.speak(u);
|
|
|
|
| 457 |
}catch(e){}
|
| 458 |
}
|
| 459 |
voiceBtn.onclick = () => {
|
| 460 |
if(!synth) return;
|
| 461 |
voiceOut = !voiceOut;
|
| 462 |
save("agentx_voice_out", voiceOut);
|
| 463 |
-
if(!voiceOut) synth.cancel();
|
| 464 |
paintVoiceBtn();
|
| 465 |
addMsg("system", voiceOut ? "<b>Voice ON</b> — I'll read replies aloud." : "<b>Voice OFF</b>.");
|
| 466 |
};
|
|
@@ -488,6 +513,18 @@ if(!SR){
|
|
| 488 |
};
|
| 489 |
}
|
| 490 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
/* ---------- modal helpers ---------- */
|
| 492 |
const back = $("modalBack"), modal = $("modal");
|
| 493 |
function openModal(html){ modal.innerHTML = '<span class="close-x" id="mx">×</span>' + html; back.classList.add("open");
|
|
|
|
| 21 |
border-bottom:1px solid var(--border); }
|
| 22 |
header .logo { width:34px; height:34px; border-radius:10px; display:flex; align-items:center; justify-content:center;
|
| 23 |
font-size:20px; background:linear-gradient(135deg,var(--accent),var(--accent2)); }
|
| 24 |
+
/* Talking face: blinks idly, looks around while thinking, mouth moves while speaking. Pure CSS/SVG. */
|
| 25 |
+
#face svg { width:100%; height:100%; display:block; }
|
| 26 |
+
#face .eye { fill:#fff; }
|
| 27 |
+
#face .pupil { fill:#1e1b4b; }
|
| 28 |
+
#face .mouth { fill:#fff; transform-box:fill-box; transform-origin:center; }
|
| 29 |
+
#face .eyes-blink { transform-box:fill-box; transform-origin:center; animation:faceBlink 4.6s infinite; }
|
| 30 |
+
@keyframes faceBlink { 0%,92%,100% { transform:scaleY(1); } 95.5% { transform:scaleY(.08); } }
|
| 31 |
+
#face.thinking .eyes-look { animation:faceLook 1.2s ease-in-out infinite; }
|
| 32 |
+
@keyframes faceLook { 0%,100% { transform:translateX(0); } 30% { transform:translateX(-1.8px); } 70% { transform:translateX(1.8px); } }
|
| 33 |
+
#face.thinking svg { animation:faceTilt 1.2s ease-in-out infinite; }
|
| 34 |
+
@keyframes faceTilt { 0%,100% { transform:rotate(-5deg); } 50% { transform:rotate(5deg); } }
|
| 35 |
+
#face.talking .mouth { animation:faceTalk .34s ease-in-out infinite; }
|
| 36 |
+
@keyframes faceTalk { 0%,100% { transform:scaleY(.3); } 50% { transform:scaleY(1.5); } }
|
| 37 |
header h1 { font-size:18px; margin:0; }
|
| 38 |
header .tag { font-size:11px; color:var(--muted); border:1px solid var(--border); border-radius:20px; padding:3px 10px; }
|
| 39 |
header .spacer { flex:1; }
|
|
|
|
| 112 |
</head>
|
| 113 |
<body>
|
| 114 |
<header>
|
| 115 |
+
<div class="logo" id="face" title="Artie">
|
| 116 |
+
<svg viewBox="0 0 40 40" aria-hidden="true">
|
| 117 |
+
<g class="eyes-blink"><g class="eyes-look">
|
| 118 |
+
<ellipse class="eye" cx="13.5" cy="15" rx="3.4" ry="4.2"/>
|
| 119 |
+
<ellipse class="eye" cx="26.5" cy="15" rx="3.4" ry="4.2"/>
|
| 120 |
+
<circle class="pupil" cx="13.5" cy="16" r="1.5"/>
|
| 121 |
+
<circle class="pupil" cx="26.5" cy="16" r="1.5"/>
|
| 122 |
+
</g></g>
|
| 123 |
+
<rect class="mouth" x="13" y="24.5" width="14" height="3.4" rx="1.7"/>
|
| 124 |
+
</svg>
|
| 125 |
+
</div>
|
| 126 |
<h1>ArtieAI</h1>
|
| 127 |
<span class="tag">free · no sign-up</span>
|
| 128 |
<span class="tag" id="usageTag">…</span>
|
|
|
|
| 314 |
const via = opts.via || ask;
|
| 315 |
const doneRe = opts.doneMarker ? new RegExp(opts.doneMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) : null;
|
| 316 |
addMsg("user", md(userText));
|
| 317 |
+
sendBtn.disabled = true; typing.style.display = "block"; faceThinking(true);
|
| 318 |
try{
|
| 319 |
let contextMsg = userText;
|
| 320 |
if(FACTUAL.test(userText)){
|
|
|
|
| 376 |
else if(m.includes("upstream")) addMsg("system", "⏳ The free AI lane is busy right now — try again in a bit. (This didn't use any of today's chats.)");
|
| 377 |
else addMsg("system", "⚠️ The free AI services are both having trouble right now. Try again in a bit — it's not your connection. (" + esc(m.slice(0,160)) + ")");
|
| 378 |
}
|
| 379 |
+
typing.style.display = "none"; sendBtn.disabled = false; faceThinking(false);
|
| 380 |
updateUsage();
|
| 381 |
chat.scrollTop = chat.scrollHeight;
|
| 382 |
}
|
|
|
|
| 476 |
if(!clean.trim()) return;
|
| 477 |
const u = new SpeechSynthesisUtterance(clean);
|
| 478 |
u.rate = 1; u.pitch = 1;
|
| 479 |
+
u.onend = u.onerror = () => faceTalking(false);
|
| 480 |
synth.speak(u);
|
| 481 |
+
faceTalking(true);
|
| 482 |
}catch(e){}
|
| 483 |
}
|
| 484 |
voiceBtn.onclick = () => {
|
| 485 |
if(!synth) return;
|
| 486 |
voiceOut = !voiceOut;
|
| 487 |
save("agentx_voice_out", voiceOut);
|
| 488 |
+
if(!voiceOut){ synth.cancel(); faceTalking(false); }
|
| 489 |
paintVoiceBtn();
|
| 490 |
addMsg("system", voiceOut ? "<b>Voice ON</b> — I'll read replies aloud." : "<b>Voice OFF</b>.");
|
| 491 |
};
|
|
|
|
| 513 |
};
|
| 514 |
}
|
| 515 |
|
| 516 |
+
/* ---------- Talking face state ---------- */
|
| 517 |
+
const face = $("face");
|
| 518 |
+
let talkWatch = null;
|
| 519 |
+
function faceThinking(on){ face.classList.toggle("thinking", !!on); }
|
| 520 |
+
function faceTalking(on){
|
| 521 |
+
face.classList.toggle("talking", !!on);
|
| 522 |
+
if(talkWatch){ clearInterval(talkWatch); talkWatch = null; }
|
| 523 |
+
if(on && synth){
|
| 524 |
+
talkWatch = setInterval(() => { if(!synth.speaking) faceTalking(false); }, 400);
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
/* ---------- modal helpers ---------- */
|
| 529 |
const back = $("modalBack"), modal = $("modal");
|
| 530 |
function openModal(html){ modal.innerHTML = '<span class="close-x" id="mx">×</span>' + html; back.classList.add("open");
|