Spaces:
Sleeping
Sleeping
Commit ·
e4bef7f
1
Parent(s): f73e782
feat: add Phase 4 Mission Control UI and human-in-the-loop engine
Browse filesCo-authored-by: aider (openai/editor-model) <aider@aider.chat>
- CHANGELOG.md +10 -0
- app.py +344 -2
- static/css/style.css +73 -0
- static/js/app.js +174 -0
- 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,
|
| 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>
|