- #578: Abgebrochene Laeufe (Schrittgrenze, Zeitueberschreitung, Abbruch, Ausnahme) tragen ihre bezahlten Token - vorher 0 in RunUsage, fuer das Budget unsichtbar. - #580: WebFetch prueft die aufgeloeste Zieladresse beim Verbinden; ein erlaubter Name, der auf 127.0.0.1 aufloest, wird gesperrt. - #575: Dateien entstehen unter Unix mit 0600 (AtomicFile, Protokolle, FileRW-Anhaengen, state.db), Protokollverzeichnisse mit 0700. - #576: SIGTERM/SIGINT beenden auch mit Fenster geordnet. - #593: OpenRouter-Statusdienst wird gestartet. - #589: Auftragsnachricht nennt aktuelle Zeit, Zeitzone und Termin. - #581: Unvollstaendiges Argument-JSON erreicht das Werkzeug nicht mehr; das Modell bekommt eine verstaendliche Rueckmeldung statt Parsertext. - #591: Weboberflaeche liest die Felder, die API und Ereignisstrom liefern (Freigaben, Protokoll, Taskboard mit Termin, Werkzeug-Einstellungen, Tokennutzung); Ereignisstrom in camelCase. - #592: Chat zeigt Nachrichten nicht mehr doppelt. - #536 (teilweise): A2A-Antworten tragen Absender und Empfaenger, Parser wirft nicht mehr; Datums- und Volltextfilter in Agenten-Chats. - deploy/publish.py: Packager-Pfad net10.0, Schalter --agent-file. - Version 0.1.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
427 lines
12 KiB
JavaScript
427 lines
12 KiB
JavaScript
// ClawdDotNet — Chat-Ansicht
|
|
//
|
|
// Leitfaden: Sicherer Chatverlauf (strikt textContent), Echtzeit-Ereignisse über SSE.
|
|
|
|
import { api, holeAktiveInstanz } from "../modules/api.js";
|
|
import { abonnieren } from "../modules/events.js";
|
|
import {
|
|
erstelleElement,
|
|
erstelleIcon,
|
|
PFADE,
|
|
formatiereZeit,
|
|
erstellePlakette,
|
|
zeigeMeldung
|
|
} from "../modules/dom.js";
|
|
|
|
let aktiverAgentId = null;
|
|
let agentenListe = [];
|
|
let nachrichtenContainer = null;
|
|
let schrittBox = null;
|
|
let eingabeTextarea = null;
|
|
let sendenButton = null;
|
|
let abbrechenButton = null;
|
|
let agentenSelector = null;
|
|
let statusPlakette = null;
|
|
let entladeEreignisse = [];
|
|
|
|
// #592: Zählt die Aufbauten dieser Ansicht. Ein Aufbau, der während seines await von
|
|
// einem neueren überholt wurde, meldet keine Ereignisse mehr an — sonst hingen zwei
|
|
// "chat"-Listener und jede Nachricht erschien doppelt.
|
|
let aufbauZaehler = 0;
|
|
|
|
function raeumeEreignisseAuf() {
|
|
entladeEreignisse.forEach((cleanup) => cleanup());
|
|
entladeEreignisse = [];
|
|
}
|
|
|
|
export async function rendereChat(parameter = {}) {
|
|
const container = document.getElementById("ansicht-inhalt");
|
|
if (!container) return;
|
|
container.replaceChildren();
|
|
|
|
const dieserAufbau = ++aufbauZaehler;
|
|
|
|
// Alte Ereignisse aufräumen
|
|
raeumeEreignisseAuf();
|
|
|
|
const chatShell = erstelleElement("div", { klasse: "chat-ansicht" });
|
|
|
|
// 1. Kopfleiste der Chat-Ansicht
|
|
const kopfleiste = erstelleElement("div", { klasse: "chat-kopf" });
|
|
|
|
const agentenWahl = erstelleElement("div", { klasse: "chat-agenten-wahl" });
|
|
agentenSelector = erstelleElement("select", {
|
|
klasse: "eingabe-select",
|
|
onchange: (e) => waehleAgent(e.target.value)
|
|
});
|
|
|
|
statusPlakette = erstellePlakette("Lädt…", "neutral");
|
|
agentenWahl.append(erstelleElement("label", { text: "Agent: " }), agentenSelector, statusPlakette);
|
|
|
|
const aktionen = erstelleElement("div", { klasse: "chat-aktionen" });
|
|
|
|
const weckenBtn = erstelleElement(
|
|
"button",
|
|
{
|
|
klasse: "schaltflaeche flach",
|
|
title: "Agenten manuell ausführen / wecken",
|
|
onclick: weckeAktivenAgent
|
|
},
|
|
erstelleIcon(PFADE.abspielen, 14),
|
|
erstelleElement("span", { text: " Wecken" })
|
|
);
|
|
|
|
const leerenBtn = erstelleElement(
|
|
"button",
|
|
{
|
|
klasse: "schaltflaeche flach",
|
|
title: "Chatverlauf dieses Agenten leeren",
|
|
onclick: leereChatverlauf
|
|
},
|
|
erstelleIcon(PFADE.leeren, 14),
|
|
erstelleElement("span", { text: " Verlauf leeren" })
|
|
);
|
|
|
|
abbrechenButton = erstelleElement(
|
|
"button",
|
|
{
|
|
klasse: "schaltflaeche gefahr flach",
|
|
title: "Laufenden Lauf abbrechen",
|
|
hidden: true,
|
|
onclick: brecheChatAb
|
|
},
|
|
erstelleIcon(PFADE.abbrechen, 14),
|
|
erstelleElement("span", { text: " Abbrechen" })
|
|
);
|
|
|
|
aktionen.append(weckenBtn, leerenBtn, abbrechenButton);
|
|
kopfleiste.append(agentenWahl, aktionen);
|
|
|
|
// 2. Nachrichtenverlauf & Schrittanzeige
|
|
nachrichtenContainer = erstelleElement("div", { klasse: "chat-verlauf" });
|
|
|
|
schrittBox = erstelleElement("div", { klasse: "chat-schritt-box", hidden: true });
|
|
const schrittIcon = erstelleIcon(PFADE.aktualisieren, 14, "rotierend");
|
|
const schrittText = erstelleElement("span", { klasse: "schritt-text", text: "" });
|
|
schrittBox.append(schrittIcon, schrittText);
|
|
|
|
// 3. Eingabebereich
|
|
const eingabeBereich = erstelleElement("div", { klasse: "chat-eingabe-bereich" });
|
|
|
|
eingabeTextarea = erstelleElement("textarea", {
|
|
klasse: "chat-eingabe",
|
|
placeholder: "Nachricht an den Agenten eingeben (Enter zum Senden, Umschalt+Enter für neue Zeile)…",
|
|
rows: "3",
|
|
onkeydown: (e) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendeNachricht();
|
|
}
|
|
}
|
|
});
|
|
|
|
sendenButton = erstelleElement(
|
|
"button",
|
|
{
|
|
klasse: "schaltflaeche primaer",
|
|
title: "Nachricht senden",
|
|
onclick: sendeNachricht
|
|
},
|
|
erstelleIcon(PFADE.senden, 16),
|
|
erstelleElement("span", { text: "Senden" })
|
|
);
|
|
|
|
eingabeBereich.append(eingabeTextarea, sendenButton);
|
|
|
|
chatShell.append(kopfleiste, nachrichtenContainer, schrittBox, eingabeBereich);
|
|
container.append(chatShell);
|
|
|
|
// Lade Agenten und initialisiere
|
|
await ladeAgenten(parameter.agentId);
|
|
|
|
// Während des Ladens neu aufgebaut? Dann gehört die Ansicht dem neueren Aufbau.
|
|
if (dieserAufbau !== aufbauZaehler) return;
|
|
|
|
// Falls zwischendurch doch etwas angemeldet wurde: vorher abräumen, nie doppelt.
|
|
raeumeEreignisseAuf();
|
|
|
|
// SSE-Abonnements
|
|
entladeEreignisse.push(
|
|
abonnieren("chat", (daten) => {
|
|
const aktiveId = holeAktiveInstanz();
|
|
if (daten.agentId === aktiverAgentId && (!daten.instanz || !aktiveId || daten.instanz === aktiveId)) {
|
|
fuegeNachrichtEin(daten);
|
|
}
|
|
})
|
|
);
|
|
|
|
entladeEreignisse.push(
|
|
abonnieren("step", (daten) => {
|
|
const aktiveId = holeAktiveInstanz();
|
|
if (daten.agentId === aktiverAgentId && (!daten.instanz || !aktiveId || daten.instanz === aktiveId)) {
|
|
zeigeSchritt(daten.text);
|
|
}
|
|
})
|
|
);
|
|
|
|
entladeEreignisse.push(
|
|
abonnieren("run", (daten) => {
|
|
const aktiveId = holeAktiveInstanz();
|
|
if (!daten.instanz || !aktiveId || daten.instanz === aktiveId) {
|
|
versteckeSchritt();
|
|
aktualisiereLaufStatus(false);
|
|
}
|
|
})
|
|
);
|
|
|
|
entladeEreignisse.push(
|
|
abonnieren("instance", (daten) => {
|
|
const aktiveId = holeAktiveInstanz();
|
|
if (daten.kind === "Agents" && (!daten.instanz || !aktiveId || daten.instanz === aktiveId)) {
|
|
ladeAgenten(aktiverAgentId);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
async function ladeAgenten(gewuenschteId = null) {
|
|
try {
|
|
agentenListe = await api("/agents");
|
|
agentenSelector.replaceChildren();
|
|
|
|
if (!agentenListe || agentenListe.length === 0) {
|
|
const opt = erstelleElement("option", { text: "Keine Agenten vorhanden", value: "" });
|
|
agentenSelector.append(opt);
|
|
statusPlakette.replaceWith(erstellePlakette("Keine Agenten", "warnung"));
|
|
return;
|
|
}
|
|
|
|
for (const a of agentenListe) {
|
|
const opt = erstelleElement("option", {
|
|
value: a.agentId,
|
|
text: `${a.displayName} (${a.agentId})`
|
|
});
|
|
agentenSelector.append(opt);
|
|
}
|
|
|
|
const zielId = gewuenschteId && agentenListe.some((a) => a.agentId === gewuenschteId)
|
|
? gewuenschteId
|
|
: agentenListe[0].agentId;
|
|
|
|
agentenSelector.value = zielId;
|
|
await waehleAgent(zielId);
|
|
} catch (err) {
|
|
zeigeMeldung(`Fehler beim Laden der Agenten: ${err.message}`, "gefahr");
|
|
}
|
|
}
|
|
|
|
async function waehleAgent(agentId) {
|
|
aktiverAgentId = agentId;
|
|
const agent = agentenListe.find((a) => a.agentId === agentId);
|
|
aktualisiereLaufStatus(agent?.isRunning ?? false);
|
|
|
|
// Adresse mitführen, ohne hashchange auszulösen (#592): Ein Setzen von location.hash
|
|
// ließ den Router die ganze Ansicht mitten im Laden ein zweites Mal aufbauen.
|
|
const ziel = `#/chat/${encodeURIComponent(agentId)}`;
|
|
if (window.location.hash !== ziel) {
|
|
history.replaceState(null, "", ziel);
|
|
}
|
|
|
|
await ladeChatVerlauf(agentId);
|
|
}
|
|
|
|
function aktualisiereLaufStatus(laeuft) {
|
|
if (statusPlakette) {
|
|
const neuePlakette = laeuft
|
|
? erstellePlakette("Beschäftigt", "warnung")
|
|
: erstellePlakette("Bereit", "erfolg");
|
|
statusPlakette.replaceWith(neuePlakette);
|
|
statusPlakette = neuePlakette;
|
|
}
|
|
|
|
if (abbrechenButton) {
|
|
abbrechenButton.hidden = !laeuft;
|
|
}
|
|
}
|
|
|
|
async function ladeChatVerlauf(agentId) {
|
|
if (!nachrichtenContainer) return;
|
|
nachrichtenContainer.replaceChildren(
|
|
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Verlauf…" })
|
|
);
|
|
|
|
try {
|
|
const verlauf = await api(`/agents/${encodeURIComponent(agentId)}/chat`);
|
|
nachrichtenContainer.replaceChildren();
|
|
|
|
if (!verlauf || verlauf.length === 0) {
|
|
nachrichtenContainer.append(
|
|
erstelleElement("div", {
|
|
klasse: "hinweis-zentriert",
|
|
text: "Noch keine Nachrichten im Verlauf vorhanden."
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
for (const eintrag of verlauf) {
|
|
fuegeNachrichtEin(eintrag);
|
|
}
|
|
|
|
scrolleNachUnten();
|
|
} catch (err) {
|
|
nachrichtenContainer.replaceChildren(
|
|
erstelleElement("div", {
|
|
klasse: "fehler-zentriert",
|
|
text: `Verlauf konnte nicht geladen werden: ${err.message}`
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
function fuegeNachrichtEin(eintrag) {
|
|
// Falls aktuell der Hinweis angezeigt wird, entfernen
|
|
const platzhalter = nachrichtenContainer.querySelector(".hinweis-zentriert");
|
|
if (platzhalter) platzhalter.remove();
|
|
|
|
const rolle = (eintrag.role || "user").toLowerCase();
|
|
const istUser = rolle === "user";
|
|
const istSystem = rolle === "system";
|
|
const istTool = rolle === "tool";
|
|
|
|
let rollenKlasse = "nachricht-benutzer";
|
|
let rollenTitel = "Benutzer";
|
|
|
|
if (istSystem) {
|
|
rollenKlasse = "nachricht-system";
|
|
rollenTitel = "System";
|
|
} else if (istTool) {
|
|
rollenKlasse = "nachricht-werkzeug";
|
|
rollenTitel = "Werkzeug";
|
|
} else if (!istUser) {
|
|
rollenKlasse = "nachricht-agent";
|
|
rollenTitel = agentenListe.find((a) => a.agentId === aktiverAgentId)?.displayName || "Agent";
|
|
}
|
|
|
|
const zeile = erstelleElement("div", { klasse: `chat-zeile ${istUser ? "zeile-benutzer" : "zeile-fremd"}` });
|
|
const nachrichtCard = erstelleElement("div", { klasse: `nachricht-blase ${rollenKlasse}` });
|
|
|
|
const kopf = erstelleElement("div", { klasse: "nachricht-meta" });
|
|
const autor = erstelleElement("strong", { text: rollenTitel });
|
|
const zeit = erstelleElement("span", {
|
|
klasse: "nachricht-zeit",
|
|
text: formatiereZeit(eintrag.timestamp)
|
|
});
|
|
|
|
kopf.append(autor);
|
|
if (eintrag.instanzName) {
|
|
const instanzBadge = erstelleElement("span", { klasse: "quelle-tag", text: eintrag.instanzName });
|
|
kopf.append(instanzBadge);
|
|
}
|
|
if (eintrag.source) {
|
|
const quelleBadge = erstelleElement("span", { klasse: "quelle-tag", text: eintrag.source });
|
|
kopf.append(quelleBadge);
|
|
}
|
|
kopf.append(zeit);
|
|
|
|
// WICHTIG (Regel 3.1): Fremdtext ist NIE HTML! Strikt textContent!
|
|
const textKörper = erstelleElement("div", {
|
|
klasse: "nachricht-text",
|
|
text: eintrag.content || ""
|
|
});
|
|
|
|
nachrichtCard.append(kopf, textKörper);
|
|
zeile.append(nachrichtCard);
|
|
nachrichtenContainer.append(zeile);
|
|
|
|
scrolleNachUnten();
|
|
}
|
|
|
|
function zeigeSchritt(text) {
|
|
if (!schrittBox) return;
|
|
const span = schrittBox.querySelector(".schritt-text");
|
|
if (span) span.textContent = text;
|
|
schrittBox.hidden = false;
|
|
aktualisiereLaufStatus(true);
|
|
}
|
|
|
|
function versteckeSchritt() {
|
|
if (schrittBox) schrittBox.hidden = true;
|
|
}
|
|
|
|
function scrolleNachUnten() {
|
|
if (nachrichtenContainer) {
|
|
nachrichtenContainer.scrollTop = nachrichtenContainer.scrollHeight;
|
|
}
|
|
}
|
|
|
|
async function sendeNachricht() {
|
|
if (!aktiverAgentId || !eingabeTextarea) return;
|
|
const nachricht = eingabeTextarea.value.trim();
|
|
if (!nachricht) return;
|
|
|
|
eingabeTextarea.value = "";
|
|
sendenButton.disabled = true;
|
|
aktualisiereLaufStatus(true);
|
|
|
|
try {
|
|
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/chat`, {
|
|
method: "POST",
|
|
body: { message: nachricht }
|
|
});
|
|
} catch (err) {
|
|
zeigeMeldung(`Nachricht konnte nicht gesendet werden: ${err.message}`, "gefahr");
|
|
aktualisiereLaufStatus(false);
|
|
} finally {
|
|
sendenButton.disabled = false;
|
|
eingabeTextarea.focus();
|
|
}
|
|
}
|
|
|
|
async function brecheChatAb() {
|
|
if (!aktiverAgentId) return;
|
|
try {
|
|
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/chat/abort`, {
|
|
method: "POST"
|
|
});
|
|
versteckeSchritt();
|
|
aktualisiereLaufStatus(false);
|
|
zeigeMeldung("Lauf wurde abgebrochen.", "warnung");
|
|
} catch (err) {
|
|
zeigeMeldung(`Abbruch fehlgeschlagen: ${err.message}`, "gefahr");
|
|
}
|
|
}
|
|
|
|
async function weckeAktivenAgent() {
|
|
if (!aktiverAgentId) return;
|
|
try {
|
|
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/run`, {
|
|
method: "POST"
|
|
});
|
|
aktualisiereLaufStatus(true);
|
|
zeigeMeldung("Agent wurde manuell gestartet.", "info");
|
|
} catch (err) {
|
|
zeigeMeldung(`Starten fehlgeschlagen: ${err.message}`, "gefahr");
|
|
}
|
|
}
|
|
|
|
async function leereChatverlauf() {
|
|
if (!aktiverAgentId) return;
|
|
if (!confirm("Soll der gesamte Chatverlauf dieses Agenten unwiderruflich gelöscht werden?")) return;
|
|
|
|
try {
|
|
await api(`/agents/${encodeURIComponent(aktiverAgentId)}/chat`, {
|
|
method: "DELETE"
|
|
});
|
|
nachrichtenContainer.replaceChildren(
|
|
erstelleElement("div", {
|
|
klasse: "hinweis-zentriert",
|
|
text: "Verlauf wurde geleert."
|
|
})
|
|
);
|
|
zeigeMeldung("Chatverlauf geleert.", "info");
|
|
} catch (err) {
|
|
zeigeMeldung(`Leeren fehlgeschlagen: ${err.message}`, "gefahr");
|
|
}
|
|
}
|