From 6854e80703a71747238152be0b2fae1d68c9a06e Mon Sep 17 00:00:00 2001 From: Jeuner <62662523+Jeuners@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:35:26 +0200 Subject: [PATCH] feat: show what Astra is doing while it works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Orb pulsierte bisher nur bei "listening"/"speaking", nicht während "responding" — während des LLM-Aufrufs sah die UI eingefroren aus, obwohl im Hintergrund gearbeitet wurde. Jetzt pulsiert er (und der Status-Punkt leuchtet) auch während "responding". Neuer "activity"-Nachrichtentyp überschreibt zusätzlich den Statustext mit konkreten Zwischenschritten, ohne den Animationszustand zu brechen — astra/tools.py meldet jetzt "Astra erzeugt ein Bild …" während ComfyUI läuft, statt dass der Nutzer nur ein generisches "Einen Moment …" sieht, das über die volle Dauer (LLM-Entscheidung, ComfyUI-Generierung, LLM-Folgeantwort) unverändert bleibt. Per Browser-Test mit echter synthetisierter Sprache verifiziert: die Statuszeile durchläuft jetzt sichtbar "Einen Moment …" → "Astra erzeugt ein Bild …" → "Einen Moment …" → "Astra spricht." statt derselben Zeile über die ganze Anfrage hinweg. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA --- astra/tools.py | 2 ++ tests/test_tools.py | 7 +++++-- web/app.js | 1 + web/style.css | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/astra/tools.py b/astra/tools.py index 13d02b7..534060e 100644 --- a/astra/tools.py +++ b/astra/tools.py @@ -17,9 +17,11 @@ def build_tools(config: Settings, notify: Callable[[dict], None], media_store: d async def handle_generate_image(params): prompt = params.arguments.get("prompt", "") + notify({"type": "activity", "text": "Astra erzeugt ein Bild …"}) try: image = await generate_image(config.comfyui_url, prompt) except ComfyUIError as exc: + notify({"type": "activity", "text": "Bilderzeugung fehlgeschlagen."}) await params.result_callback({"error": str(exc)}) return image_id = uuid.uuid4().hex diff --git a/tests/test_tools.py b/tests/test_tools.py index f44ad8e..0a23446 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -37,7 +37,8 @@ async def test_generate_image_tool_stores_media_and_notifies(monkeypatch): assert len(media_store) == 1 image_id, image_bytes = next(iter(media_store.items())) assert image_bytes == b"png-bytes" - assert notifications == [{"type": "image", "url": f"/api/media/{image_id}"}] + assert notifications[0]["type"] == "activity" + assert notifications[1] == {"type": "image", "url": f"/api/media/{image_id}"} assert params.results[0]["status"] == "ok" @@ -48,9 +49,10 @@ async def test_generate_image_tool_reports_comfyui_errors_without_storing_media( monkeypatch.setattr(tools_module, "generate_image", failing_generate_image) + notifications = [] media_store: dict[str, bytes] = {} schema = tools_module.build_tools( - Settings(comfyui_url="http://fake-comfyui"), lambda _: None, media_store + Settings(comfyui_url="http://fake-comfyui"), notifications.append, media_store ).standard_tools[0] params = FakeFunctionCallParams({"prompt": "a cat"}) @@ -58,3 +60,4 @@ async def test_generate_image_tool_reports_comfyui_errors_without_storing_media( assert media_store == {} assert "error" in params.results[0] + assert [n["type"] for n in notifications] == ["activity", "activity"] diff --git a/web/app.js b/web/app.js index 6b955e4..1bdeb03 100644 --- a/web/app.js +++ b/web/app.js @@ -88,6 +88,7 @@ function receive(event) { let message; try { message = JSON.parse(event.data); } catch { return; } if (message.type === "state") state(message.state); + if (message.type === "activity" && !muted) $("status").textContent = message.text; if (message.type === "partial") $("partial").textContent = message.text; if (message.type === "transcript") addMessage(message); if (message.type === "image") addImage(message.url); diff --git a/web/style.css b/web/style.css index e874c99..329cea7 100644 --- a/web/style.css +++ b/web/style.css @@ -1,3 +1,3 @@ @font-face{font-family:system;src:local("Avenir Next")} :root{color-scheme:dark;--bg:#111820;--panel:#19222b;--text:#eef2ef;--muted:#a3b1b8;--accent:#b4e5cc;--border:#303c44} -*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% 25%,#1c2d34 0%,var(--bg) 58%);color:var(--text);font-family:system,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh}main{max-width:1060px;margin:auto;padding:36px 48px 25px}header{display:flex;justify-content:space-between;align-items:center}.brand{text-decoration:none;color:var(--text);font-size:30px;letter-spacing:-1.5px;font-weight:600}.star{color:var(--accent);font-size:35px;vertical-align:middle;margin-right:7px}.local{display:flex;align-items:center;gap:9px;font-size:12px;color:#c3d3ce;border:1px solid var(--border);padding:9px 13px;border-radius:30px}.local i{height:6px;width:6px;background:var(--accent);border-radius:50%}.conversation{text-align:center;padding:60px 0 34px}.eyebrow{font-size:10px;font-weight:600;letter-spacing:2.8px;color:var(--accent);margin-bottom:17px}h1{font-size:clamp(32px,5vw,49px);font-weight:500;letter-spacing:-1.8px;margin:0 0 13px}.description{font-size:14px;color:var(--muted);margin:0}.orb-wrap{position:relative;width:210px;height:210px;display:grid;place-items:center;margin:31px auto 15px}.orb{width:145px;height:145px;border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle at 30% 25%,#a3e1cf77,#4e938a22 55%,#172b3544);border:1px solid #a3d3c35c;box-shadow:inset 0 0 38px #92e1ce19,0 0 65px #84c5b215}.orbit{position:absolute;inset:6px;border:1px solid #9acdb01b;border-radius:50%;box-shadow:0 0 0 16px #98cdb104}.wave{height:40px;display:flex;align-items:center;gap:5px}.wave i{display:block;width:4px;height:13px;border-radius:5px;background:#d0f3e4;transition:height .3s}.wave i:nth-child(2),.wave i:nth-child(6){height:22px}.wave i:nth-child(3),.wave i:nth-child(5){height:32px}.wave i:nth-child(4){height:41px}body[data-state="speaking"] .wave i,body[data-state="listening"] .wave i{animation:voice 1.2s ease-in-out infinite alternate}.wave i:nth-child(2n){animation-delay:-.8s!important}.wave i:nth-child(3n){animation-delay:-.4s!important}@keyframes voice{to{height:8px;opacity:.6}}.live-status{display:flex;justify-content:center;align-items:center;gap:8px;height:27px}.live-status p{font-size:14px;margin:0}#state-dot{height:6px;width:6px;border-radius:50%;background:var(--muted)}body[data-state="listening"] #state-dot,body[data-state="speaking"] #state-dot{background:var(--accent)}.partial{color:var(--muted);font-size:13px;min-height:22px;max-width:640px;margin:13px auto 17px;overflow-wrap:anywhere}.voice-picker{display:flex;justify-content:center;align-items:center;gap:10px;margin:0 0 18px}.voice-picker label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}.voice-picker select{font:inherit;font-size:13px;background:var(--panel);color:var(--text);border:1px solid #576b68;border-radius:9px;padding:8px 12px;cursor:pointer}.voice-picker select:disabled{opacity:.5;cursor:wait}.voice-picker select:focus-visible{outline:3px solid #d5f3e5;outline-offset:3px}.controls{display:flex;justify-content:center;gap:12px;flex-wrap:wrap}button{font:inherit;cursor:pointer;border:0;transition:background .2s,transform .2s}button:active{transform:translateY(1px)}button:focus-visible,a:focus-visible{outline:3px solid #d5f3e5;outline-offset:5px}.primary{background:var(--accent);color:#162c25;border-radius:9px;font-size:14px;font-weight:600;padding:15px 24px;min-width:211px}.primary:hover{background:#cef2df}.primary span{margin-right:9px}.primary:disabled{background:#43594f;color:#bfcbc4;cursor:wait}.secondary{border:1px solid #576b68;border-radius:9px;padding:14px 18px;background:transparent;color:var(--text);font-size:13px}.secondary[aria-pressed="true"]{background:#553a38;border-color:#c2887c}.hint{font-size:11px;color:var(--muted);margin:16px 0 0}.error{color:#ffd5cd;background:#4a2c2a;border:1px solid #8e564f;border-radius:9px;padding:12px;max-width:640px;margin:15px auto;font-size:13px;overflow-wrap:anywhere}.transcript{border:1px solid var(--border);border-radius:14px;background:#19222b9c;padding:22px 26px}.section-top{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border);padding-bottom:16px}h2{margin:0;font-size:13px;font-weight:500}.text-button{color:var(--muted);background:none;font-size:11px;padding:7px}.text-button:hover{color:var(--text)}#messages{max-height:330px;overflow:auto;scroll-behavior:smooth;padding-top:5px}.empty{font-size:12px;color:var(--muted);line-height:1.9;text-align:center;padding:18px 0 12px}.message{padding:15px 0 7px}.message .speaker{font-size:10px;letter-spacing:1px;font-weight:600;text-transform:uppercase;color:var(--accent)}.message.user .speaker{color:#b1bdcb}.message p{font-size:14px;line-height:1.65;margin:6px 0;white-space:pre-wrap;overflow-wrap:anywhere}.message small{color:var(--muted);font-size:11px}.generated-image{display:block;max-width:100%;border-radius:9px;margin:8px 0;border:1px solid var(--border)}footer{display:flex;justify-content:space-between;gap:15px;margin-top:21px;color:var(--muted);font-size:10px}footer b{padding:0 6px;font-weight:400}.credits{display:flex;align-items:center}footer a{color:var(--muted);text-decoration:none}footer a:hover{color:var(--accent)}[hidden]{display:none!important}@media(max-width:620px){main{padding:23px 20px}.conversation{padding-top:44px}.description{max-width:270px;margin:auto;line-height:1.6}.local{font-size:10px;padding:8px 10px}.transcript{padding:18px}footer{flex-direction:column;gap:8px}.orb-wrap{margin-top:21px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}} +*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% 25%,#1c2d34 0%,var(--bg) 58%);color:var(--text);font-family:system,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh}main{max-width:1060px;margin:auto;padding:36px 48px 25px}header{display:flex;justify-content:space-between;align-items:center}.brand{text-decoration:none;color:var(--text);font-size:30px;letter-spacing:-1.5px;font-weight:600}.star{color:var(--accent);font-size:35px;vertical-align:middle;margin-right:7px}.local{display:flex;align-items:center;gap:9px;font-size:12px;color:#c3d3ce;border:1px solid var(--border);padding:9px 13px;border-radius:30px}.local i{height:6px;width:6px;background:var(--accent);border-radius:50%}.conversation{text-align:center;padding:60px 0 34px}.eyebrow{font-size:10px;font-weight:600;letter-spacing:2.8px;color:var(--accent);margin-bottom:17px}h1{font-size:clamp(32px,5vw,49px);font-weight:500;letter-spacing:-1.8px;margin:0 0 13px}.description{font-size:14px;color:var(--muted);margin:0}.orb-wrap{position:relative;width:210px;height:210px;display:grid;place-items:center;margin:31px auto 15px}.orb{width:145px;height:145px;border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle at 30% 25%,#a3e1cf77,#4e938a22 55%,#172b3544);border:1px solid #a3d3c35c;box-shadow:inset 0 0 38px #92e1ce19,0 0 65px #84c5b215}.orbit{position:absolute;inset:6px;border:1px solid #9acdb01b;border-radius:50%;box-shadow:0 0 0 16px #98cdb104}.wave{height:40px;display:flex;align-items:center;gap:5px}.wave i{display:block;width:4px;height:13px;border-radius:5px;background:#d0f3e4;transition:height .3s}.wave i:nth-child(2),.wave i:nth-child(6){height:22px}.wave i:nth-child(3),.wave i:nth-child(5){height:32px}.wave i:nth-child(4){height:41px}body[data-state="speaking"] .wave i,body[data-state="listening"] .wave i,body[data-state="responding"] .wave i{animation:voice 1.2s ease-in-out infinite alternate}.wave i:nth-child(2n){animation-delay:-.8s!important}.wave i:nth-child(3n){animation-delay:-.4s!important}@keyframes voice{to{height:8px;opacity:.6}}.live-status{display:flex;justify-content:center;align-items:center;gap:8px;height:27px}.live-status p{font-size:14px;margin:0}#state-dot{height:6px;width:6px;border-radius:50%;background:var(--muted)}body[data-state="listening"] #state-dot,body[data-state="speaking"] #state-dot,body[data-state="responding"] #state-dot{background:var(--accent)}.partial{color:var(--muted);font-size:13px;min-height:22px;max-width:640px;margin:13px auto 17px;overflow-wrap:anywhere}.voice-picker{display:flex;justify-content:center;align-items:center;gap:10px;margin:0 0 18px}.voice-picker label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}.voice-picker select{font:inherit;font-size:13px;background:var(--panel);color:var(--text);border:1px solid #576b68;border-radius:9px;padding:8px 12px;cursor:pointer}.voice-picker select:disabled{opacity:.5;cursor:wait}.voice-picker select:focus-visible{outline:3px solid #d5f3e5;outline-offset:3px}.controls{display:flex;justify-content:center;gap:12px;flex-wrap:wrap}button{font:inherit;cursor:pointer;border:0;transition:background .2s,transform .2s}button:active{transform:translateY(1px)}button:focus-visible,a:focus-visible{outline:3px solid #d5f3e5;outline-offset:5px}.primary{background:var(--accent);color:#162c25;border-radius:9px;font-size:14px;font-weight:600;padding:15px 24px;min-width:211px}.primary:hover{background:#cef2df}.primary span{margin-right:9px}.primary:disabled{background:#43594f;color:#bfcbc4;cursor:wait}.secondary{border:1px solid #576b68;border-radius:9px;padding:14px 18px;background:transparent;color:var(--text);font-size:13px}.secondary[aria-pressed="true"]{background:#553a38;border-color:#c2887c}.hint{font-size:11px;color:var(--muted);margin:16px 0 0}.error{color:#ffd5cd;background:#4a2c2a;border:1px solid #8e564f;border-radius:9px;padding:12px;max-width:640px;margin:15px auto;font-size:13px;overflow-wrap:anywhere}.transcript{border:1px solid var(--border);border-radius:14px;background:#19222b9c;padding:22px 26px}.section-top{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border);padding-bottom:16px}h2{margin:0;font-size:13px;font-weight:500}.text-button{color:var(--muted);background:none;font-size:11px;padding:7px}.text-button:hover{color:var(--text)}#messages{max-height:330px;overflow:auto;scroll-behavior:smooth;padding-top:5px}.empty{font-size:12px;color:var(--muted);line-height:1.9;text-align:center;padding:18px 0 12px}.message{padding:15px 0 7px}.message .speaker{font-size:10px;letter-spacing:1px;font-weight:600;text-transform:uppercase;color:var(--accent)}.message.user .speaker{color:#b1bdcb}.message p{font-size:14px;line-height:1.65;margin:6px 0;white-space:pre-wrap;overflow-wrap:anywhere}.message small{color:var(--muted);font-size:11px}.generated-image{display:block;max-width:100%;border-radius:9px;margin:8px 0;border:1px solid var(--border)}footer{display:flex;justify-content:space-between;gap:15px;margin-top:21px;color:var(--muted);font-size:10px}footer b{padding:0 6px;font-weight:400}.credits{display:flex;align-items:center}footer a{color:var(--muted);text-decoration:none}footer a:hover{color:var(--accent)}[hidden]{display:none!important}@media(max-width:620px){main{padding:23px 20px}.conversation{padding-top:44px}.description{max-width:270px;margin:auto;line-height:1.6}.local{font-size:10px;padding:8px 10px}.transcript{padding:18px}footer{flex-direction:column;gap:8px}.orb-wrap{margin-top:21px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}