From b0282900d2265ccb6bdf13eac5ba5c9859d3cec4 Mon Sep 17 00:00:00 2001 From: Jeuner <62662523+Jeuners@users.noreply.github.com> Date: Mon, 7 Sep 2026 18:36:09 +0200 Subject: [PATCH] feat: show RSS headlines as clickable links in the transcript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read_news bisher zeigte nur Klartext-Titel in der Karte an — man konnte im Chat selbst nicht auf den Artikel klicken (nur das Modell kannte den Link, für ein mögliches read_article danach). Jetzt trägt der tool_result-Event zusätzlich strukturierte 'items' (source, title, link), das Frontend rendert daraus eine Liste mit echten -Links statt reinem Text. read_article bleibt unverändert bei reinem Text (items bleibt leer, kein Link zum Verlinken vorhanden). Verifiziert: die Rendering-Logik direkt per simuliertem Datenkanal- Event getestet (unabhängig von der Modell-Zuverlässigkeit, die bei read_news aktuell bei ~15-20% liegt) — zwei Links korrekt mit href, target=_blank, rel=noopener und Quelle gerendert. NICHT gepusht — weiterhin nur lokal, wie angewiesen. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA --- astra/tools.py | 11 +++++++++-- tests/test_tools.py | 4 ++++ web/app.js | 38 ++++++++++++++++++++++++++++++++------ web/style.css | 2 +- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/astra/tools.py b/astra/tools.py index f0e7963..bf36a2d 100644 --- a/astra/tools.py +++ b/astra/tools.py @@ -47,8 +47,15 @@ def build_tools(config: Settings, notify: Callable[[dict], None], media_store: d notify({"type": "tool_error", "text": str(exc)}) await params.result_callback({"error": str(exc)}) return - headlines = "\n".join(f"- ({entry.source}) {entry.title}" for entry in entries) - notify({"type": "tool_result", "text": headlines}) + notify( + { + "type": "tool_result", + "text": "\n".join(f"- ({entry.source}) {entry.title}" for entry in entries), + "items": [ + {"source": e.source, "title": e.title, "link": e.link} for e in entries + ], + } + ) await params.result_callback( { "status": "ok", diff --git a/tests/test_tools.py b/tests/test_tools.py index 348862c..f36b54d 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -98,6 +98,10 @@ async def test_read_news_tool_reports_headlines(monkeypatch): assert [n["type"] for n in notifications] == ["activity", "tool_start", "tool_result"] assert "KI-Durchbruch" in notifications[2]["text"] assert "Neuer Chip" in notifications[2]["text"] + assert notifications[2]["items"] == [ + {"source": "heise online", "title": "KI-Durchbruch", "link": "https://x"}, + {"source": "Golem.de", "title": "Neuer Chip", "link": "https://y"}, + ] assert params.results[0]["status"] == "ok" assert len(params.results[0]["headlines"]) == 2 diff --git a/web/app.js b/web/app.js index 2e8b4d8..bf56d22 100644 --- a/web/app.js +++ b/web/app.js @@ -62,7 +62,7 @@ function addToolError(text) { } showError(text); } -function addToolResult(text) { +function addToolResult(text, items) { const pending = document.getElementById("pending-tool"); const article = pending || document.createElement("article"); if (pending) { @@ -76,10 +76,36 @@ function addToolResult(text) { const speaker = document.createElement("span"); speaker.className = "speaker"; speaker.textContent = "Astra"; - const body = document.createElement("p"); - body.className = "tool-result"; - body.textContent = text; - article.append(speaker, body); + article.append(speaker); + if (items?.length) { + const list = document.createElement("ul"); + list.className = "tool-result-list"; + for (const item of items) { + const li = document.createElement("li"); + if (item.link) { + const link = document.createElement("a"); + link.href = item.link; + link.target = "_blank"; + link.rel = "noopener"; + link.textContent = item.title; + li.append(link); + } else { + li.textContent = item.title; + } + if (item.source) { + const source = document.createElement("small"); + source.textContent = ` (${item.source})`; + li.append(source); + } + list.append(li); + } + article.append(list); + } else { + const body = document.createElement("p"); + body.className = "tool-result"; + body.textContent = text; + article.append(body); + } if (!pending) $("messages").append(article); $("messages").scrollTop = $("messages").scrollHeight; } @@ -148,7 +174,7 @@ function receive(event) { if (message.type === "activity" && !muted) $("status").textContent = message.text; if (message.type === "tool_start") addToolStart(message.text); if (message.type === "tool_error") addToolError(message.text); - if (message.type === "tool_result") addToolResult(message.text); + if (message.type === "tool_result") addToolResult(message.text, message.items); 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 62aad67..4470a6f 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,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)}.pending-tool .pending-text{color:var(--muted);display:flex;align-items:center;gap:8px}.spinner{display:inline-block;width:11px;height:11px;border-radius:50%;border:2px solid var(--border);border-top-color:var(--accent);animation:spin .7s linear infinite;flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}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)}.pending-tool .pending-text{color:var(--muted);display:flex;align-items:center;gap:8px}.tool-result-list{list-style:none;margin:6px 0;padding:0;display:flex;flex-direction:column;gap:8px}.tool-result-list li{font-size:14px;line-height:1.5}.tool-result-list a{color:var(--accent);text-decoration:none}.tool-result-list a:hover{text-decoration:underline}.tool-result-list small{color:var(--muted);font-size:11px}.spinner{display:inline-block;width:11px;height:11px;border-radius:50%;border:2px solid var(--border);border-top-color:var(--accent);animation:spin .7s linear infinite;flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}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}}