diff --git a/desktop/public/mesh-buddy.html b/desktop/public/mesh-buddy.html index 890cab47f8..853c18ce8d 100644 --- a/desktop/public/mesh-buddy.html +++ b/desktop/public/mesh-buddy.html @@ -10,7 +10,6 @@ --background: 240 6% 7%; --foreground: 0 0% 95%; --muted-foreground: 240 4% 60%; - --status-added: 145 45% 55%; } html, body { @@ -26,9 +25,12 @@ cursor: default; } .wrap { text-align: center; } - .face { font-size: 96px; line-height: 1; } + .face { width: 160px; height: 110px; margin: 0 auto; } + .face svg { width: 100%; height: 100%; } + .eye { fill: hsl(var(--foreground)); } + .lid { fill: hsl(var(--background)); } .count { - margin-top: 18px; + margin-top: 10px; font-size: 40px; font-weight: 700; font-variant-numeric: tabular-nums; @@ -43,13 +45,24 @@ .nodes { margin-top: 14px; font-size: 13px; - color: hsl(var(--status-added)); + color: hsl(var(--muted-foreground)); }
-
🙂
+
+ + + + + + + + + +
0
tokens helped compute
 
@@ -74,22 +87,152 @@ window.addEventListener("storage", applyTheme); window.addEventListener("focus", applyTheme); + // ---- Procedural eyes (Cozmo/Vector style) -------------------------- + // Each eye is a rounded rectangle with a bendable bottom edge. Instead + // of canned frames, the rig holds one parameter set and eases it toward + // the current expression every frame, so any state change (and blinks) + // becomes a smooth morph. Inspired by procedural expression polygons: + // https://github.com/ggldnl/Procedural-Expression-Library + // + // Params: w/h size, r corner radius, curve (+ bows the bottom edge up: + // a happy squint), y vertical offset, gaze x drift. + const EXPRESSIONS = { + sleeping: { w: 44, h: 5, r: 2.5, curve: 0, y: 16 }, + waking: { w: 40, h: 22, r: 9, curve: 0, y: 6 }, + neutral: { w: 38, h: 44, r: 12, curve: 0, y: 0 }, + happy: { w: 42, h: 38, r: 13, curve: 16, y: -2 }, + tired: { w: 40, h: 14, r: 6, curve: 0, y: 10 }, + }; + const CENTERS = [46, 114]; // eye x-centers in the 160x110 viewBox + const MID_Y = 48; + + const rig = { + current: { ...EXPRESSIONS.sleeping, gaze: 0 }, + target: { ...EXPRESSIONS.sleeping, gaze: 0 }, + mood: "sleeping", + blink: 1, // height multiplier; dips toward 0 during a blink + blinkPhase: "open", + nextBlinkAt: performance.now() + 2500, + }; + + function eyePath(cx, p, blink) { + const h = Math.max(3, p.h * blink); + const r = Math.min(p.r, h / 2, p.w / 2); + const top = MID_Y + p.y - h / 2; + const bottom = top + h; + const left = cx + p.gaze - p.w / 2; + const right = left + p.w; + return [ + `M ${left + r} ${top}`, + `H ${right - r}`, + `Q ${right} ${top} ${right} ${top + r}`, + `V ${bottom - r}`, + `Q ${right} ${bottom} ${right - r} ${bottom}`, + `H ${left + r}`, + `Q ${left} ${bottom} ${left} ${bottom - r}`, + `V ${top + r}`, + `Q ${left} ${top} ${left + r} ${top}`, + "Z", + ].join(" "); + } + + const eyeL = document.getElementById("eyeL"); + const eyeR = document.getElementById("eyeR"); + const lidL = document.getElementById("lidL"); + const lidR = document.getElementById("lidR"); + + // Lower-lid mask: an ellipse whose top edge rises `curve` px into the + // eye from its bottom, turning the eye into a smooth happy crescent. + function placeLid(lid, cx, p, blink) { + const h = Math.max(3, p.h * blink); + const bottom = MID_Y + p.y + h / 2; + const rise = Math.max(0, p.curve) * blink; + lid.setAttribute("cx", cx + p.gaze); + lid.setAttribute("cy", bottom - rise + 26); + } + + function stepBlink(now) { + if (rig.mood === "sleeping" || rig.mood === "tired") { + rig.blink = 1; + rig.blinkPhase = "open"; + return; + } + if (rig.blinkPhase === "open" && now >= rig.nextBlinkAt) { + rig.blinkPhase = "closing"; + } + if (rig.blinkPhase === "closing") { + rig.blink = Math.max(0.08, rig.blink - 0.28); + if (rig.blink <= 0.08) rig.blinkPhase = "opening"; + } else if (rig.blinkPhase === "opening") { + rig.blink = Math.min(1, rig.blink + 0.22); + if (rig.blink >= 1) { + rig.blinkPhase = "open"; + rig.nextBlinkAt = now + 2200 + Math.random() * 4200; + } + } + } + + let lastGazeShift = 0; + function stepGaze(now) { + if (rig.mood !== "neutral") { + rig.target.gaze = 0; + return; + } + if (now - lastGazeShift > 2800 + Math.random() * 1500) { + lastGazeShift = now; + rig.target.gaze = (Math.random() - 0.5) * 14; + } + } + + function frame(now) { + stepBlink(now); + stepGaze(now); + const c = rig.current; + const t = rig.target; + for (const key of ["w", "h", "r", "curve", "y", "gaze"]) { + c[key] += (t[key] - c[key]) * 0.12; + } + // Sleeping breathes: a slow, gentle bob. + const breath = + rig.mood === "sleeping" ? Math.sin(now / 900) * 1.6 : 0; + const p = { ...c, y: c.y + breath }; + eyeL.setAttribute("d", eyePath(CENTERS[0], p, rig.blink)); + eyeR.setAttribute("d", eyePath(CENTERS[1], p, rig.blink)); + placeLid(lidL, CENTERS[0], p, rig.blink); + placeLid(lidR, CENTERS[1], p, rig.blink); + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); + + function setMood(mood) { + if (rig.mood === mood) return; + rig.mood = mood; + rig.target = { ...EXPRESSIONS[mood], gaze: 0 }; + } + + // ---- State from the mesh runtime ---------------------------------- const invoke = window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke; async function tick() { if (!invoke) return; try { - const usage = await invoke("mesh_serving_usage"); + const [usage, status] = await Promise.all([ + invoke("mesh_serving_usage"), + invoke("mesh_node_status"), + ]); const tokens = usage?.tokensServed || 0; const inflight = usage?.inflight || 0; document.getElementById("count").textContent = tokens.toLocaleString(); - document.getElementById("face").textContent = - inflight > 0 ? "😄" : tokens > 0 ? "😊" : "🙂"; const nodes = (usage?.peers || 0) + 1; document.getElementById("nodes").textContent = nodes + (nodes === 1 ? " active node in mesh" : " active nodes in mesh"); + const state = status?.state; + if (state === "running") setMood(inflight > 0 ? "happy" : "neutral"); + else if (state === "starting") setMood("waking"); + else if (state === "failed") setMood("tired"); + else setMood("sleeping"); } catch { - // Mesh not running — keep whatever is shown. + setMood("sleeping"); } } tick();