Stop War Room A2A loops and unlock full chat history with dates.
Detect repeating topics and pause A2A, load older messages beyond the old ~120 cap, and show clear date/time stamps on every bubble.
This commit is contained in:
@@ -572,16 +572,36 @@ async def warroom_open_dm(agent_key: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
@router.get("/warroom/rooms/{room_id}/messages")
|
||||
async def warroom_list_messages(room_id: int, limit: int = 100, after_id: int | None = None) -> dict[str, Any]:
|
||||
async def warroom_list_messages(
|
||||
room_id: int,
|
||||
limit: int = 500,
|
||||
after_id: int | None = None,
|
||||
before_id: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
from app.services import agent_chat
|
||||
|
||||
agent_chat.ensure_warroom()
|
||||
room = agent_chat.get_room(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404, "Room niet gevonden")
|
||||
items = agent_chat.list_messages(room_id, limit=min(limit, 300), after_id=after_id)
|
||||
lim = min(max(1, limit), 5000)
|
||||
items = agent_chat.list_messages(
|
||||
room_id, limit=lim, after_id=after_id, before_id=before_id
|
||||
)
|
||||
deleted_ids = agent_chat.list_deleted_ids(room_id) if after_id else []
|
||||
return {"ok": True, "room": room, "items": items, "deleted_ids": deleted_ids}
|
||||
total = agent_chat.count_messages(room_id)
|
||||
oldest = agent_chat.oldest_message_id(room_id)
|
||||
first_id = int(items[0]["id"]) if items else None
|
||||
has_more = bool(oldest is not None and first_id is not None and first_id > oldest)
|
||||
return {
|
||||
"ok": True,
|
||||
"room": room,
|
||||
"items": items,
|
||||
"deleted_ids": deleted_ids,
|
||||
"total": total,
|
||||
"has_more": has_more,
|
||||
"oldest_id": oldest,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/warroom/rooms/{room_id}/messages")
|
||||
|
||||
@@ -197,13 +197,30 @@ def get_room(room_id: int) -> dict[str, Any] | None:
|
||||
return _iso(fetch_one("SELECT * FROM warroom_rooms WHERE id = %s", (room_id,)))
|
||||
|
||||
|
||||
def list_messages(room_id: int, *, limit: int = 100, after_id: int | None = None) -> list[dict[str, Any]]:
|
||||
def list_messages(
|
||||
room_id: int,
|
||||
*,
|
||||
limit: int = 100,
|
||||
after_id: int | None = None,
|
||||
before_id: int | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List messages. Default = newest page. before_id = older page (scroll up)."""
|
||||
lim = max(1, min(int(limit), 5000))
|
||||
if after_id:
|
||||
rows = fetch_all(
|
||||
"""SELECT * FROM warroom_messages
|
||||
WHERE room_id = %s AND id > %s AND deleted_at IS NULL
|
||||
ORDER BY created_at ASC, id ASC LIMIT %s""",
|
||||
(room_id, after_id, limit),
|
||||
(room_id, after_id, lim),
|
||||
)
|
||||
elif before_id:
|
||||
rows = fetch_all(
|
||||
"""SELECT * FROM (
|
||||
SELECT * FROM warroom_messages
|
||||
WHERE room_id = %s AND id < %s AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC, id DESC LIMIT %s
|
||||
) t ORDER BY created_at ASC, id ASC""",
|
||||
(room_id, before_id, lim),
|
||||
)
|
||||
else:
|
||||
rows = fetch_all(
|
||||
@@ -212,11 +229,29 @@ def list_messages(room_id: int, *, limit: int = 100, after_id: int | None = None
|
||||
WHERE room_id = %s AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC, id DESC LIMIT %s
|
||||
) t ORDER BY created_at ASC, id ASC""",
|
||||
(room_id, limit),
|
||||
(room_id, lim),
|
||||
)
|
||||
return [_iso(r) for r in rows] # type: ignore
|
||||
|
||||
|
||||
def count_messages(room_id: int) -> int:
|
||||
row = fetch_one(
|
||||
"SELECT COUNT(*) AS c FROM warroom_messages WHERE room_id = %s AND deleted_at IS NULL",
|
||||
(room_id,),
|
||||
)
|
||||
return int((row or {}).get("c") or 0)
|
||||
|
||||
|
||||
def oldest_message_id(room_id: int) -> int | None:
|
||||
row = fetch_one(
|
||||
"""SELECT id FROM warroom_messages
|
||||
WHERE room_id = %s AND deleted_at IS NULL
|
||||
ORDER BY id ASC LIMIT 1""",
|
||||
(room_id,),
|
||||
)
|
||||
return int(row["id"]) if row and row.get("id") is not None else None
|
||||
|
||||
|
||||
def delete_message(room_id: int, message_id: int) -> dict[str, Any] | None:
|
||||
"""Soft-delete a warroom message. Returns the row or None if missing/already deleted."""
|
||||
row = fetch_one(
|
||||
@@ -508,8 +543,10 @@ def _soul_system(soul: dict[str, Any], *, room_kind: str) -> str:
|
||||
accuracy
|
||||
+ "War Room-regels:\n"
|
||||
+ "- Max 4 korte zinnen, Nederlands.\n"
|
||||
+ "- Geen @zelf-prefix / geen herhalingen.\n"
|
||||
+ "- Peer @mentions verplicht: eindig met @iemand + gerichte vraag of reactie.\n"
|
||||
+ "- Geen @zelf-prefix.\n"
|
||||
+ "- GEEN herhalingen: zeg niet opnieuw wat net al besloten/gezegd is.\n"
|
||||
+ "- Peer @mentions alleen als je écht iets nieuws toevoegt; anders geen @mention-ketting.\n"
|
||||
+ "- Bij gesloten besluiten (gestopt/klaar): max 1 bevestiging, daarna ander dossier of stop.\n"
|
||||
+ "- Zeg nooit dat agents uit staan.\n"
|
||||
+ "- Menselijk & echt: reageer concreet op de vorige spreker (feit, mop of vraag).\n"
|
||||
+ "- Lichte food-moppen / Foodlinkk-humor mag (respectvol, geen geloof/politiek/grof).\n"
|
||||
@@ -1200,7 +1237,9 @@ async def post_user_message_stream(
|
||||
user_focus = (
|
||||
f"Prioriteit: beantwoord EERST het bericht van {username or 'Aissa'}. "
|
||||
f"Spreek hem aan als Aissa. "
|
||||
f"Voor escalatie naar CEO-inbox: @aissa + concrete vraag. Daarna optioneel 1 @mention naar een collega."
|
||||
f"Voor escalatie naar CEO-inbox: @aissa + concrete vraag. "
|
||||
f"Geen herhaling van eerdere berichten. Max 3 zinnen. "
|
||||
f"@mention alleen als je iets nieuws toevoegt."
|
||||
)
|
||||
for key in speakers:
|
||||
async for ev in _emit_speak(key, trig=f"{trigger}\n\n{user_focus}"):
|
||||
@@ -1220,6 +1259,74 @@ async def post_user_message_stream(
|
||||
yield {"event": "done", "ok": True, "a2a": True}
|
||||
|
||||
|
||||
|
||||
def _norm_msg(text: str) -> str:
|
||||
t = (text or "").lower()
|
||||
t = re.sub(r"@\w+", " ", t)
|
||||
t = re.sub(r"[^a-z0-9àáäâèéëêìíïîòóöôùúüû\s]+", " ", t)
|
||||
t = re.sub(r"\s+", " ", t).strip()
|
||||
return t
|
||||
|
||||
|
||||
def _token_set(text: str) -> set[str]:
|
||||
stop = {
|
||||
"de", "het", "een", "en", "of", "op", "voor", "van", "naar", "met", "zijn", "niet",
|
||||
"geen", "wel", "dat", "die", "dit", "dan", "ook", "als", "bij", "uit", "aan", "er",
|
||||
"ik", "je", "we", "jij", "hij", "zij", "aissa", "herman", "even", "check", "checken",
|
||||
"begrepen", "helder", "prima", "punt", "dus", "nog", "meer", "kun", "kan", "moet",
|
||||
}
|
||||
return {w for w in _norm_msg(text).split() if len(w) > 2 and w not in stop}
|
||||
|
||||
|
||||
def _overlap_ratio(a: str, b: str) -> float:
|
||||
sa, sb = _token_set(a), _token_set(b)
|
||||
if not sa or not sb:
|
||||
return 0.0
|
||||
return len(sa & sb) / max(1, min(len(sa), len(sb)))
|
||||
|
||||
|
||||
def _is_conversation_looping(messages: list[dict], *, min_msgs: int = 4, threshold: float = 0.42) -> bool:
|
||||
"""True if recent agent messages keep restating the same decision/theme."""
|
||||
agents = [m for m in messages if m.get("sender_type") == "agent"][-8:]
|
||||
if len(agents) < min_msgs:
|
||||
return False
|
||||
texts = [str(m.get("content") or "") for m in agents]
|
||||
# pairwise overlap among last messages
|
||||
hits = 0
|
||||
pairs = 0
|
||||
for i in range(len(texts)):
|
||||
for j in range(i + 1, len(texts)):
|
||||
pairs += 1
|
||||
if _overlap_ratio(texts[i], texts[j]) >= threshold:
|
||||
hits += 1
|
||||
if pairs and (hits / pairs) >= 0.45:
|
||||
return True
|
||||
# keyword lock: same closed decision repeated
|
||||
lock_words = ("website stopt", "website stop", "geen website", "dielines", "label-mock", "op de nas", "scheelt", "oprui")
|
||||
recent_join = " ".join(_norm_msg(t) for t in texts[-5:])
|
||||
lock_hits = sum(1 for w in lock_words if w in recent_join)
|
||||
return lock_hits >= 3
|
||||
|
||||
|
||||
def _anti_repeat_block(messages: list[dict], *, limit: int = 4) -> str:
|
||||
agents = [m for m in messages if m.get("sender_type") == "agent"][-limit:]
|
||||
if not agents:
|
||||
return ""
|
||||
lines = ["ANTI-HERHALING (hard):"]
|
||||
lines.append("- Herhaal NIET wat collega's net al zeiden (besluit, mop, of check-vraag).")
|
||||
lines.append("- Als het besluit al gevallen is: 1 korte bevestiging MAX, daarna NIEUW onderwerp of stilte.")
|
||||
lines.append("- Verboden: opnieuw 'website stopt', 'dielines op NAS', 'even checken @product' als dat al 2x gezegd is.")
|
||||
lines.append("- Lever 1 nieuw feit/actie uit JOUW rol, of zeg niks nieuws en @mention iemand met een écht nieuwe vraag.")
|
||||
lines.append("Recent al gezegd (NIET napraten):")
|
||||
for m in agents:
|
||||
who = m.get("agent_key") or "?"
|
||||
excerpt = (m.get("content") or "").strip().replace("\n", " ")
|
||||
if len(excerpt) > 140:
|
||||
excerpt = excerpt[:139] + "…"
|
||||
lines.append(f"- @{who}: {excerpt}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def continue_warroom_stream(
|
||||
room_id: int,
|
||||
*,
|
||||
@@ -1267,7 +1374,40 @@ async def continue_warroom_stream(
|
||||
if not topic:
|
||||
topic = "Volg Aissa's input als leidraad; spreek hem aan als Aissa; wissel cockpit-dossier met lichte humor; escaleer alleen met @aissa als echt nodig."
|
||||
|
||||
produced = [m for m in recent if m.get("sender_type") == "agent"][-6:]
|
||||
produced = [m for m in recent if m.get("sender_type") == "agent"][-8:]
|
||||
anti = _anti_repeat_block(recent, limit=5)
|
||||
looping = _is_conversation_looping(recent)
|
||||
|
||||
# Bij herhalingslus: stop autonome A2A i.p.v. nog meer napraters
|
||||
if looping:
|
||||
yield {
|
||||
"event": "continue_start",
|
||||
"topic": "Herhaling gedetecteerd — A2A pauzeert dit onderwerp",
|
||||
}
|
||||
# Herman mag 1x afronden en doorpakken, daarna klaar
|
||||
close_trig = (
|
||||
f"Topic van Aissa: {topic}\n"
|
||||
f"{anti}\n"
|
||||
"De War Room herhaalt zich. Jij bent @herman.\n"
|
||||
"Schrijf MAX 3 zinnen: (1) besluit is al genomen — stop dit onderwerp, "
|
||||
"(2) noem 1 concreet ander dossier uit cockpit (CRM/retail/pitch), "
|
||||
"(3) GEEN @mention-ketting over website/packaging/NAS-opruimen.\n"
|
||||
"Geen mop verplicht. Natuurlijk NL."
|
||||
)
|
||||
async for kind_ev, payload in _agent_speak_stream(
|
||||
room, "herman", trigger=close_trig, hermes_user=hermes_user, in_reply_to=None
|
||||
):
|
||||
if kind_ev == "start":
|
||||
yield {"event": "typing_start", **payload}
|
||||
elif kind_ev == "token":
|
||||
yield {"event": "typing", **payload}
|
||||
elif kind_ev == "done":
|
||||
yield {"event": "agent", "message": payload}
|
||||
elif kind_ev == "error":
|
||||
yield {"event": "system", "message": payload}
|
||||
yield {"event": "done", "ok": True, "continued": True, "loop_break": True}
|
||||
return
|
||||
|
||||
yield {"event": "continue_start", "topic": topic[:200]}
|
||||
|
||||
for turn in range(max(1, min(int(max_turns), 5))):
|
||||
@@ -1295,16 +1435,18 @@ async def continue_warroom_stream(
|
||||
src = last
|
||||
trig = (
|
||||
f"Autonoom War Room A2A (user kan meelezen). Topic: {topic}\n"
|
||||
f"Jij bent @{target}. Reageer ECHT op @{src}.\n"
|
||||
f"Kies passend: (a) 1 cockpit-feit + gerichte vraag, OF (b) korte respectvolle food-mop + @mention terug naar de inhoud.\n"
|
||||
f"Breng JOUW expertise in (niet herhalen wat @{src} zei). Spreek de CEO aan als Aissa. Altijd @mention 1 collega; escalatie naar CEO-inbox via @aissa. Max 4 zinnen. Natuurlijk NL."
|
||||
f"{anti}\n"
|
||||
f"Jij bent @{target}. Reageer op @{src} met NIEUWE info uit jouw rol.\n"
|
||||
f"Verboden: napraten van hetzelfde besluit. Max 3 zinnen. "
|
||||
f"@mention alleen bij écht nieuwe vraag. Spreek Aissa aan als Aissa."
|
||||
)
|
||||
else:
|
||||
target, src = nxt[0]
|
||||
trig = (
|
||||
f"Autonoom War Room A2A. Topic: {topic}\n"
|
||||
f"@{src} sprak je aan. Reageer op die inhoud (of mop) alsof je in een echt teamoverleg zit.\n"
|
||||
f"Geef waarde vanuit JOUW rol (feit/actie/vraag). Trek er 1 collega bij die iets kan toevoegen via @mention — niet alleen Herman. Korte food-humor mag."
|
||||
f"{anti}\n"
|
||||
f"@{src} noemde je. Geef 1 NIEUW punt vanuit jouw rol — geen echo.\n"
|
||||
f"Max 3 zinnen. Geen herhaling van website-stop/NAS-check als dat al gezegd is."
|
||||
)
|
||||
|
||||
async for kind_ev, payload in _agent_speak_stream(
|
||||
|
||||
@@ -662,3 +662,37 @@
|
||||
background: rgba(14, 40, 64, 0.98);
|
||||
border-color: rgba(56, 189, 248, 0.7);
|
||||
}
|
||||
|
||||
|
||||
.wr-history-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.45rem 0.35rem 0.65rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
background: linear-gradient(180deg, rgba(8,12,18,0.96), rgba(8,12,18,0.72) 70%, transparent);
|
||||
}
|
||||
.wr-time {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.wr-day-sep {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0.75rem 0 0.5rem;
|
||||
}
|
||||
.wr-day-sep span {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.2rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ window.WarRoomHub = (function () {
|
||||
replyHighlightId: null,
|
||||
stickToBottom: true,
|
||||
showJumpLatest: false,
|
||||
hasMoreHistory: false,
|
||||
loadingHistory: false,
|
||||
historyTotal: 0,
|
||||
_loadingOlder: false,
|
||||
_typingScrollAt: 0,
|
||||
rosterCollapsed: true,
|
||||
a2aPausedUntil: 0,
|
||||
@@ -158,13 +162,15 @@ window.WarRoomHub = (function () {
|
||||
try {
|
||||
if (!(this.rooms || []).length) await this.loadRooms();
|
||||
var meta = (this.rooms || []).find(function (r) { return Number(r.id) === id; }) || null;
|
||||
var r = await fetch('/api/agents/warroom/rooms/' + id + '/messages?limit=120').then(function (x) {
|
||||
var r = await fetch('/api/agents/warroom/rooms/' + id + '/messages?limit=800').then(function (x) {
|
||||
return x.json();
|
||||
});
|
||||
// messages API returns {items}, NOT {room} — keep room from rooms list
|
||||
this.room = meta || r.room || { id: id, kind: 'warroom', title: 'War Room' };
|
||||
this.messages = this.normalizeList(r.items || []);
|
||||
this.lastId = this.maxId(this.messages);
|
||||
this.historyTotal = r.total || this.messages.length;
|
||||
this.hasMoreHistory = !!r.has_more;
|
||||
this.stickToBottom = true;
|
||||
this.showJumpLatest = false;
|
||||
this.$nextTick(function () {
|
||||
@@ -174,7 +180,7 @@ window.WarRoomHub = (function () {
|
||||
}.bind(this));
|
||||
var last = (this.messages || [])[(this.messages || []).length - 1];
|
||||
if (last && last.sender_type === 'agent' && this.room && this.room.kind === 'warroom') {
|
||||
if (this.autoDebate) this.scheduleContinue(60000);
|
||||
if (this.autoDebate) this.scheduleContinue(120000);
|
||||
}
|
||||
} catch (e) {
|
||||
this.status = e.message || 'Laden mislukt';
|
||||
@@ -246,6 +252,58 @@ window.WarRoomHub = (function () {
|
||||
var near = this.isNearBottom(el, 100);
|
||||
this.stickToBottom = near;
|
||||
this.showJumpLatest = !near;
|
||||
// bovenaan: oudere geschiedenis laden
|
||||
if (el.scrollTop < 80) this.loadOlderMessages();
|
||||
},
|
||||
|
||||
minId(items) {
|
||||
var n = null;
|
||||
(items || []).forEach(function (m) {
|
||||
var id = Number(m && m.id);
|
||||
if (!isNaN(id) && (n === null || id < n)) n = id;
|
||||
});
|
||||
return n;
|
||||
},
|
||||
|
||||
async loadOlderMessages() {
|
||||
if (!this.room || !this.room.id) return;
|
||||
if (this.loadingHistory || this._loadingOlder || !this.hasMoreHistory) return;
|
||||
if (!(this.messages || []).length) return;
|
||||
this._loadingOlder = true;
|
||||
this.loadingHistory = true;
|
||||
var el = this.$refs && this.$refs.wrLog;
|
||||
var prevHeight = el ? el.scrollHeight : 0;
|
||||
var prevTop = el ? el.scrollTop : 0;
|
||||
try {
|
||||
var before = this.minId(this.messages);
|
||||
var r = await fetch(
|
||||
'/api/agents/warroom/rooms/' + this.room.id +
|
||||
'/messages?limit=400&before_id=' + before,
|
||||
{ credentials: 'same-origin' }
|
||||
).then(function (x) { return x.json(); });
|
||||
var older = this.normalizeList(r.items || []);
|
||||
if (older.length) {
|
||||
var existing = {};
|
||||
(this.messages || []).forEach(function (m) { existing[String(m.id)] = true; });
|
||||
var merged = older.filter(function (m) { return !existing[String(m.id)]; })
|
||||
.concat(this.messages || []);
|
||||
this.messages = merged;
|
||||
this.historyTotal = r.total || this.historyTotal;
|
||||
this.hasMoreHistory = !!r.has_more;
|
||||
var self = this;
|
||||
this.$nextTick(function () {
|
||||
self.paintAvatars();
|
||||
if (el) {
|
||||
// scrollpositie behouden na prepend
|
||||
el.scrollTop = el.scrollHeight - prevHeight + prevTop;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.hasMoreHistory = false;
|
||||
}
|
||||
} catch (e) { /* quiet */ }
|
||||
this.loadingHistory = false;
|
||||
this._loadingOlder = false;
|
||||
},
|
||||
|
||||
scrollLog(force) {
|
||||
@@ -538,7 +596,7 @@ window.WarRoomHub = (function () {
|
||||
setTimeout(function () { self.replyHighlightId = null; }, 4000);
|
||||
});
|
||||
}
|
||||
if (this.autoDebate) this.scheduleContinue(60000);
|
||||
if (this.autoDebate) this.scheduleContinue(120000);
|
||||
} catch (e) {
|
||||
// Keep optimistic message; restore draft only on hard fail
|
||||
this.status = e.message || 'Verzenden mislukt';
|
||||
@@ -642,6 +700,10 @@ window.WarRoomHub = (function () {
|
||||
if (window.Cockpit) Cockpit.toast(this.status, 'error');
|
||||
} else if (type === 'done') {
|
||||
this.typing = null;
|
||||
if (ev.loop_break) {
|
||||
this.pauseA2A(180000);
|
||||
this.status = 'A2A pauze — onderwerp herhaalde zich';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -733,13 +795,15 @@ window.WarRoomHub = (function () {
|
||||
opts = opts || {};
|
||||
if (!this.room || !this.room.id) return;
|
||||
try {
|
||||
var r = await fetch('/api/agents/warroom/rooms/' + this.room.id + '/messages?limit=200', {
|
||||
var r = await fetch('/api/agents/warroom/rooms/' + this.room.id + '/messages?limit=800', {
|
||||
credentials: 'same-origin',
|
||||
}).then(function (x) { return x.json(); });
|
||||
var items = this.normalizeList(r.items || []);
|
||||
if (items.length) {
|
||||
this.messages = items;
|
||||
this.lastId = this.maxId(items);
|
||||
this.historyTotal = r.total || items.length;
|
||||
this.hasMoreHistory = !!r.has_more;
|
||||
}
|
||||
if (!opts.silent) {
|
||||
this.$nextTick(function () {
|
||||
@@ -793,11 +857,11 @@ window.WarRoomHub = (function () {
|
||||
this.busy = false;
|
||||
this.status = 'A2A live';
|
||||
// keep conversation alive
|
||||
this.scheduleContinue(55000);
|
||||
this.scheduleContinue(90000);
|
||||
} catch (e) {
|
||||
this.busy = false;
|
||||
this.status = 'A2A hervatten…';
|
||||
this.scheduleContinue(45000);
|
||||
this.scheduleContinue(75000);
|
||||
}
|
||||
this._continueBusy = false;
|
||||
},
|
||||
@@ -871,6 +935,21 @@ window.WarRoomHub = (function () {
|
||||
return d.toLocaleDateString('nl-NL', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' });
|
||||
},
|
||||
|
||||
/** Compact stamp on each bubble: 16 aug · 00:53 */
|
||||
formatMsgStamp(m) {
|
||||
var raw = (m && (m.created_at || m.createdAt || m.ts)) || '';
|
||||
if (!raw) return '';
|
||||
var d = new Date(raw);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
var today = new Date();
|
||||
var sameDay = d.getFullYear() === today.getFullYear() && d.getMonth() === today.getMonth() && d.getDate() === today.getDate();
|
||||
var time = d.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });
|
||||
if (sameDay) return time;
|
||||
var date = d.toLocaleDateString('nl-NL', { day: 'numeric', month: 'short', year: d.getFullYear() !== today.getFullYear() ? 'numeric' : undefined });
|
||||
return date + ' · ' + time;
|
||||
},
|
||||
|
||||
|
||||
dayKey(m) {
|
||||
var raw = (m && (m.created_at || m.createdAt || m.ts)) || '';
|
||||
if (!raw) return '';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
|
||||
<link rel="stylesheet" href="/static/css/agents-mesh.css?v=8" />
|
||||
<link rel="stylesheet" href="/static/css/agent-characters.css?v=8" />
|
||||
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=40" />
|
||||
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=42" />
|
||||
<script src="/static/js/agent-avatars.js?v=5"></script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
@@ -313,7 +313,7 @@
|
||||
<h2 x-text="room ? (room.title || ('#'+room.kind)) : 'War Room'"></h2>
|
||||
<small class="muted" x-show="status" x-text="status"></small>
|
||||
<small class="wr-msg-count muted" x-show="messages.length"
|
||||
x-text="(screenCleared ? '0' : visibleMessages().length) + ' / ' + messages.length + ' berichten'"></small>
|
||||
x-text="(screenCleared ? '0' : visibleMessages().length) + ' geladen' + (historyTotal ? (' · ' + historyTotal + ' totaal') : '')"></small>
|
||||
</div>
|
||||
<div class="wr-head-actions">
|
||||
<button type="button" class="btn btn-sm wr-tool-btn" :class="{active: searchOpen}" @click="toggleSearch()" title="Zoeken in chat">🔍 Zoek</button>
|
||||
@@ -352,6 +352,13 @@
|
||||
<button type="button" class="btn btn-sm" @click="toggleSearch()">Sluit</button>
|
||||
</div>
|
||||
<div class="wr-log-wrap"><div class="wr-log" x-ref="wrLog" @scroll.passive="onLogScroll()">
|
||||
<div class="wr-history-bar" x-show="!screenCleared && (hasMoreHistory || loadingHistory)" x-cloak>
|
||||
<button type="button" class="btn btn-sm" @click="loadOlderMessages()" :disabled="loadingHistory">
|
||||
<span x-show="!loadingHistory">↑ Oudere berichten laden</span>
|
||||
<span x-show="loadingHistory">Laden…</span>
|
||||
</button>
|
||||
<small class="muted" x-show="historyTotal" x-text="'Totaal in archief: ' + historyTotal"></small>
|
||||
</div>
|
||||
<p class="wr-empty" x-show="screenCleared && !typing">
|
||||
Scherm is geleegd.
|
||||
<button type="button" class="btn btn-sm" @click="restoreScreen()">Herstellen</button>
|
||||
@@ -383,7 +390,7 @@
|
||||
<div class="wr-bubble-meta">
|
||||
<span class="wr-who" x-text="labelForMessage(m)"></span>
|
||||
<span class="wr-handle" x-show="m.agent_key" x-text="'@'+m.agent_key"></span>
|
||||
<time class="wr-time" x-text="formatMsgTime(m)" :title="formatMsgDate(m) + ' ' + formatMsgTime(m)"></time>
|
||||
<time class="wr-time" x-text="formatMsgStamp(m)" :title="formatMsgDate(m) + ' ' + formatMsgTime(m)"></time>
|
||||
<button type="button" class="wr-msg-reply" title="Beantwoorden"
|
||||
@click.stop="setReply(m)">↩ Reply</button>
|
||||
<button type="button" class="wr-msg-del" title="Bericht verwijderen"
|
||||
@@ -622,7 +629,7 @@
|
||||
<script src="/static/js/page-viz.js?v=1"></script>
|
||||
<script src="/static/js/agents-mesh.js?v=11"></script>
|
||||
<script src="/static/js/agents-terminals.js?v=6"></script>
|
||||
<script src="/static/js/agents-warroom.js?v=40"></script>
|
||||
<script src="/static/js/agents-warroom.js?v=42"></script>
|
||||
<script>
|
||||
function agentsSoulHub() {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user