Add War Room voice meeting, fix chat overlay, and ship 5 cyan UI themes.

Voice Room lets humans talk with agents via Whisper/TTS; chat typing no longer blocked by a full-screen overlay, attachments/avatars are larger, and Settings exposes five vivid mid-dark cyan/blue themes for the whole cockpit.
This commit is contained in:
Aissa
2026-08-16 15:49:16 +00:00
parent 1dd607b060
commit 910ba465d7
16 changed files with 2162 additions and 139 deletions
+178 -4
View File
@@ -573,10 +573,11 @@ 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]:
from app.services import agent_chat
room = agent_chat.get_room(room_id)
if not room:
raise HTTPException(404, "Room niet gevonden")
return {"room": room, "items": agent_chat.list_messages(room_id, limit=min(limit, 200), after_id=after_id)}
agent_chat.ensure_warroom()
items = agent_chat.list_messages(room_id, limit=min(limit, 300), after_id=after_id)
deleted_ids = agent_chat.list_deleted_ids(room_id) if after_id else []
return {"ok": True, "items": items, "deleted_ids": deleted_ids}
@router.post("/warroom/rooms/{room_id}/messages")
@@ -663,3 +664,176 @@ async def warroom_upload(file: UploadFile = File(...)) -> dict[str, Any]:
"mime": file.content_type or "application/octet-stream",
"size": len(data),
}
@router.post("/warroom/digest")
async def warroom_daily_digest(hours: int = 24, deliver: bool = True) -> dict[str, Any]:
"""Generate Herman War Room day report and optionally push to Aïssa + Mo on Telegram."""
from app.services import warroom_digest
try:
return await warroom_digest.run_daily_digest(hours=max(1, min(hours, 72)), deliver=deliver)
except Exception as exc:
raise HTTPException(500, str(exc)) from exc
@router.post("/warroom/rooms/{room_id}/continue")
async def warroom_continue(request: Request, room_id: int, turns: int = 2):
"""Autonomous A2A continuation — agents keep debating with cockpit data."""
import json
from app.services import agent_chat
user = getattr(request.state, "user", None) or {}
async def gen():
try:
async for ev in agent_chat.continue_warroom_stream(
room_id,
username=(user.get("username") if isinstance(user, dict) else None),
role=(user.get("role") if isinstance(user, dict) else None),
max_turns=max(1, min(int(turns or 2), 4)),
):
yield f"data: {json.dumps(ev, default=str)}\n\n"
except ValueError as exc:
yield f"data: {json.dumps({'event': 'error', 'detail': str(exc)})}\n\n"
except Exception as exc:
yield f"data: {json.dumps({'event': 'error', 'detail': str(exc)})}\n\n"
return StreamingResponse(
gen(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@router.delete("/warroom/rooms/{room_id}/messages/{message_id}")
async def warroom_delete_message(room_id: int, message_id: int) -> dict[str, Any]:
"""Soft-delete a War Room message."""
from app.services import agent_chat
room = agent_chat.get_room(room_id)
if not room:
raise HTTPException(404, "Room niet gevonden")
row = agent_chat.delete_message(room_id, message_id)
if not row:
raise HTTPException(404, "Bericht niet gevonden")
return {"ok": True, "id": message_id}
@router.post("/warroom/rooms/{room_id}/voice-room")
async def warroom_voice_room(request: Request, room_id: int) -> dict[str, Any]:
"""Open/announce a Foodlinkk Voice Room (humans + agents via STT/TTS)."""
from app.services import agent_chat
room = agent_chat.get_room(room_id)
if not room:
raise HTTPException(404, "Room niet gevonden")
body = {}
try:
body = await request.json()
except Exception:
body = {}
action = str(body.get("action") or "join").lower()
agents = body.get("agents") or ["herman", "retail", "bizdev", "halal", "finance"]
if not isinstance(agents, list):
agents = ["herman"]
agents = [str(a).strip().lower() for a in agents if str(a).strip()][:12]
user = getattr(request.state, "user", None) or {}
who = user.get("username") if isinstance(user, dict) else "user"
if action == "leave":
agent_chat.post_system_line(
f"🎙️ {who} verliet de Voice Room",
agent_key="herman",
metadata={"kind": "voice_room", "action": "leave", "opened_by": who},
)
return {"ok": True, "action": "leave"}
agent_chat.post_system_line(
f"🎙️ Voice Room open — {who} + agents: " + ", ".join("@" + a for a in agents),
agent_key="herman",
metadata={
"kind": "voice_room",
"action": "join",
"opened_by": who,
"agents": agents,
},
)
return {"ok": True, "action": "join", "agents": agents, "host": who}
@router.post("/warroom/rooms/{room_id}/call")
async def warroom_start_call(request: Request, room_id: int) -> dict[str, Any]:
"""Open a simple no-account video room in an external HTTPS window.
Cockpit stays on HTTP. Mic/camera need HTTPS in a separate tab.
Default provider is Freifunk meet (no 8x8 login / no moderator gate).
"""
import os
from urllib.parse import quote
from app.db import execute, fetch_one
from app.services import agent_chat
room = agent_chat.get_room(room_id)
if not room:
raise HTTPException(404, "Room niet gevonden")
base = (os.getenv("WARROOM_MEET_BASE") or "https://meet.ffmuc.net").rstrip("/")
slug = f"FoodlinkkWR{room_id}"
if room.get("kind") == "dm" and room.get("agent_key"):
slug = f"FoodlinkkDM{room.get('agent_key')}{room_id}"
url = f"{base}/{quote(slug)}"
user = getattr(request.state, "user", None) or {}
who = user.get("username") if isinstance(user, dict) else "user"
recent = fetch_one(
"""SELECT id, metadata FROM warroom_messages
WHERE room_id = %s AND deleted_at IS NULL
AND metadata->>'kind' = 'call'
AND created_at > NOW() - INTERVAL '20 minutes'
ORDER BY id DESC LIMIT 1""",
(room_id,),
)
reused = bool(recent and (recent.get("metadata") or {}).get("call_url") == url)
if not reused:
execute(
"""UPDATE warroom_messages
SET deleted_at = NOW(), content = '',
metadata = COALESCE(metadata, '{}'::jsonb) || '{"deleted": true}'::jsonb
WHERE room_id = %s AND deleted_at IS NULL
AND (
metadata->>'kind' = 'call'
OR content ILIKE %s
OR content ILIKE %s
)""",
(room_id, "%War Room-call%", "%Call open%"),
)
agent_chat.post_system_line(
f"📹 Call open — geen account nodig: {url}",
agent_key="herman",
metadata={"kind": "call", "call_url": url, "room_slug": slug, "opened_by": who},
)
return {
"ok": True,
"url": url,
"slug": slug,
"provider": "ffmuc",
"embed": False,
"mode": "external",
"reused": reused,
"hint": "Cockpit blijft op HTTP. Gesprek opent in een apart venster — geen login.",
}