mirror of
https://github.com/Jeuners/astra-vision.git
synced 2026-09-09 15:02:35 +02:00
feat: show the ComfyUI job itself in the transcript, not just a status line
Ein Statustext über dem Orb war zu unauffällig. Ein Tool-Aufruf legt
jetzt eine eigene Karte im Gesprächsverlauf an, mit Spinner und dem
tatsächlichen Prompt, den das LLM an ComfyUI schickt ("Anfrage an
ComfyUI: ..."). Sobald das Bild fertig ist, wird dieselbe Karte
in-place durch das Ergebnis ersetzt statt eine zweite anzuhängen;
schlägt ComfyUI fehl, zeigt sie stattdessen die Fehlermeldung.
Der Prompt-Text landet über sichere DOM-Erstellung (createTextNode),
nicht per innerHTML, weil er vom LLM erzeugt und damit indirekt
nutzerbeeinflusst ist.
Per Browser-Test mit echter synthetisierter Sprache verifiziert: Karte
mit Spinner + Prompt erscheint sofort, wird sauber (kein Leerlauf-id im
DOM) durch das Bild ersetzt, gefolgt von Astras Textantwort.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA
This commit is contained in:
parent
6854e80703
commit
76a5d4e499
4 changed files with 53 additions and 7 deletions
|
|
@ -18,10 +18,12 @@ def build_tools(config: Settings, notify: Callable[[dict], None], media_store: d
|
||||||
async def handle_generate_image(params):
|
async def handle_generate_image(params):
|
||||||
prompt = params.arguments.get("prompt", "")
|
prompt = params.arguments.get("prompt", "")
|
||||||
notify({"type": "activity", "text": "Astra erzeugt ein Bild …"})
|
notify({"type": "activity", "text": "Astra erzeugt ein Bild …"})
|
||||||
|
notify({"type": "tool_start", "text": f'Anfrage an ComfyUI: "{prompt}"'})
|
||||||
try:
|
try:
|
||||||
image = await generate_image(config.comfyui_url, prompt)
|
image = await generate_image(config.comfyui_url, prompt)
|
||||||
except ComfyUIError as exc:
|
except ComfyUIError as exc:
|
||||||
notify({"type": "activity", "text": "Bilderzeugung fehlgeschlagen."})
|
notify({"type": "activity", "text": "Bilderzeugung fehlgeschlagen."})
|
||||||
|
notify({"type": "tool_error", "text": f"ComfyUI-Anfrage fehlgeschlagen: {exc}"})
|
||||||
await params.result_callback({"error": str(exc)})
|
await params.result_callback({"error": str(exc)})
|
||||||
return
|
return
|
||||||
image_id = uuid.uuid4().hex
|
image_id = uuid.uuid4().hex
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,9 @@ async def test_generate_image_tool_stores_media_and_notifies(monkeypatch):
|
||||||
assert len(media_store) == 1
|
assert len(media_store) == 1
|
||||||
image_id, image_bytes = next(iter(media_store.items()))
|
image_id, image_bytes = next(iter(media_store.items()))
|
||||||
assert image_bytes == b"png-bytes"
|
assert image_bytes == b"png-bytes"
|
||||||
assert notifications[0]["type"] == "activity"
|
assert [n["type"] for n in notifications] == ["activity", "tool_start", "image"]
|
||||||
assert notifications[1] == {"type": "image", "url": f"/api/media/{image_id}"}
|
assert "a cat" in notifications[1]["text"]
|
||||||
|
assert notifications[2] == {"type": "image", "url": f"/api/media/{image_id}"}
|
||||||
assert params.results[0]["status"] == "ok"
|
assert params.results[0]["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -60,4 +61,9 @@ async def test_generate_image_tool_reports_comfyui_errors_without_storing_media(
|
||||||
|
|
||||||
assert media_store == {}
|
assert media_store == {}
|
||||||
assert "error" in params.results[0]
|
assert "error" in params.results[0]
|
||||||
assert [n["type"] for n in notifications] == ["activity", "activity"]
|
assert [n["type"] for n in notifications] == [
|
||||||
|
"activity",
|
||||||
|
"tool_start",
|
||||||
|
"activity",
|
||||||
|
"tool_error",
|
||||||
|
]
|
||||||
|
|
|
||||||
44
web/app.js
44
web/app.js
|
|
@ -33,10 +33,46 @@ function addMessage(event) {
|
||||||
while ($("messages").children.length > 80) $("messages").firstElementChild.remove();
|
while ($("messages").children.length > 80) $("messages").firstElementChild.remove();
|
||||||
$("messages").scrollTop = $("messages").scrollHeight;
|
$("messages").scrollTop = $("messages").scrollHeight;
|
||||||
}
|
}
|
||||||
function addImage(url) {
|
function addToolStart(text) {
|
||||||
$("messages").querySelector(".empty")?.remove();
|
$("messages").querySelector(".empty")?.remove();
|
||||||
|
document.getElementById("pending-tool")?.remove();
|
||||||
const article = document.createElement("article");
|
const article = document.createElement("article");
|
||||||
article.className = "message assistant";
|
article.className = "message assistant pending-tool";
|
||||||
|
article.id = "pending-tool";
|
||||||
|
const speaker = document.createElement("span");
|
||||||
|
speaker.className = "speaker";
|
||||||
|
speaker.textContent = "Astra";
|
||||||
|
const body = document.createElement("p");
|
||||||
|
body.className = "pending-text";
|
||||||
|
const spinner = document.createElement("i");
|
||||||
|
spinner.className = "spinner";
|
||||||
|
body.append(spinner, document.createTextNode(` ${text}`));
|
||||||
|
article.append(speaker, body);
|
||||||
|
$("messages").append(article);
|
||||||
|
$("messages").scrollTop = $("messages").scrollHeight;
|
||||||
|
}
|
||||||
|
function addToolError(text) {
|
||||||
|
const pending = document.getElementById("pending-tool");
|
||||||
|
if (pending) {
|
||||||
|
pending.removeAttribute("id");
|
||||||
|
pending.classList.remove("pending-tool");
|
||||||
|
pending.querySelector(".pending-text").textContent = text;
|
||||||
|
$("messages").scrollTop = $("messages").scrollHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showError(text);
|
||||||
|
}
|
||||||
|
function addImage(url) {
|
||||||
|
const pending = document.getElementById("pending-tool");
|
||||||
|
const article = pending || document.createElement("article");
|
||||||
|
if (pending) {
|
||||||
|
pending.removeAttribute("id");
|
||||||
|
pending.className = "message assistant";
|
||||||
|
pending.replaceChildren();
|
||||||
|
} else {
|
||||||
|
$("messages").querySelector(".empty")?.remove();
|
||||||
|
article.className = "message assistant";
|
||||||
|
}
|
||||||
const speaker = document.createElement("span");
|
const speaker = document.createElement("span");
|
||||||
speaker.className = "speaker";
|
speaker.className = "speaker";
|
||||||
speaker.textContent = "Astra";
|
speaker.textContent = "Astra";
|
||||||
|
|
@ -45,7 +81,7 @@ function addImage(url) {
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.alt = "Von Astra erzeugtes Bild";
|
img.alt = "Von Astra erzeugtes Bild";
|
||||||
article.append(speaker, img);
|
article.append(speaker, img);
|
||||||
$("messages").append(article);
|
if (!pending) $("messages").append(article);
|
||||||
$("messages").scrollTop = $("messages").scrollHeight;
|
$("messages").scrollTop = $("messages").scrollHeight;
|
||||||
}
|
}
|
||||||
function addUploadNote(filename) {
|
function addUploadNote(filename) {
|
||||||
|
|
@ -89,6 +125,8 @@ function receive(event) {
|
||||||
try { message = JSON.parse(event.data); } catch { return; }
|
try { message = JSON.parse(event.data); } catch { return; }
|
||||||
if (message.type === "state") state(message.state);
|
if (message.type === "state") state(message.state);
|
||||||
if (message.type === "activity" && !muted) $("status").textContent = message.text;
|
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 === "partial") $("partial").textContent = message.text;
|
if (message.type === "partial") $("partial").textContent = message.text;
|
||||||
if (message.type === "transcript") addMessage(message);
|
if (message.type === "transcript") addMessage(message);
|
||||||
if (message.type === "image") addImage(message.url);
|
if (message.type === "image") addImage(message.url);
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue