diwash-barla1 aider (openai/editor-model) commited on
Commit
e4bef7f
·
1 Parent(s): f73e782

feat: add Phase 4 Mission Control UI and human-in-the-loop engine

Browse files

Co-authored-by: aider (openai/editor-model) <aider@aider.chat>

Files changed (5) hide show
  1. CHANGELOG.md +10 -0
  2. app.py +344 -2
  3. static/css/style.css +73 -0
  4. static/js/app.js +174 -0
  5. templates/index.html +171 -0
CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
  # Changelog
2
 
 
 
 
 
 
 
 
 
 
 
3
  ## [2.2.0-phase3] - Version 2.0.0 Phase 3 Release
4
 
5
  ### Added
 
1
  # Changelog
2
 
3
+ ## [2.3.0-phase4] - Version 2.0.0 Phase 4 Release
4
+
5
+ ### Added
6
+ - **Production UI Separation (`templates/index.html`, `static/css/style.css`, `static/js/app.js`)**: Mobile-First responsive Mission Control dashboard supporting touch targets >= 44px across all viewports (320px–480px+).
7
+ - **Conversation Agent (`ConversationAgent`)**: Dedicated agent handling human chat interactions, mission explanations, failure reports, and approval prompts with persistent memory in SQLite.
8
+ - **Human-in-the-Loop Approval Workflow (`ApprovalRequestModel`)**: Safety checkpoint engine pausing missions for human verification on sensitive actions (`LOGIN_CONFIRMATION`, `OTP_ENTRY`, `SENSITIVE_FORM`, `FILE_UPLOAD`, `EXTERNAL_PUBLISHING`).
9
+ - **Central Notification Center (`NotificationEngine`)**: Real-time notification management system delivering persisted alerts (`INFO`, `WARNING`, `URGENT`, `ACTION_REQUIRED`) with WS live streaming.
10
+ - **Mission Interruption & Control APIs**: Dynamic mission modification and subtask insertion endpoints (`/api/v1/missions/{mission_id}/modify`).
11
+ - **Phase 4 REST API Suite**: Added `/api/v1/conversation/*`, `/api/v1/approvals/*`, `/api/v1/notifications/*`, and `/api/v1/missions/{mission_id}/modify`.
12
+
13
  ## [2.2.0-phase3] - Version 2.0.0 Phase 3 Release
14
 
15
  ### Added
app.py CHANGED
@@ -17,6 +17,7 @@ import uuid
17
 
18
  from fastapi import BackgroundTasks, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect, status
19
  from fastapi.responses import HTMLResponse
 
20
  from pydantic import BaseModel, Field
21
  import uvicorn
22
 
@@ -572,6 +573,74 @@ class AgentReflectionDetail(BaseModel):
572
  timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
573
 
574
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  # --- CONFIGURATION ENGINE ---
576
  class ConfigEngine:
577
  """Manages runtime system settings dynamically."""
@@ -902,6 +971,39 @@ class DatabaseManager:
902
  timestamp TEXT NOT NULL
903
  )
904
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
  # Safe schema migration for blackboard
906
  try:
907
  cursor.execute("ALTER TABLE blackboard ADD COLUMN version INTEGER DEFAULT 1")
@@ -973,6 +1075,8 @@ class DatabaseManager:
973
  conn.execute("DELETE FROM discussions WHERE mission_id=?", (mission_id,))
974
  conn.execute("DELETE FROM debates WHERE mission_id=?", (mission_id,))
975
  conn.execute("DELETE FROM agent_reflections_v2 WHERE mission_id=?", (mission_id,))
 
 
976
  conn.commit()
977
 
978
  await asyncio.to_thread(_exec)
@@ -1828,6 +1932,104 @@ class DatabaseManager:
1828
 
1829
  return await asyncio.to_thread(_exec)
1830
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1831
 
1832
  # --- WEBSOCKET CONNECTION MANAGER ---
1833
  class WebSocketManager:
@@ -2363,6 +2565,49 @@ class AgentReputationEngine:
2363
  return rep
2364
 
2365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2366
  # --- CONFIDENCE ENGINE & COST TRACKER ---
2367
  class ConfidenceEngine:
2368
  """Calculates objective confidence metrics based on evidence attributes."""
@@ -3284,7 +3529,7 @@ class PluginManager:
3284
 
3285
  # --- V2 COLONY RUNTIME KERNEL ---
3286
  class ColonyRuntime:
3287
- """V2 Colony Operating System Runtime Kernel managing dynamic agents, shared blackboard, resource telemetry, universal models, debates, consensus, and agent reputation."""
3288
 
3289
  def __init__(self, db: DatabaseManager, event_bus: EventBus, message_bus: MessageBus):
3290
  self.db = db
@@ -3300,9 +3545,18 @@ class ColonyRuntime:
3300
  self.debate_engine = DebateEngine(db, event_bus, self.model_manager)
3301
  self.negotiation_engine = TaskNegotiationEngine(db, event_bus)
3302
  self.reputation_engine = AgentReputationEngine(db)
 
 
3303
  self.dynamic_agents: Dict[str, DynamicWorkerAgent] = {}
3304
  self.active_contexts: Dict[str, MissionContextModel] = {}
3305
 
 
 
 
 
 
 
 
3306
  async def record_timeline_step(self, mission_id: str, agent_id: str, step_type: TimelineStepType, description: str, metadata: Optional[Dict[str, Any]] = None):
3307
  evt = TimelineEventModel(mission_id=mission_id, agent_id=agent_id, step_type=step_type, description=description, metadata=metadata or {})
3308
  await self.db.save_timeline_event(evt)
@@ -3393,6 +3647,7 @@ class ColonyOS:
3393
  self.vision = VisionAgent(self.db, self.message_bus, self.event_bus)
3394
  self.browser = BrowserAgent(self.db, self.message_bus, self.event_bus, self.browser_pool)
3395
  self.memory = MemoryAgent(self.db, self.message_bus, self.event_bus)
 
3396
 
3397
  self.agent_registry = {
3398
  self.commander.agent_id: self.commander,
@@ -3403,6 +3658,7 @@ class ColonyOS:
3403
  self.vision.agent_id: self.vision,
3404
  self.browser.agent_id: self.browser,
3405
  self.memory.agent_id: self.memory,
 
3406
  }
3407
 
3408
  async def awaken(self) -> None:
@@ -3443,6 +3699,12 @@ app = FastAPI(
3443
  lifespan=lifespan,
3444
  )
3445
 
 
 
 
 
 
 
3446
 
3447
  # --- WEBSOCKET REAL-TIME EVENT STREAM ---
3448
  @app.websocket("/api/v1/ws")
@@ -3753,7 +4015,10 @@ MISSION_CONTROL_HTML = """<!DOCTYPE html>
3753
  # --- HTML DASHBOARD ENDPOINT ---
3754
  @app.get("/", response_class=HTMLResponse)
3755
  async def get_mission_control_ui():
3756
- """Serve NASA Mission Control Operator Dashboard."""
 
 
 
3757
  return HTMLResponse(content=MISSION_CONTROL_HTML)
3758
 
3759
 
@@ -4464,5 +4729,82 @@ async def search_collective_memory(query: str = Query(..., min_length=1), tag: O
4464
  return await colony_os.db.search_memories(query, tag)
4465
 
4466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4467
  if __name__ == "__main__":
4468
  uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)
 
17
 
18
  from fastapi import BackgroundTasks, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect, status
19
  from fastapi.responses import HTMLResponse
20
+ from fastapi.staticfiles import StaticFiles
21
  from pydantic import BaseModel, Field
22
  import uvicorn
23
 
 
573
  timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
574
 
575
 
576
+ # --- V2 PHASE 4 DOMAIN MODELS ---
577
+ class ApprovalStatus(str, Enum):
578
+ PENDING = "PENDING"
579
+ APPROVED = "APPROVED"
580
+ REJECTED = "REJECTED"
581
+
582
+
583
+ class ActionType(str, Enum):
584
+ LOGIN_CONFIRMATION = "LOGIN_CONFIRMATION"
585
+ OTP_ENTRY = "OTP_ENTRY"
586
+ SENSITIVE_FORM = "SENSITIVE_FORM"
587
+ FILE_UPLOAD = "FILE_UPLOAD"
588
+ EXTERNAL_PUBLISHING = "EXTERNAL_PUBLISHING"
589
+
590
+
591
+ class ApprovalRequestModel(BaseModel):
592
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
593
+ mission_id: str
594
+ agent_id: str
595
+ action_type: ActionType
596
+ prompt_message: str
597
+ status: ApprovalStatus = ApprovalStatus.PENDING
598
+ input_data: Dict[str, Any] = Field(default_factory=dict)
599
+ created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
600
+
601
+
602
+ class NotificationLevel(str, Enum):
603
+ INFO = "INFO"
604
+ WARNING = "WARNING"
605
+ URGENT = "URGENT"
606
+ ACTION_REQUIRED = "ACTION_REQUIRED"
607
+
608
+
609
+ class NotificationModel(BaseModel):
610
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
611
+ level: NotificationLevel
612
+ title: str
613
+ message: str
614
+ mission_id: Optional[str] = None
615
+ acknowledged: bool = False
616
+ created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
617
+
618
+
619
+ class ConversationMessageModel(BaseModel):
620
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
621
+ user_id: str = "human-operator"
622
+ sender: str
623
+ message: str
624
+ metadata: Dict[str, Any] = Field(default_factory=dict)
625
+ timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
626
+
627
+
628
+ class ChatRequest(BaseModel):
629
+ message: str
630
+ user_id: Optional[str] = "human-operator"
631
+
632
+
633
+ class ResolveApprovalRequest(BaseModel):
634
+ approved: bool
635
+ input_data: Optional[Dict[str, Any]] = Field(default_factory=dict)
636
+
637
+
638
+ class ModifyMissionRequest(BaseModel):
639
+ new_topic: Optional[str] = None
640
+ insert_subtask: Optional[str] = None
641
+ priority: Optional[int] = None
642
+
643
+
644
  # --- CONFIGURATION ENGINE ---
645
  class ConfigEngine:
646
  """Manages runtime system settings dynamically."""
 
971
  timestamp TEXT NOT NULL
972
  )
973
  """)
974
+ cursor.execute("""
975
+ CREATE TABLE IF NOT EXISTS approval_requests (
976
+ id TEXT PRIMARY KEY,
977
+ mission_id TEXT NOT NULL,
978
+ agent_id TEXT NOT NULL,
979
+ action_type TEXT NOT NULL,
980
+ prompt_message TEXT NOT NULL,
981
+ status TEXT NOT NULL,
982
+ input_data_json TEXT,
983
+ created_at TEXT NOT NULL
984
+ )
985
+ """)
986
+ cursor.execute("""
987
+ CREATE TABLE IF NOT EXISTS notifications (
988
+ id TEXT PRIMARY KEY,
989
+ level TEXT NOT NULL,
990
+ title TEXT NOT NULL,
991
+ message TEXT NOT NULL,
992
+ mission_id TEXT,
993
+ acknowledged INTEGER DEFAULT 0,
994
+ created_at TEXT NOT NULL
995
+ )
996
+ """)
997
+ cursor.execute("""
998
+ CREATE TABLE IF NOT EXISTS conversation_logs (
999
+ id TEXT PRIMARY KEY,
1000
+ user_id TEXT NOT NULL,
1001
+ sender TEXT NOT NULL,
1002
+ message TEXT NOT NULL,
1003
+ metadata_json TEXT,
1004
+ timestamp TEXT NOT NULL
1005
+ )
1006
+ """)
1007
  # Safe schema migration for blackboard
1008
  try:
1009
  cursor.execute("ALTER TABLE blackboard ADD COLUMN version INTEGER DEFAULT 1")
 
1075
  conn.execute("DELETE FROM discussions WHERE mission_id=?", (mission_id,))
1076
  conn.execute("DELETE FROM debates WHERE mission_id=?", (mission_id,))
1077
  conn.execute("DELETE FROM agent_reflections_v2 WHERE mission_id=?", (mission_id,))
1078
+ conn.execute("DELETE FROM approval_requests WHERE mission_id=?", (mission_id,))
1079
+ conn.execute("DELETE FROM notifications WHERE mission_id=?", (mission_id,))
1080
  conn.commit()
1081
 
1082
  await asyncio.to_thread(_exec)
 
1932
 
1933
  return await asyncio.to_thread(_exec)
1934
 
1935
+ async def save_approval_request(self, req: ApprovalRequestModel) -> None:
1936
+ def _exec():
1937
+ with self._get_connection() as conn:
1938
+ conn.execute(
1939
+ """
1940
+ INSERT INTO approval_requests (id, mission_id, agent_id, action_type, prompt_message, status, input_data_json, created_at)
1941
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1942
+ ON CONFLICT(id) DO UPDATE SET status=excluded.status, input_data_json=excluded.input_data_json
1943
+ """,
1944
+ (req.id, req.mission_id, req.agent_id, req.action_type.value, req.prompt_message, req.status.value, json.dumps(req.input_data), req.created_at),
1945
+ )
1946
+ conn.commit()
1947
+
1948
+ await asyncio.to_thread(_exec)
1949
+
1950
+ async def get_approval_request(self, approval_id: str) -> Optional[Dict[str, Any]]:
1951
+ def _exec():
1952
+ with self._get_connection() as conn:
1953
+ cursor = conn.cursor()
1954
+ cursor.execute("SELECT * FROM approval_requests WHERE id = ?", (approval_id,))
1955
+ row = cursor.fetchone()
1956
+ if not row:
1957
+ return None
1958
+ item = dict(row)
1959
+ item["input_data"] = json.loads(item["input_data_json"]) if item.get("input_data_json") else {}
1960
+ return item
1961
+
1962
+ return await asyncio.to_thread(_exec)
1963
+
1964
+ async def get_pending_approvals(self) -> List[Dict[str, Any]]:
1965
+ def _exec():
1966
+ with self._get_connection() as conn:
1967
+ cursor = conn.cursor()
1968
+ cursor.execute("SELECT * FROM approval_requests WHERE status = 'PENDING' ORDER BY created_at ASC")
1969
+ rows = cursor.fetchall()
1970
+ results = []
1971
+ for r in rows:
1972
+ item = dict(r)
1973
+ item["input_data"] = json.loads(item["input_data_json"]) if item.get("input_data_json") else {}
1974
+ results.append(item)
1975
+ return results
1976
+
1977
+ return await asyncio.to_thread(_exec)
1978
+
1979
+ async def save_notification(self, notif: NotificationModel) -> None:
1980
+ def _exec():
1981
+ with self._get_connection() as conn:
1982
+ conn.execute(
1983
+ "INSERT INTO notifications (id, level, title, message, mission_id, acknowledged, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
1984
+ (notif.id, notif.level.value, notif.title, notif.message, notif.mission_id, 1 if notif.acknowledged else 0, notif.created_at),
1985
+ )
1986
+ conn.commit()
1987
+
1988
+ await asyncio.to_thread(_exec)
1989
+
1990
+ async def get_unacknowledged_notifications(self) -> List[Dict[str, Any]]:
1991
+ def _exec():
1992
+ with self._get_connection() as conn:
1993
+ cursor = conn.cursor()
1994
+ cursor.execute("SELECT * FROM notifications WHERE acknowledged = 0 ORDER BY created_at DESC")
1995
+ return [dict(r) for r in cursor.fetchall()]
1996
+
1997
+ return await asyncio.to_thread(_exec)
1998
+
1999
+ async def acknowledge_notification(self, notif_id: str) -> None:
2000
+ def _exec():
2001
+ with self._get_connection() as conn:
2002
+ conn.execute("UPDATE notifications SET acknowledged = 1 WHERE id = ?", (notif_id,))
2003
+ conn.commit()
2004
+
2005
+ await asyncio.to_thread(_exec)
2006
+
2007
+ async def save_conversation_log(self, msg: ConversationMessageModel) -> None:
2008
+ def _exec():
2009
+ with self._get_connection() as conn:
2010
+ conn.execute(
2011
+ "INSERT INTO conversation_logs (id, user_id, sender, message, metadata_json, timestamp) VALUES (?, ?, ?, ?, ?, ?)",
2012
+ (msg.id, msg.user_id, msg.sender, msg.message, json.dumps(msg.metadata), msg.timestamp),
2013
+ )
2014
+ conn.commit()
2015
+
2016
+ await asyncio.to_thread(_exec)
2017
+
2018
+ async def get_conversation_history(self, user_id: str = "human-operator", limit: int = 50) -> List[Dict[str, Any]]:
2019
+ def _exec():
2020
+ with self._get_connection() as conn:
2021
+ cursor = conn.cursor()
2022
+ cursor.execute("SELECT * FROM conversation_logs WHERE user_id = ? ORDER BY timestamp ASC LIMIT ?", (user_id, limit))
2023
+ rows = cursor.fetchall()
2024
+ results = []
2025
+ for r in rows:
2026
+ item = dict(r)
2027
+ item["metadata"] = json.loads(item["metadata_json"]) if item.get("metadata_json") else {}
2028
+ results.append(item)
2029
+ return results
2030
+
2031
+ return await asyncio.to_thread(_exec)
2032
+
2033
 
2034
  # --- WEBSOCKET CONNECTION MANAGER ---
2035
  class WebSocketManager:
 
2565
  return rep
2566
 
2567
 
2568
+ # --- V2 PHASE 4 CONVERSATION & NOTIFICATION ENGINES ---
2569
+ class NotificationEngine:
2570
+ """Central notification center managing persisted system alerts and WS events."""
2571
+
2572
+ def __init__(self, db: DatabaseManager, event_bus: EventBus):
2573
+ self.db = db
2574
+ self.event_bus = event_bus
2575
+
2576
+ async def notify(self, level: NotificationLevel, title: str, message: str, mission_id: Optional[str] = None) -> NotificationModel:
2577
+ notif = NotificationModel(level=level, title=title, message=message, mission_id=mission_id)
2578
+ await self.db.save_notification(notif)
2579
+ await self.event_bus.emit("NotificationCreated", mission_id or "system", "NotificationEngine", notif.model_dump())
2580
+ return notif
2581
+
2582
+
2583
+ class ConversationAgent(BaseAgent):
2584
+ """Dedicated Conversation Agent for human interaction, clarification, failure reporting, and approvals."""
2585
+
2586
+ def __init__(self, db: DatabaseManager, message_bus: MessageBus, event_bus: EventBus, model_manager: ModelManager):
2587
+ super().__init__("agent-conversation-01", "Mnemosyne Chat", "Human Interface & Conversation Specialist", db, message_bus, event_bus)
2588
+ self.model_manager = model_manager
2589
+
2590
+ async def process_user_message(self, user_message: str, user_id: str = "human-operator") -> str:
2591
+ # Record user message in conversation history
2592
+ user_log = ConversationMessageModel(user_id=user_id, sender="Human Operator", message=user_message)
2593
+ await self.db.save_conversation_log(user_log)
2594
+
2595
+ # Context lookup from memory vault
2596
+ memories = await self.db.search_memories(user_message)
2597
+ ctx_str = "\n".join([m["content"] for m in memories[:3]]) if memories else "No direct memory match."
2598
+
2599
+ prompt = f"User said: '{user_message}'\nRelevant Memory Context:\n{ctx_str}\nProvide a helpful, polite, and strategic response as Spark Colony OS Operator Assistant."
2600
+ resp = await self.model_manager.generate_response(LogicalModel.MDL_FST, prompt)
2601
+ reply_text = resp["content"]
2602
+
2603
+ # Record agent reply
2604
+ agent_log = ConversationMessageModel(user_id=user_id, sender=self.name, message=reply_text)
2605
+ await self.db.save_conversation_log(agent_log)
2606
+ await self.record_memory("system", f"Human Chat Interaction: {user_message} -> {reply_text}", ["conversation", "human_interaction"])
2607
+
2608
+ return reply_text
2609
+
2610
+
2611
  # --- CONFIDENCE ENGINE & COST TRACKER ---
2612
  class ConfidenceEngine:
2613
  """Calculates objective confidence metrics based on evidence attributes."""
 
3529
 
3530
  # --- V2 COLONY RUNTIME KERNEL ---
3531
  class ColonyRuntime:
3532
+ """V2 Colony Operating System Runtime Kernel managing dynamic agents, shared blackboard, resource telemetry, universal models, debates, consensus, human approvals, and notifications."""
3533
 
3534
  def __init__(self, db: DatabaseManager, event_bus: EventBus, message_bus: MessageBus):
3535
  self.db = db
 
3545
  self.debate_engine = DebateEngine(db, event_bus, self.model_manager)
3546
  self.negotiation_engine = TaskNegotiationEngine(db, event_bus)
3547
  self.reputation_engine = AgentReputationEngine(db)
3548
+ self.notification_engine = NotificationEngine(db, event_bus)
3549
+ self.conversation_agent = ConversationAgent(db, message_bus, event_bus, self.model_manager)
3550
  self.dynamic_agents: Dict[str, DynamicWorkerAgent] = {}
3551
  self.active_contexts: Dict[str, MissionContextModel] = {}
3552
 
3553
+ async def request_human_approval(self, mission_id: str, agent_id: str, action_type: ActionType, prompt_message: str) -> ApprovalRequestModel:
3554
+ req = ApprovalRequestModel(mission_id=mission_id, agent_id=agent_id, action_type=action_type, prompt_message=prompt_message)
3555
+ await self.db.save_approval_request(req)
3556
+ await self.db.update_mission_status(mission_id, MissionStatus.PAUSED)
3557
+ await self.notification_engine.notify(NotificationLevel.ACTION_REQUIRED, f"Approval Required: {action_type.value}", prompt_message, mission_id)
3558
+ return req
3559
+
3560
  async def record_timeline_step(self, mission_id: str, agent_id: str, step_type: TimelineStepType, description: str, metadata: Optional[Dict[str, Any]] = None):
3561
  evt = TimelineEventModel(mission_id=mission_id, agent_id=agent_id, step_type=step_type, description=description, metadata=metadata or {})
3562
  await self.db.save_timeline_event(evt)
 
3647
  self.vision = VisionAgent(self.db, self.message_bus, self.event_bus)
3648
  self.browser = BrowserAgent(self.db, self.message_bus, self.event_bus, self.browser_pool)
3649
  self.memory = MemoryAgent(self.db, self.message_bus, self.event_bus)
3650
+ self.conversation_agent = self.runtime.conversation_agent
3651
 
3652
  self.agent_registry = {
3653
  self.commander.agent_id: self.commander,
 
3658
  self.vision.agent_id: self.vision,
3659
  self.browser.agent_id: self.browser,
3660
  self.memory.agent_id: self.memory,
3661
+ self.conversation_agent.agent_id: self.conversation_agent,
3662
  }
3663
 
3664
  async def awaken(self) -> None:
 
3699
  lifespan=lifespan,
3700
  )
3701
 
3702
+ # Mount static files directory
3703
+ os.makedirs("static/css", exist_ok=True)
3704
+ os.makedirs("static/js", exist_ok=True)
3705
+ os.makedirs("templates", exist_ok=True)
3706
+ app.mount("/static", StaticFiles(directory="static"), name="static")
3707
+
3708
 
3709
  # --- WEBSOCKET REAL-TIME EVENT STREAM ---
3710
  @app.websocket("/api/v1/ws")
 
4015
  # --- HTML DASHBOARD ENDPOINT ---
4016
  @app.get("/", response_class=HTMLResponse)
4017
  async def get_mission_control_ui():
4018
+ """Serve NASA Mission Control Operator Dashboard Template."""
4019
+ if os.path.exists("templates/index.html"):
4020
+ with open("templates/index.html", "r", encoding="utf-8") as f:
4021
+ return HTMLResponse(content=f.read())
4022
  return HTMLResponse(content=MISSION_CONTROL_HTML)
4023
 
4024
 
 
4729
  return await colony_os.db.search_memories(query, tag)
4730
 
4731
 
4732
+ # --- V2 PHASE 4 CONVERSATION & HUMAN-IN-THE-LOOP APIs ---
4733
+ @app.post("/api/v1/conversation/chat")
4734
+ async def chat_with_conversation_agent(req: ChatRequest):
4735
+ """Chat directly with the Colony Conversation Agent (Mnemosyne Chat)."""
4736
+ reply = await colony_os.runtime.conversation_agent.process_user_message(req.message, req.user_id or "human-operator")
4737
+ return {"response": reply, "agent": colony_os.runtime.conversation_agent.name}
4738
+
4739
+
4740
+ @app.get("/api/v1/conversation/history")
4741
+ async def get_conversation_chat_history(user_id: str = "human-operator", limit: int = 50):
4742
+ """Retrieve persistent conversation history for human operator."""
4743
+ return await colony_os.db.get_conversation_history(user_id, limit)
4744
+
4745
+
4746
+ @app.get("/api/v1/approvals/pending")
4747
+ async def get_pending_human_approvals():
4748
+ """List pending human-in-the-loop approval requests."""
4749
+ return await colony_os.db.get_pending_approvals()
4750
+
4751
+
4752
+ @app.post("/api/v1/approvals/{approval_id}/resolve")
4753
+ async def resolve_human_approval(approval_id: str, req: ResolveApprovalRequest):
4754
+ """Approve or reject a pending human approval request and resume mission execution."""
4755
+ approval = await colony_os.db.get_approval_request(approval_id)
4756
+ if not approval:
4757
+ raise HTTPException(status_code=404, detail="Approval request not found.")
4758
+
4759
+ status_val = ApprovalStatus.APPROVED if req.approved else ApprovalStatus.REJECTED
4760
+ approval_obj = ApprovalRequestModel(
4761
+ id=approval["id"],
4762
+ mission_id=approval["mission_id"],
4763
+ agent_id=approval["agent_id"],
4764
+ action_type=ActionType(approval["action_type"]),
4765
+ prompt_message=approval["prompt_message"],
4766
+ status=status_val,
4767
+ input_data=req.input_data or {},
4768
+ )
4769
+ await colony_os.db.save_approval_request(approval_obj)
4770
+
4771
+ if req.approved:
4772
+ await colony_os.db.update_mission_status(approval["mission_id"], MissionStatus.IN_PROGRESS)
4773
+ await colony_os.event_bus.emit("MissionResumed", approval["mission_id"], "HumanOperator", {"approval_id": approval_id})
4774
+
4775
+ return {"status": status_val.value, "approval_id": approval_id, "mission_id": approval["mission_id"]}
4776
+
4777
+
4778
+ # --- V2 PHASE 4 NOTIFICATION APIs ---
4779
+ @app.get("/api/v1/notifications")
4780
+ async def get_unacknowledged_notifications():
4781
+ """Fetch all unacknowledged system notifications."""
4782
+ return await colony_os.db.get_unacknowledged_notifications()
4783
+
4784
+
4785
+ @app.post("/api/v1/notifications/{notification_id}/acknowledge")
4786
+ async def acknowledge_notification(notification_id: str):
4787
+ """Acknowledge a system notification."""
4788
+ await colony_os.db.acknowledge_notification(notification_id)
4789
+ return {"status": "ACKNOWLEDGED", "notification_id": notification_id}
4790
+
4791
+
4792
+ # --- V2 PHASE 4 MISSION INTERRUPTION & CONTROL APIs ---
4793
+ @app.post("/api/v1/missions/{mission_id}/modify")
4794
+ async def modify_running_mission(mission_id: str, req: ModifyMissionRequest):
4795
+ """Modify parameters of a running mission (topic, priority, subtask insertion)."""
4796
+ mission = await colony_os.db.get_mission(mission_id)
4797
+ if not mission:
4798
+ raise HTTPException(status_code=404, detail="Mission not found.")
4799
+
4800
+ if req.new_topic:
4801
+ await colony_os.db.save_mission(mission_id, req.new_topic, MissionStatus(mission["status"]), DecisionStage(mission["stage"]))
4802
+
4803
+ if req.insert_subtask:
4804
+ await colony_os.db.save_journal("Commander Prime", mission_id, f"[INTERRUPTION INSERT TASK]: {req.insert_subtask}")
4805
+
4806
+ return {"status": "MODIFIED", "mission_id": mission_id}
4807
+
4808
+
4809
  if __name__ == "__main__":
4810
  uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)
static/css/style.css ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-base: #06080d;
3
+ --bg-card: rgba(13, 18, 31, 0.85);
4
+ --border-card: rgba(0, 240, 255, 0.2);
5
+ --accent-cyan: #00f0ff;
6
+ --accent-green: #00ff88;
7
+ --accent-purple: #9d00ff;
8
+ --accent-yellow: #ffb700;
9
+ --accent-red: #ff0055;
10
+ --text-main: #e2e8f0;
11
+ --text-muted: #64748b;
12
+ }
13
+
14
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
15
+ body { background: var(--bg-base); color: var(--text-main); display: flex; flex-direction: column; min-height: 100vh; overflow-x: hidden; }
16
+
17
+ .app-header {
18
+ background: rgba(10, 14, 23, 0.95);
19
+ border-bottom: 1px solid var(--border-card);
20
+ padding: 12px 16px;
21
+ display: flex; justify-content: space-between; align-items: center;
22
+ }
23
+ .brand-title { font-weight: 800; font-size: 1rem; color: var(--accent-cyan); letter-spacing: 1px; }
24
+ .brand-badge { background: rgba(0, 240, 255, 0.1); border: 1px solid var(--accent-cyan); font-size: 0.65rem; padding: 2px 6px; border-radius: 4px; margin-left: 6px; }
25
+
26
+ .header-status { display: flex; align-items: center; gap: 6px; font-size: 0.75rem; color: var(--accent-green); }
27
+ .status-indicator { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-green); box-shadow: 0 0 8px var(--accent-green); }
28
+
29
+ .mobile-nav {
30
+ display: flex; background: rgba(13, 18, 31, 0.95);
31
+ border-bottom: 1px solid var(--border-card); overflow-x: auto;
32
+ }
33
+ .nav-btn {
34
+ flex: 1; min-width: 70px; padding: 12px 8px; background: transparent; border: none;
35
+ color: var(--text-muted); font-size: 0.75rem; font-weight: 600; cursor: pointer; min-height: 44px;
36
+ }
37
+ .nav-btn.active { color: var(--accent-cyan); border-bottom: 2px solid var(--accent-cyan); }
38
+
39
+ .content-container { flex: 1; padding: 12px; display: flex; flex-direction: column; gap: 12px; }
40
+ .tab-content { display: none; flex-direction: column; gap: 12px; }
41
+ .tab-content.active { display: flex; }
42
+
43
+ .telemetry-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
44
+ @media (min-width: 600px) { .telemetry-grid { grid-template-columns: repeat(4, 1fr); } }
45
+
46
+ .tele-card { background: var(--bg-card); border: 1px solid var(--border-card); padding: 10px; border-radius: 6px; display: flex; flex-direction: column; }
47
+ .tele-label { font-size: 0.65rem; color: var(--text-muted); }
48
+ .tele-value { font-size: 0.9rem; font-weight: bold; color: var(--accent-cyan); margin-top: 4px; }
49
+
50
+ .section-panel { background: var(--bg-card); border: 1px solid var(--border-card); border-radius: 6px; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
51
+ .panel-header { font-size: 0.8rem; font-weight: bold; color: var(--accent-cyan); letter-spacing: 0.5px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 6px; }
52
+
53
+ .primary-btn { background: var(--accent-cyan); color: #000; font-weight: bold; border: none; padding: 12px 16px; border-radius: 6px; cursor: pointer; min-height: 44px; }
54
+ .secondary-btn { background: transparent; border: 1px solid var(--text-muted); color: #fff; padding: 12px 16px; border-radius: 6px; cursor: pointer; min-height: 44px; }
55
+
56
+ .form-group { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
57
+ .form-group label { font-size: 0.75rem; color: var(--text-muted); }
58
+ .form-group input, .form-group select { background: rgba(0,0,0,0.5); border: 1px solid var(--border-card); color: #fff; padding: 10px; border-radius: 4px; font-size: 0.85rem; min-height: 44px; }
59
+
60
+ .chat-container { display: flex; flex-direction: column; height: 60vh; background: var(--bg-card); border: 1px solid var(--border-card); border-radius: 6px; padding: 10px; }
61
+ .chat-messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding-right: 4px; }
62
+ .chat-msg { padding: 8px 12px; border-radius: 6px; font-size: 0.8rem; max-width: 85%; }
63
+ .chat-msg.agent { background: rgba(0, 240, 255, 0.1); border-left: 3px solid var(--accent-cyan); align-self: flex-start; }
64
+ .chat-msg.user { background: rgba(0, 255, 136, 0.1); border-right: 3px solid var(--accent-green); align-self: flex-end; }
65
+ .chat-input-row { display: flex; gap: 6px; margin-top: 8px; }
66
+
67
+ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: none; justify-content: center; align-items: center; z-index: 1000; padding: 16px; }
68
+ .modal-content { background: #0b101d; border: 1px solid var(--accent-cyan); border-radius: 8px; width: 100%; max-width: 440px; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
69
+ .modal-actions { display: flex; justify-content: flex-end; gap: 8px; }
70
+
71
+ .empty-state { font-size: 0.75rem; color: var(--text-muted); text-align: center; padding: 12px; }
72
+ .agent-cards-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
73
+ @media (min-width: 600px) { .agent-cards-grid { grid-template-columns: repeat(2, 1fr); } }
static/js/app.js ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let currentTab = 'dashboard';
2
+ let activeApprovalId = null;
3
+ const ws = new WebSocket(`ws://${location.host}/api/v1/ws`);
4
+
5
+ ws.onmessage = (event) => {
6
+ const data = JSON.parse(event.data);
7
+ if (data.event_type === "NotificationCreated" || data.event_type === "ApprovalRequired") {
8
+ fetchNotifications();
9
+ }
10
+ fetchStatus();
11
+ };
12
+
13
+ function switchTab(tabId) {
14
+ currentTab = tabId;
15
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
16
+ document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
17
+ document.getElementById(`tab-${tabId}`).classList.add('active');
18
+ event.target.classList.add('active');
19
+
20
+ if (tabId === 'browser') fetchBrowserStream();
21
+ if (tabId === 'agents') fetchAgents();
22
+ }
23
+
24
+ async function fetchStatus() {
25
+ try {
26
+ const res = await fetch('/api/v1/system/status');
27
+ const data = await res.json();
28
+ document.getElementById('teleState').innerText = data.app_state;
29
+ document.getElementById('teleHardware').innerText = `${data.cpu_usage_percent}% / ${data.memory_usage_percent}%`;
30
+ document.getElementById('teleMissions').innerText = data.total_missions;
31
+ document.getElementById('teleCost').innerText = `$${data.total_cost_usd.toFixed(4)}`;
32
+ } catch (e) {
33
+ console.error("Failed fetching status", e);
34
+ }
35
+ }
36
+
37
+ async function fetchNotifications() {
38
+ try {
39
+ const res = await fetch('/api/v1/notifications');
40
+ const items = await res.json();
41
+ const container = document.getElementById('notificationList');
42
+ if (!items || items.length === 0) {
43
+ container.innerHTML = '<div class="empty-state">No pending notifications.</div>';
44
+ return;
45
+ }
46
+ container.innerHTML = items.map(n => `
47
+ <div style="padding:8px; background:rgba(0,0,0,0.3); border-radius:4px; border-left:3px solid var(--accent-cyan); margin-bottom:6px; font-size:0.75rem;">
48
+ <strong>[${n.level}] ${n.title}</strong>: ${n.message}
49
+ ${n.level === 'ACTION_REQUIRED' ? `<button class="primary-btn" style="padding:4px 8px; min-height:28px; font-size:0.65rem; margin-top:4px;" onclick="openApprovalModal('${n.id}', '${n.message}')">Review Action</button>` : ''}
50
+ </div>
51
+ `).join('');
52
+ } catch (e) {
53
+ console.error("Failed fetching notifications", e);
54
+ }
55
+ }
56
+
57
+ async function fetchAgents() {
58
+ try {
59
+ const res = await fetch('/api/v1/agents');
60
+ const agents = await res.json();
61
+ const container = document.getElementById('agentCardsContainer');
62
+ container.innerHTML = agents.map(a => `
63
+ <div style="padding:10px; background:rgba(0,0,0,0.4); border:1px solid var(--border-card); border-radius:6px; font-size:0.75rem;">
64
+ <div style="display:flex; justify-content:space-between; font-weight:bold; color:var(--accent-cyan);">
65
+ <span>${a.name}</span>
66
+ <span>${a.state}</span>
67
+ </div>
68
+ <div style="color:var(--text-muted); margin-top:4px;">Role: ${a.role}</div>
69
+ <div style="margin-top:2px;">Task: ${a.current_task || 'Idle'}</div>
70
+ </div>
71
+ `).join('');
72
+ } catch (e) {
73
+ console.error("Failed fetching agents", e);
74
+ }
75
+ }
76
+
77
+ async function fetchBrowserStream() {
78
+ try {
79
+ const res = await fetch('/api/v1/browser/thumbnails');
80
+ const data = await res.json();
81
+ const img = document.getElementById('browserStreamImg');
82
+ const placeholder = document.getElementById('browserStreamPlaceholder');
83
+ if (data && data.length > 0 && data[0].has_screenshot) {
84
+ img.src = `data:image/png;base64,${data[0].has_screenshot}`;
85
+ img.style.display = 'block';
86
+ placeholder.style.display = 'none';
87
+ } else {
88
+ img.style.display = 'none';
89
+ placeholder.style.display = 'block';
90
+ }
91
+ } catch (e) {
92
+ console.error("Failed fetching browser stream", e);
93
+ }
94
+ }
95
+
96
+ async function sendChatMessage() {
97
+ const input = document.getElementById('chatInput');
98
+ const msg = input.value.trim();
99
+ if (!msg) return;
100
+
101
+ const chatBox = document.getElementById('chatMessages');
102
+ chatBox.innerHTML += `<div class="chat-msg user"><strong>You:</strong> ${msg}</div>`;
103
+ input.value = '';
104
+ chatBox.scrollTop = chatBox.scrollHeight;
105
+
106
+ try {
107
+ const res = await fetch('/api/v1/conversation/chat', {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/json' },
110
+ body: JSON.stringify({ message: msg })
111
+ });
112
+ const reply = await res.json();
113
+ chatBox.innerHTML += `<div class="chat-msg agent"><strong>Conversation Agent:</strong> ${reply.response}</div>`;
114
+ chatBox.scrollTop = chatBox.scrollHeight;
115
+ } catch (e) {
116
+ console.error("Failed sending chat message", e);
117
+ }
118
+ }
119
+
120
+ function openDirectiveModal() { document.getElementById('directiveModal').style.display = 'flex'; }
121
+ function closeDirectiveModal() { document.getElementById('directiveModal').style.display = 'none'; }
122
+
123
+ async function submitDirective() {
124
+ const topic = document.getElementById('modalTopic').value;
125
+ const mode = document.getElementById('modalMode').value;
126
+ if (!topic) return;
127
+
128
+ closeDirectiveModal();
129
+ await fetch('/api/v1/missions', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify({ topic, thinking_mode: mode })
133
+ });
134
+ fetchStatus();
135
+ }
136
+
137
+ function openApprovalModal(id, msg) {
138
+ activeApprovalId = id;
139
+ document.getElementById('approvalPromptMsg').innerText = msg;
140
+ if (msg.toLowerCase().includes('otp') || msg.toLowerCase().includes('code')) {
141
+ document.getElementById('otpInputGroup').style.display = 'flex';
142
+ } else {
143
+ document.getElementById('otpInputGroup').style.display = 'none';
144
+ }
145
+ document.getElementById('approvalModal').style.display = 'flex';
146
+ }
147
+
148
+ async function resolveApproval(approved) {
149
+ if (!activeApprovalId) return;
150
+ const otp = document.getElementById('approvalOtpCode').value;
151
+ await fetch(`/api/v1/approvals/${activeApprovalId}/resolve`, {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ approved, input_data: otp ? { otp } : {} })
155
+ });
156
+ document.getElementById('approvalModal').style.display = 'none';
157
+ activeApprovalId = null;
158
+ fetchNotifications();
159
+ }
160
+
161
+ async function saveSettings() {
162
+ const mdlFst = document.getElementById('cfgMdlFst').value;
163
+ const mdlAdv = document.getElementById('cfgMdlAdv').value;
164
+ await fetch('/api/v1/config', {
165
+ method: 'POST',
166
+ headers: { 'Content-Type': 'application/json' },
167
+ body: JSON.stringify({ max_cost_per_mission: 2.0 })
168
+ });
169
+ alert("Configuration saved successfully.");
170
+ }
171
+
172
+ setInterval(fetchStatus, 3000);
173
+ fetchStatus();
174
+ fetchNotifications();
templates/index.html ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>SPARK COLONY OS // MISSION CONTROL</title>
7
+ <link rel="stylesheet" href="/static/css/style.css">
8
+ </head>
9
+ <body>
10
+ <header class="app-header">
11
+ <div class="brand">
12
+ <span class="brand-title">SPARK COLONY OS</span>
13
+ <span class="brand-badge">V2.4 MISSION CONTROL</span>
14
+ </div>
15
+ <div class="header-status">
16
+ <div class="status-indicator" id="wsIndicator"></div>
17
+ <span id="wsStatusText">WS ONLINE</span>
18
+ </div>
19
+ </header>
20
+
21
+ <nav class="mobile-nav">
22
+ <button class="nav-btn active" onclick="switchTab('dashboard')">Dashboard</button>
23
+ <button class="nav-btn" onclick="switchTab('conversation')">Chat</button>
24
+ <button class="nav-btn" onclick="switchTab('agents')">Agents</button>
25
+ <button class="nav-btn" onclick="switchTab('browser')">Browser</button>
26
+ <button class="nav-btn" onclick="switchTab('settings')">Settings</button>
27
+ </nav>
28
+
29
+ <main class="content-container">
30
+ <!-- DASHBOARD TAB -->
31
+ <section id="tab-dashboard" class="tab-content active">
32
+ <div class="telemetry-grid">
33
+ <div class="tele-card">
34
+ <span class="tele-label">SYSTEM STATE</span>
35
+ <span class="tele-value" id="teleState">READY</span>
36
+ </div>
37
+ <div class="tele-card">
38
+ <span class="tele-label">CPU / RAM</span>
39
+ <span class="tele-value" id="teleHardware">0% / 0%</span>
40
+ </div>
41
+ <div class="tele-card">
42
+ <span class="tele-label">ACTIVE MISSIONS</span>
43
+ <span class="tele-value" id="teleMissions">0</span>
44
+ </div>
45
+ <div class="tele-card">
46
+ <span class="tele-label">API COST</span>
47
+ <span class="tele-value" id="teleCost">$0.00</span>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="action-bar">
52
+ <button class="primary-btn" onclick="openDirectiveModal()">+ Dispatch New Directive</button>
53
+ </div>
54
+
55
+ <div class="section-panel">
56
+ <div class="panel-header">ACTIVE NOTIFICATIONS & APPROVALS</div>
57
+ <div id="notificationList" class="notification-list">
58
+ <div class="empty-state">No pending notifications.</div>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="section-panel">
63
+ <div class="panel-header">COLONY MISSION TIMELINE</div>
64
+ <div id="missionTimelineList" class="timeline-list">
65
+ <div class="empty-state">No active mission timeline.</div>
66
+ </div>
67
+ </div>
68
+ </section>
69
+
70
+ <!-- CONVERSATION AGENT TAB -->
71
+ <section id="tab-conversation" class="tab-content">
72
+ <div class="chat-container">
73
+ <div class="chat-header">
74
+ <span>CONVERSATION AGENT (Mnemosyne Chat)</span>
75
+ </div>
76
+ <div id="chatMessages" class="chat-messages">
77
+ <div class="chat-msg agent">
78
+ <strong>Conversation Agent:</strong> Hello! I am the Colony Conversation Agent. How can I assist you with running or reviewing missions?
79
+ </div>
80
+ </div>
81
+ <div class="chat-input-row">
82
+ <input type="text" id="chatInput" placeholder="Type instructions or questions..." onkeydown="if(event.key==='Enter') sendChatMessage()">
83
+ <button class="primary-btn" onclick="sendChatMessage()">Send</button>
84
+ </div>
85
+ </div>
86
+ </section>
87
+
88
+ <!-- AGENTS MONITOR TAB -->
89
+ <section id="tab-agents" class="tab-content">
90
+ <div class="section-panel">
91
+ <div class="panel-header">ACTIVE AGENT MATRIX</div>
92
+ <div id="agentCardsContainer" class="agent-cards-grid"></div>
93
+ </div>
94
+ </section>
95
+
96
+ <!-- BROWSER LIVE VIEW TAB -->
97
+ <section id="tab-browser" class="tab-content">
98
+ <div class="section-panel">
99
+ <div class="panel-header">LIVE BROWSER VIEWPORT & STREAM</div>
100
+ <div class="browser-viewport">
101
+ <img id="browserStreamImg" src="" alt="Browser Live Viewport" style="display:none; width:100%; border-radius:4px;">
102
+ <div id="browserStreamPlaceholder" class="empty-state">No active browser screenshot stream available.</div>
103
+ </div>
104
+ </div>
105
+ </section>
106
+
107
+ <!-- SETTINGS TAB -->
108
+ <section id="tab-settings" class="tab-content">
109
+ <div class="section-panel">
110
+ <div class="panel-header">COLONY RUNTIME CONFIGURATION</div>
111
+ <div class="form-group">
112
+ <label>Fast Logical Model (mdl_fst)</label>
113
+ <input type="text" id="cfgMdlFst" value="gemini-2.5-flash">
114
+ </div>
115
+ <div class="form-group">
116
+ <label>Advanced Logical Model (mdl_adv)</label>
117
+ <input type="text" id="cfgMdlAdv" value="llama-3.3-70b-versatile">
118
+ </div>
119
+ <div class="form-group">
120
+ <label>Screenshot Frequency (seconds)</label>
121
+ <input type="number" id="cfgScreenshotFreq" value="2">
122
+ </div>
123
+ <button class="primary-btn" onclick="saveSettings()">Save Configuration</button>
124
+ </div>
125
+ </section>
126
+ </main>
127
+
128
+ <!-- DIRECTIVE MODAL -->
129
+ <div id="directiveModal" class="modal-overlay">
130
+ <div class="modal-content">
131
+ <div class="modal-header">DISPATCH RESEARCH DIRECTIVE</div>
132
+ <div class="form-group">
133
+ <label>Mission Directive / Topic</label>
134
+ <input type="text" id="modalTopic" placeholder="e.g. Autonomous AI Colony Architectures">
135
+ </div>
136
+ <div class="form-group">
137
+ <label>Thinking Mode</label>
138
+ <select id="modalMode">
139
+ <option value="Research">Research (Deep Search)</option>
140
+ <option value="Analytical">Analytical</option>
141
+ <option value="Fast">Fast</option>
142
+ <option value="Critical">Critical</option>
143
+ <option value="Scientific">Scientific</option>
144
+ </select>
145
+ </div>
146
+ <div class="modal-actions">
147
+ <button class="secondary-btn" onclick="closeDirectiveModal()">Cancel</button>
148
+ <button class="primary-btn" onclick="submitDirective()">Dispatch</button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- APPROVAL MODAL -->
154
+ <div id="approvalModal" class="modal-overlay">
155
+ <div class="modal-content">
156
+ <div class="modal-header">HUMAN APPROVAL REQUIRED</div>
157
+ <p id="approvalPromptMsg" style="margin-bottom:12px; font-size:0.85rem; color:#e2e8f0;"></p>
158
+ <div class="form-group" id="otpInputGroup" style="display:none;">
159
+ <label>OTP / Verification Code</label>
160
+ <input type="text" id="approvalOtpCode" placeholder="Enter code">
161
+ </div>
162
+ <div class="modal-actions">
163
+ <button class="secondary-btn" onclick="resolveApproval(false)">Reject</button>
164
+ <button class="primary-btn" onclick="resolveApproval(true)">Approve & Resume</button>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <script src="/static/js/app.js"></script>
170
+ </body>
171
+ </html>