Spaces:
Sleeping
Sleeping
Claude Code commited on
Commit ·
083834d
1
Parent(s): 0bbe516
Claude Code: Review the user onboarding flow by examining and
Browse files- ONBOARDING_FLOW_REPORT.md +212 -0
- app.py +347 -11
- frontend/invite.html +50 -43
- frontend/join.html +24 -24
ONBOARDING_FLOW_REPORT.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Cain's Office - Onboarding Flow Review Report
|
| 2 |
+
|
| 3 |
+
## Executive Summary
|
| 4 |
+
|
| 5 |
+
The user onboarding flow for Cain's office is **PARTIALLY IMPLEMENTED**. The frontend HTML files exist and are well-designed, but the backend integration is incomplete in the currently running instance.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Current Status
|
| 10 |
+
|
| 11 |
+
### ✅ What Exists (Complete)
|
| 12 |
+
|
| 13 |
+
| Component | Status | Notes |
|
| 14 |
+
|-----------|--------|-------|
|
| 15 |
+
| `frontend/invite.html` | ✅ Complete | Well-designed invitation page with instructions |
|
| 16 |
+
| `frontend/join.html` | ✅ Complete | Functional form for name + join key entry |
|
| 17 |
+
| `frontend/office-agent-push.py` | ✅ Complete | Agent state push script (references external office URL) |
|
| 18 |
+
| `frontend/electron-standalone.html` | ✅ Complete | Main pixel office UI |
|
| 19 |
+
| Static assets | ✅ Served | Fonts, images, sprites all accessible via `/static/` |
|
| 20 |
+
|
| 21 |
+
### ⚠️ Partially Working (Integration Issues)
|
| 22 |
+
|
| 23 |
+
| Component | Status | Issue |
|
| 24 |
+
|-----------|--------|-------|
|
| 25 |
+
| `/invite` route | ❌ Not served | Returns OpenClaw control panel instead |
|
| 26 |
+
| `/join` route | ❌ Not served | Returns OpenClaw control panel instead |
|
| 27 |
+
| `/join-agent` (POST) | ❌ Missing | Endpoint not available |
|
| 28 |
+
| `/leave-agent` (POST) | ❌ Missing | Endpoint not available |
|
| 29 |
+
| `/agent-push` (POST) | ❌ Missing | Endpoint not available |
|
| 30 |
+
| `/admin/*` endpoints | ❌ Missing | Admin endpoints not available |
|
| 31 |
+
|
| 32 |
+
### ✅ Working Endpoints (via OpenClaw)
|
| 33 |
+
|
| 34 |
+
| Endpoint | Response | Notes |
|
| 35 |
+
|----------|----------|-------|
|
| 36 |
+
| `/health` | `{"ok":true,"status":"live"}` | Working |
|
| 37 |
+
| `/api/state` | Cain's state data | Working |
|
| 38 |
+
| `/status` | Same as /api/state | Working |
|
| 39 |
+
| `/agents` | List of 3 agents (Adam, Eve, Cain) | Working |
|
| 40 |
+
| `/static/*` | Serves frontend files | Working |
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## Issues Found
|
| 45 |
+
|
| 46 |
+
### 1. **Routing Conflict with OpenClaw**
|
| 47 |
+
The updated `app.py` contains all necessary endpoints (`/invite`, `/join`, `/join-agent`, `/leave-agent`, `/agent-push`, `/admin/*`), but Cain is currently running through the OpenClaw framework which has its own routing layer.
|
| 48 |
+
|
| 49 |
+
**Impact:** New endpoints are not accessible even though the code exists.
|
| 50 |
+
|
| 51 |
+
**Evidence:**
|
| 52 |
+
```bash
|
| 53 |
+
# This works (static files served by OpenClaw)
|
| 54 |
+
curl /static/office-agent-push.py ✅
|
| 55 |
+
|
| 56 |
+
# This returns 404 (new endpoint not registered)
|
| 57 |
+
curl -X POST /admin/create-join-key ❌
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### 2. **Brand Inconsistencies (Now Fixed)**
|
| 61 |
+
- **Before:** `invite.html` mentioned "海辛办公室" (Hyacinth's office)
|
| 62 |
+
- **Before:** `join.html` mentioned "Star 的像素办公室" (Star's pixel office)
|
| 63 |
+
- **Fixed:** Both now consistently reference "Cain's Office"
|
| 64 |
+
|
| 65 |
+
### 3. **Placeholder URLs (Now Fixed)**
|
| 66 |
+
- **Before:** `invite.html` had `https://office.example.com/join`
|
| 67 |
+
- **Fixed:** Now uses relative paths (`/invite`, `/join`)
|
| 68 |
+
|
| 69 |
+
### 4. **External Office URL in Push Script**
|
| 70 |
+
The `office-agent-push.py` script hardcodes `https://office.hyacinth.im` as the target office URL. For Cain's office, this should be configurable or point to Cain's URL.
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Complete Onboarding Flow (As Designed)
|
| 75 |
+
|
| 76 |
+
### Intended User Journey:
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
1. Admin creates join key
|
| 80 |
+
POST /admin/create-join-key → { "joinKey": "ocj_abc123" }
|
| 81 |
+
|
| 82 |
+
2. User receives invite link + key
|
| 83 |
+
https://cain-office.hf.space/invite
|
| 84 |
+
Key: ocj_abc123
|
| 85 |
+
|
| 86 |
+
3. User visits invite page
|
| 87 |
+
GET /invite → HTML with instructions
|
| 88 |
+
|
| 89 |
+
4. User configures agent script
|
| 90 |
+
- Downloads office-agent-push.py
|
| 91 |
+
- Edits JOIN_KEY and AGENT_NAME
|
| 92 |
+
- Runs: python3 office-agent-push.py
|
| 93 |
+
|
| 94 |
+
5. Agent joins office
|
| 95 |
+
POST /join-agent { name, joinKey } → { agentId, area }
|
| 96 |
+
|
| 97 |
+
6. Agent pushes state updates
|
| 98 |
+
POST /agent-push { agentId, state, detail } → { area }
|
| 99 |
+
|
| 100 |
+
7. Office displays agent
|
| 101 |
+
GET /agents → [Cain, NewAgent]
|
| 102 |
+
Office UI updates to show new agent sprite
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
## What Needs to Happen
|
| 108 |
+
|
| 109 |
+
### Option A: Deploy Updated app.py (Recommended)
|
| 110 |
+
|
| 111 |
+
To enable the complete onboarding flow:
|
| 112 |
+
|
| 113 |
+
1. **Restart Cain** with the updated `app.py` that includes:
|
| 114 |
+
- `/invite` → Serves invite.html
|
| 115 |
+
- `/join` → Serves join.html
|
| 116 |
+
- `/join-agent` → Handles agent registration
|
| 117 |
+
- `/leave-agent` → Handles agent removal
|
| 118 |
+
- `/agent-push` → Receives state updates
|
| 119 |
+
- `/admin/create-join-key` → Creates invite keys
|
| 120 |
+
- `/admin/join-keys` → Lists keys
|
| 121 |
+
- `/admin/clear-agents` → Clears agents
|
| 122 |
+
|
| 123 |
+
2. **Potential OpenClaw Integration Concern:**
|
| 124 |
+
- OpenClaw may be wrapping app.py with its own routing
|
| 125 |
+
- Need to verify if OpenClaw allows custom endpoints
|
| 126 |
+
- May need to configure OpenClaw to proxy certain routes
|
| 127 |
+
|
| 128 |
+
### Option B: OpenClaw Integration (Alternative)
|
| 129 |
+
|
| 130 |
+
If OpenClaw has extension hooks, the onboarding endpoints could be added as an OpenClaw extension/plugin instead of modifying app.py directly.
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## Data Persistence
|
| 135 |
+
|
| 136 |
+
The updated app.py includes:
|
| 137 |
+
- **Agent Registry:** Stored in `/data/office_agents.json`
|
| 138 |
+
- **Join Keys:** Stored in `/data/office_join_keys.json`
|
| 139 |
+
- **Auto-save:** Registry persists on every join/leave/update
|
| 140 |
+
|
| 141 |
+
Current state: `/data/` directory doesn't exist yet (will be created on first run).
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
## Testing the Flow
|
| 146 |
+
|
| 147 |
+
Once deployed, test with:
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
# 1. Create a join key
|
| 151 |
+
curl -X POST http://localhost:7860/admin/create-join-key \
|
| 152 |
+
-H "Content-Type: application/json" \
|
| 153 |
+
-d '{"note": "Test invite"}'
|
| 154 |
+
# Response: {"ok":true,"joinKey":"ocj_xxxxx"}
|
| 155 |
+
|
| 156 |
+
# 2. Join as an agent
|
| 157 |
+
curl -X POST http://localhost:7860/join-agent \
|
| 158 |
+
-H "Content-Type: application/json" \
|
| 159 |
+
-d '{"name":"TestBot","joinKey":"ocj_xxxxx"}'
|
| 160 |
+
# Response: {"ok":true,"agentId":"testbot","area":"breakroom"}
|
| 161 |
+
|
| 162 |
+
# 3. Push state update
|
| 163 |
+
curl -X POST http://localhost:7860/agent-push \
|
| 164 |
+
-H "Content-Type: application/json" \
|
| 165 |
+
-d '{"agentId":"testbot","state":"writing","detail":"Coding"}'
|
| 166 |
+
# Response: {"ok":true,"area":"desk"}
|
| 167 |
+
|
| 168 |
+
# 4. Verify agent appears
|
| 169 |
+
curl http://localhost:7860/agents
|
| 170 |
+
# Should show Cain + TestBot
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
## UX Gaps Identified
|
| 176 |
+
|
| 177 |
+
### Current UX Issues:
|
| 178 |
+
1. **No error feedback** when joining with invalid key (endpoint not reachable)
|
| 179 |
+
2. **No visual confirmation** that agent successfully joined
|
| 180 |
+
3. **No way to see who's currently in the office** without checking /agents API
|
| 181 |
+
4. **office-agent-push.py** needs manual configuration (no web UI for setup)
|
| 182 |
+
|
| 183 |
+
### Recommended UX Improvements:
|
| 184 |
+
1. Add real-time validation on join form
|
| 185 |
+
2. Show "Current occupants in office" on join page
|
| 186 |
+
3. Add notification/toast system for join success/failure
|
| 187 |
+
4. Create web-based agent config wizard (no manual script editing)
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## Files Modified
|
| 192 |
+
|
| 193 |
+
1. **`app.py`** - Added complete onboarding system:
|
| 194 |
+
- Agent registry management
|
| 195 |
+
- Join key generation/validation
|
| 196 |
+
- HTML page serving
|
| 197 |
+
- Admin endpoints
|
| 198 |
+
- Static file mounting
|
| 199 |
+
|
| 200 |
+
2. **`frontend/invite.html`** - Updated branding to Cain's Office, fixed URLs
|
| 201 |
+
|
| 202 |
+
3. **`frontend/join.html`** - Updated branding to Cain's Office, fixed URLs
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## Conclusion
|
| 207 |
+
|
| 208 |
+
**The onboarding flow CODE is complete and ready**, but it's not currently running because Cain is executing through OpenClaw's routing layer which doesn't include the new endpoints yet.
|
| 209 |
+
|
| 210 |
+
**To activate:** Restart Cain with the updated `app.py` or integrate the endpoints into OpenClaw's extension system.
|
| 211 |
+
|
| 212 |
+
**Estimated deployment complexity:** Low - just need to ensure the updated app.py is what runs when the container starts.
|
app.py
CHANGED
|
@@ -4,8 +4,14 @@ import sys
|
|
| 4 |
import json
|
| 5 |
import time
|
| 6 |
import datetime
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
|
| 9 |
import uvicorn
|
| 10 |
|
| 11 |
# Try importing audio libraries, but don't crash if they fail
|
|
@@ -18,7 +24,7 @@ except ImportError:
|
|
| 18 |
AUDIO_AVAILABLE = False
|
| 19 |
print("Audio libraries not available, running in text-only mode")
|
| 20 |
|
| 21 |
-
# Global state for the agent
|
| 22 |
AGENT_STATE = {
|
| 23 |
"state": "idle",
|
| 24 |
"detail": "Cain is running",
|
|
@@ -33,6 +39,90 @@ AGENT_STATE = {
|
|
| 33 |
"authStatus": "approved"
|
| 34 |
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
# Create FastAPI app for custom routes
|
| 37 |
fastapi_app = FastAPI()
|
| 38 |
|
|
@@ -63,8 +153,231 @@ async def health_check():
|
|
| 63 |
|
| 64 |
@fastapi_app.get("/agents")
|
| 65 |
async def get_agents():
|
| 66 |
-
"""Return agents
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
# Placeholder for other features
|
| 70 |
def transcribe(audio):
|
|
@@ -113,18 +426,41 @@ with gr.Blocks() as demo:
|
|
| 113 |
else:
|
| 114 |
gr.Markdown("Audio features are disabled in this environment")
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
# Mount Gradio app to FastAPI
|
| 117 |
# This allows both Gradio UI and custom API endpoints to work together
|
| 118 |
-
|
|
|
|
| 119 |
|
| 120 |
# Launch the app
|
| 121 |
-
print("Launching
|
|
|
|
| 122 |
print("Available endpoints:")
|
| 123 |
-
print(" - /
|
| 124 |
-
print(" - /
|
| 125 |
-
print(" - /
|
| 126 |
-
print(" - /
|
| 127 |
-
print(" - /
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
if __name__ == "__main__":
|
| 130 |
import uvicorn
|
|
|
|
| 4 |
import json
|
| 5 |
import time
|
| 6 |
import datetime
|
| 7 |
+
import random
|
| 8 |
+
import string
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Dict, List, Optional
|
| 11 |
+
from fastapi import FastAPI, Request
|
| 12 |
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
|
| 14 |
+
from fastapi.staticfiles import StaticFiles
|
| 15 |
import uvicorn
|
| 16 |
|
| 17 |
# Try importing audio libraries, but don't crash if they fail
|
|
|
|
| 24 |
AUDIO_AVAILABLE = False
|
| 25 |
print("Audio libraries not available, running in text-only mode")
|
| 26 |
|
| 27 |
+
# Global state for the main agent (Cain)
|
| 28 |
AGENT_STATE = {
|
| 29 |
"state": "idle",
|
| 30 |
"detail": "Cain is running",
|
|
|
|
| 39 |
"authStatus": "approved"
|
| 40 |
}
|
| 41 |
|
| 42 |
+
# Office configuration
|
| 43 |
+
OFFICE_NAME = "Cain's Office"
|
| 44 |
+
OFFICE_ADMIN = "Cain"
|
| 45 |
+
|
| 46 |
+
# Agent registry - stores all agents in the office
|
| 47 |
+
# Format: {agent_id: {name, state, detail, area, joined_at, updated_at}}
|
| 48 |
+
AGENTS_REGISTRY: Dict[str, dict] = {
|
| 49 |
+
"cain": AGENT_STATE.copy()
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
# Join keys - one-time use keys for inviting agents
|
| 53 |
+
# Format: {key: {used, created_at, used_by}}
|
| 54 |
+
JOIN_KEYS: Dict[str, dict] = {}
|
| 55 |
+
|
| 56 |
+
# Data directory for persistence
|
| 57 |
+
DATA_DIR = Path("/data")
|
| 58 |
+
AGENTS_FILE = DATA_DIR / "office_agents.json"
|
| 59 |
+
JOIN_KEYS_FILE = DATA_DIR / "office_join_keys.json"
|
| 60 |
+
|
| 61 |
+
# Ensure data directory exists
|
| 62 |
+
DATA_DIR.mkdir(exist_ok=True)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def load_registry():
|
| 66 |
+
"""Load agents registry from disk."""
|
| 67 |
+
global AGENTS_REGISTRY, JOIN_KEYS
|
| 68 |
+
try:
|
| 69 |
+
if AGENTS_FILE.exists():
|
| 70 |
+
with open(AGENTS_FILE, "r", encoding="utf-8") as f:
|
| 71 |
+
AGENTS_REGISTRY = json.load(f)
|
| 72 |
+
if JOIN_KEYS_FILE.exists():
|
| 73 |
+
with open(JOIN_KEYS_FILE, "r", encoding="utf-8") as f:
|
| 74 |
+
JOIN_KEYS = json.load(f)
|
| 75 |
+
except Exception as e:
|
| 76 |
+
print(f"Error loading registry: {e}")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def save_registry():
|
| 80 |
+
"""Save agents registry to disk."""
|
| 81 |
+
try:
|
| 82 |
+
with open(AGENTS_FILE, "w", encoding="utf-8") as f:
|
| 83 |
+
json.dump(AGENTS_REGISTRY, f, ensure_ascii=False, indent=2)
|
| 84 |
+
with open(JOIN_KEYS_FILE, "w", encoding="utf-8") as f:
|
| 85 |
+
json.dump(JOIN_KEYS, f, ensure_ascii=False, indent=2)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"Error saving registry: {e}")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def generate_agent_id(name: str) -> str:
|
| 91 |
+
"""Generate a unique agent ID from name."""
|
| 92 |
+
# Clean name: lowercase, remove special chars, replace spaces with underscores
|
| 93 |
+
clean = "".join(c if c.isalnum() else "_" for c in name.lower()).strip("_")
|
| 94 |
+
base_id = clean[:20] # Limit length
|
| 95 |
+
agent_id = base_id
|
| 96 |
+
|
| 97 |
+
# Ensure uniqueness
|
| 98 |
+
counter = 1
|
| 99 |
+
while agent_id in AGENTS_REGISTRY:
|
| 100 |
+
agent_id = f"{base_id}_{counter}"
|
| 101 |
+
counter += 1
|
| 102 |
+
return agent_id
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def generate_join_key() -> str:
|
| 106 |
+
"""Generate a new one-time join key."""
|
| 107 |
+
# Format: ocj_xxxxx (office join)
|
| 108 |
+
suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
| 109 |
+
return f"ocj_{suffix}"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def get_area_for_state(state: str) -> str:
|
| 113 |
+
"""Map agent state to office area."""
|
| 114 |
+
state_lower = state.lower()
|
| 115 |
+
if state_lower in ("error",):
|
| 116 |
+
return "serverroom"
|
| 117 |
+
elif state_lower in ("idle",):
|
| 118 |
+
return "breakroom"
|
| 119 |
+
else:
|
| 120 |
+
return "desk"
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# Load registry on startup
|
| 124 |
+
load_registry()
|
| 125 |
+
|
| 126 |
# Create FastAPI app for custom routes
|
| 127 |
fastapi_app = FastAPI()
|
| 128 |
|
|
|
|
| 153 |
|
| 154 |
@fastapi_app.get("/agents")
|
| 155 |
async def get_agents():
|
| 156 |
+
"""Return all agents in the office."""
|
| 157 |
+
# Update timestamp for Cain
|
| 158 |
+
AGENT_STATE["updated_at"] = datetime.datetime.now().isoformat()
|
| 159 |
+
AGENTS_REGISTRY["cain"] = AGENT_STATE.copy()
|
| 160 |
+
save_registry()
|
| 161 |
+
return list(AGENTS_REGISTRY.values())
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ==================== Office HTML Pages ====================
|
| 165 |
+
|
| 166 |
+
@fastapi_app.get("/invite", response_class=HTMLResponse)
|
| 167 |
+
async def invite_page():
|
| 168 |
+
"""Serve the invite page."""
|
| 169 |
+
invite_path = Path(__file__).parent / "frontend" / "invite.html"
|
| 170 |
+
if invite_path.exists():
|
| 171 |
+
return HTMLResponse(content=invite_path.read_text(encoding="utf-8"))
|
| 172 |
+
return HTMLResponse(content="<h1>Invite page not found</h1>", status_code=404)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@fastapi_app.get("/join", response_class=HTMLResponse)
|
| 176 |
+
async def join_page():
|
| 177 |
+
"""Serve the join page."""
|
| 178 |
+
join_path = Path(__file__).parent / "frontend" / "join.html"
|
| 179 |
+
if join_path.exists():
|
| 180 |
+
return HTMLResponse(content=join_path.read_text(encoding="utf-8"))
|
| 181 |
+
return HTMLResponse(content="<h1>Join page not found</h1>", status_code=404)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
@fastapi_app.get("/", response_class=HTMLResponse)
|
| 185 |
+
async def office_page():
|
| 186 |
+
"""Serve the main office page."""
|
| 187 |
+
office_path = Path(__file__).parent / "frontend" / "electron-standalone.html"
|
| 188 |
+
if office_path.exists():
|
| 189 |
+
return HTMLResponse(content=office_path.read_text(encoding="utf-8"))
|
| 190 |
+
# Fallback to Gradio if office page not found
|
| 191 |
+
return None # Let Gradio handle it
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# ==================== Agent Join/Leave Endpoints ====================
|
| 195 |
+
|
| 196 |
+
@fastapi_app.post("/join-agent")
|
| 197 |
+
async def join_agent(request: Request):
|
| 198 |
+
"""Handle agent join requests."""
|
| 199 |
+
try:
|
| 200 |
+
data = await request.json()
|
| 201 |
+
name = data.get("name", "").strip()
|
| 202 |
+
join_key = data.get("joinKey", "").strip()
|
| 203 |
+
initial_state = data.get("state", "idle")
|
| 204 |
+
initial_detail = data.get("detail", "Just joined")
|
| 205 |
+
|
| 206 |
+
if not name:
|
| 207 |
+
return JSONResponse(content={"ok": False, "msg": "Name is required"}, status_code=400)
|
| 208 |
+
if not join_key:
|
| 209 |
+
return JSONResponse(content={"ok": False, "msg": "Join key is required"}, status_code=400)
|
| 210 |
+
|
| 211 |
+
# Check if join key exists and is valid
|
| 212 |
+
if join_key not in JOIN_KEYS:
|
| 213 |
+
return JSONResponse(content={"ok": False, "msg": "Invalid join key"}, status_code=401)
|
| 214 |
+
|
| 215 |
+
key_info = JOIN_KEYS[join_key]
|
| 216 |
+
if key_info.get("used"):
|
| 217 |
+
return JSONResponse(content={"ok": False, "msg": "Join key already used"}, status_code=401)
|
| 218 |
+
|
| 219 |
+
# Generate agent ID
|
| 220 |
+
agent_id = generate_agent_id(name)
|
| 221 |
+
area = get_area_for_state(initial_state)
|
| 222 |
+
|
| 223 |
+
# Register the agent
|
| 224 |
+
agent_data = {
|
| 225 |
+
"agentId": agent_id,
|
| 226 |
+
"name": name,
|
| 227 |
+
"state": initial_state,
|
| 228 |
+
"detail": initial_detail,
|
| 229 |
+
"area": area,
|
| 230 |
+
"authStatus": "approved",
|
| 231 |
+
"joined_at": datetime.datetime.now().isoformat(),
|
| 232 |
+
"updated_at": datetime.datetime.now().isoformat()
|
| 233 |
+
}
|
| 234 |
+
AGENTS_REGISTRY[agent_id] = agent_data
|
| 235 |
+
|
| 236 |
+
# Mark join key as used
|
| 237 |
+
JOIN_KEYS[join_key]["used"] = True
|
| 238 |
+
JOIN_KEYS[join_key]["used_by"] = agent_id
|
| 239 |
+
JOIN_KEYS[join_key]["used_at"] = datetime.datetime.now().isoformat()
|
| 240 |
+
|
| 241 |
+
save_registry()
|
| 242 |
+
|
| 243 |
+
return JSONResponse(content={
|
| 244 |
+
"ok": True,
|
| 245 |
+
"agentId": agent_id,
|
| 246 |
+
"area": area,
|
| 247 |
+
"msg": f"Welcome to {OFFICE_NAME}!"
|
| 248 |
+
})
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
return JSONResponse(content={"ok": False, "msg": f"Error: {str(e)}"}, status_code=500)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
@fastapi_app.post("/leave-agent")
|
| 255 |
+
async def leave_agent(request: Request):
|
| 256 |
+
"""Handle agent leave requests."""
|
| 257 |
+
try:
|
| 258 |
+
data = await request.json()
|
| 259 |
+
name = data.get("name", "").strip()
|
| 260 |
+
|
| 261 |
+
if not name:
|
| 262 |
+
return JSONResponse(content={"ok": False, "msg": "Name is required"}, status_code=400)
|
| 263 |
+
|
| 264 |
+
# Find agent by name (case-insensitive)
|
| 265 |
+
agent_id = None
|
| 266 |
+
for aid, agent in AGENTS_REGISTRY.items():
|
| 267 |
+
if agent.get("name", "").lower() == name.lower() and aid != "cain":
|
| 268 |
+
agent_id = aid
|
| 269 |
+
break
|
| 270 |
+
|
| 271 |
+
if not agent_id:
|
| 272 |
+
return JSONResponse(content={"ok": False, "msg": "Agent not found"}, status_code=404)
|
| 273 |
+
|
| 274 |
+
# Don't allow removing Cain
|
| 275 |
+
if agent_id == "cain":
|
| 276 |
+
return JSONResponse(content={"ok": False, "msg": "Cannot remove the office admin"}, status_code=403)
|
| 277 |
+
|
| 278 |
+
# Remove agent
|
| 279 |
+
del AGENTS_REGISTRY[agent_id]
|
| 280 |
+
save_registry()
|
| 281 |
+
|
| 282 |
+
return JSONResponse(content={"ok": True, "msg": f"Goodbye, {name}!"})
|
| 283 |
+
|
| 284 |
+
except Exception as e:
|
| 285 |
+
return JSONResponse(content={"ok": False, "msg": f"Error: {str(e)}"}, status_code=500)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
@fastapi_app.post("/agent-push")
|
| 289 |
+
async def agent_push(request: Request):
|
| 290 |
+
"""Receive agent state updates."""
|
| 291 |
+
try:
|
| 292 |
+
data = await request.json()
|
| 293 |
+
agent_id = data.get("agentId", "").strip()
|
| 294 |
+
join_key = data.get("joinKey", "").strip()
|
| 295 |
+
state = data.get("state", "idle")
|
| 296 |
+
detail = data.get("detail", "")
|
| 297 |
+
name = data.get("name", "").strip()
|
| 298 |
+
|
| 299 |
+
if not agent_id:
|
| 300 |
+
return JSONResponse(content={"ok": False, "msg": "agentId is required"}, status_code=400)
|
| 301 |
+
|
| 302 |
+
# Check if agent exists
|
| 303 |
+
if agent_id not in AGENTS_REGISTRY:
|
| 304 |
+
return JSONResponse(content={"ok": False, "msg": "Agent not found"}, status_code=404)
|
| 305 |
+
|
| 306 |
+
# Verify join key (optional validation for security)
|
| 307 |
+
# For now, we trust agents that have already joined
|
| 308 |
+
|
| 309 |
+
# Update agent state
|
| 310 |
+
area = get_area_for_state(state)
|
| 311 |
+
AGENTS_REGISTRY[agent_id].update({
|
| 312 |
+
"state": state,
|
| 313 |
+
"detail": detail,
|
| 314 |
+
"area": area,
|
| 315 |
+
"updated_at": datetime.datetime.now().isoformat()
|
| 316 |
+
})
|
| 317 |
+
if name:
|
| 318 |
+
AGENTS_REGISTRY[agent_id]["name"] = name
|
| 319 |
+
|
| 320 |
+
save_registry()
|
| 321 |
+
|
| 322 |
+
return JSONResponse(content={
|
| 323 |
+
"ok": True,
|
| 324 |
+
"area": area,
|
| 325 |
+
"msg": "State updated"
|
| 326 |
+
})
|
| 327 |
+
|
| 328 |
+
except Exception as e:
|
| 329 |
+
return JSONResponse(content={"ok": False, "msg": f"Error: {str(e)}"}, status_code=500)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
# ==================== Admin Endpoints ====================
|
| 333 |
+
|
| 334 |
+
@fastapi_app.post("/admin/create-join-key")
|
| 335 |
+
async def create_join_key(request: Request):
|
| 336 |
+
"""Create a new join key (admin endpoint)."""
|
| 337 |
+
try:
|
| 338 |
+
data = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {}
|
| 339 |
+
max_uses = data.get("maxUses", 1)
|
| 340 |
+
note = data.get("note", "")
|
| 341 |
+
|
| 342 |
+
join_key = generate_join_key()
|
| 343 |
+
JOIN_KEYS[join_key] = {
|
| 344 |
+
"created_at": datetime.datetime.now().isoformat(),
|
| 345 |
+
"used": False,
|
| 346 |
+
"max_uses": max_uses,
|
| 347 |
+
"note": note
|
| 348 |
+
}
|
| 349 |
+
save_registry()
|
| 350 |
+
|
| 351 |
+
return JSONResponse(content={
|
| 352 |
+
"ok": True,
|
| 353 |
+
"joinKey": join_key,
|
| 354 |
+
"note": note
|
| 355 |
+
})
|
| 356 |
+
|
| 357 |
+
except Exception as e:
|
| 358 |
+
return JSONResponse(content={"ok": False, "msg": f"Error: {str(e)}"}, status_code=500)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
@fastapi_app.get("/admin/join-keys")
|
| 362 |
+
async def list_join_keys():
|
| 363 |
+
"""List all join keys (admin endpoint)."""
|
| 364 |
+
return JSONResponse(content={
|
| 365 |
+
"ok": True,
|
| 366 |
+
"keys": JOIN_KEYS
|
| 367 |
+
})
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
@fastapi_app.post("/admin/clear-agents")
|
| 371 |
+
async def clear_agents(request: Request):
|
| 372 |
+
"""Clear all agents except Cain (admin endpoint)."""
|
| 373 |
+
global AGENTS_REGISTRY
|
| 374 |
+
try:
|
| 375 |
+
cain_state = AGENTS_REGISTRY.get("cain", AGENT_STATE.copy())
|
| 376 |
+
AGENTS_REGISTRY = {"cain": cain_state}
|
| 377 |
+
save_registry()
|
| 378 |
+
return JSONResponse(content={"ok": True, "msg": "All agents cleared"})
|
| 379 |
+
except Exception as e:
|
| 380 |
+
return JSONResponse(content={"ok": False, "msg": f"Error: {str(e)}"}, status_code=500)
|
| 381 |
|
| 382 |
# Placeholder for other features
|
| 383 |
def transcribe(audio):
|
|
|
|
| 426 |
else:
|
| 427 |
gr.Markdown("Audio features are disabled in this environment")
|
| 428 |
|
| 429 |
+
# Mount static files directory
|
| 430 |
+
static_dir = Path(__file__).parent / "frontend"
|
| 431 |
+
if static_dir.exists():
|
| 432 |
+
fastapi_app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
| 433 |
+
else:
|
| 434 |
+
print(f"Warning: Static directory not found at {static_dir}")
|
| 435 |
+
|
| 436 |
# Mount Gradio app to FastAPI
|
| 437 |
# This allows both Gradio UI and custom API endpoints to work together
|
| 438 |
+
# Note: We mount Gradio at /gradio to avoid conflicts with our custom routes
|
| 439 |
+
gradio_app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio")
|
| 440 |
|
| 441 |
# Launch the app
|
| 442 |
+
print("Launching Cain's Office with FastAPI on 0.0.0.0:7860...")
|
| 443 |
+
print("=" * 60)
|
| 444 |
print("Available endpoints:")
|
| 445 |
+
print(" - / (Main Office UI)")
|
| 446 |
+
print(" - /invite (Invite page)")
|
| 447 |
+
print(" - /join (Join page)")
|
| 448 |
+
print(" - /gradio (Gradio UI)")
|
| 449 |
+
print(" - /api/state (Agent state API)")
|
| 450 |
+
print(" - /status (Agent status)")
|
| 451 |
+
print(" - /agents (List all agents)")
|
| 452 |
+
print(" - /health (Health check)")
|
| 453 |
+
print("")
|
| 454 |
+
print("Agent API endpoints:")
|
| 455 |
+
print(" POST /join-agent (Join the office)")
|
| 456 |
+
print(" POST /leave-agent (Leave the office)")
|
| 457 |
+
print(" POST /agent-push (Push state updates)")
|
| 458 |
+
print("")
|
| 459 |
+
print("Admin endpoints:")
|
| 460 |
+
print(" POST /admin/create-join-key (Create invite key)")
|
| 461 |
+
print(" GET /admin/join-keys (List all keys)")
|
| 462 |
+
print(" POST /admin/clear-agents (Remove all agents)")
|
| 463 |
+
print("=" * 60)
|
| 464 |
|
| 465 |
if __name__ == "__main__":
|
| 466 |
import uvicorn
|
frontend/invite.html
CHANGED
|
@@ -1,42 +1,44 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html lang="
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
| 7 |
<style>
|
| 8 |
body {
|
| 9 |
margin: 0;
|
| 10 |
padding: 40px;
|
| 11 |
-
font-family: "
|
| 12 |
-
background: linear-gradient(135deg, #
|
| 13 |
min-height: 100vh;
|
| 14 |
box-sizing: border-box;
|
|
|
|
| 15 |
}
|
| 16 |
.card {
|
| 17 |
max-width: 800px;
|
| 18 |
margin: 0 auto;
|
| 19 |
-
background:
|
| 20 |
padding: 48px;
|
| 21 |
border-radius: 16px;
|
| 22 |
-
box-shadow: 0 10px 40px rgba(0,0,0,0.
|
|
|
|
| 23 |
}
|
| 24 |
h1 {
|
| 25 |
margin-top: 0;
|
| 26 |
-
color: #
|
| 27 |
font-size: 28px;
|
| 28 |
}
|
| 29 |
h2 {
|
| 30 |
margin-top: 32px;
|
| 31 |
-
color: #
|
| 32 |
font-size: 18px;
|
| 33 |
}
|
| 34 |
p, li {
|
| 35 |
-
color: #
|
| 36 |
line-height: 1.8;
|
| 37 |
}
|
| 38 |
.steps {
|
| 39 |
-
background:
|
| 40 |
padding: 24px;
|
| 41 |
border-radius: 12px;
|
| 42 |
margin: 16px 0;
|
|
@@ -53,7 +55,7 @@
|
|
| 53 |
width: 28px;
|
| 54 |
height: 28px;
|
| 55 |
border-radius: 50%;
|
| 56 |
-
background: #
|
| 57 |
color: white;
|
| 58 |
font-weight: 600;
|
| 59 |
display: flex;
|
|
@@ -66,93 +68,98 @@
|
|
| 66 |
flex: 1;
|
| 67 |
}
|
| 68 |
.step-text strong {
|
| 69 |
-
color: #
|
| 70 |
}
|
| 71 |
-
.
|
| 72 |
-
background:
|
| 73 |
-
padding:
|
| 74 |
-
border-radius:
|
| 75 |
-
font-family:
|
| 76 |
-
|
| 77 |
-
word-break: break-all;
|
| 78 |
-
margin-top: 8px;
|
| 79 |
}
|
| 80 |
.note {
|
| 81 |
margin-top: 24px;
|
| 82 |
padding: 16px;
|
| 83 |
-
border-left: 4px solid #
|
| 84 |
-
background:
|
| 85 |
border-radius: 0 8px 8px 0;
|
| 86 |
}
|
| 87 |
.note strong {
|
| 88 |
-
color: #
|
| 89 |
}
|
| 90 |
.footer {
|
| 91 |
margin-top: 32px;
|
| 92 |
padding-top: 24px;
|
| 93 |
-
border-top: 1px solid
|
| 94 |
-
color: #
|
| 95 |
font-size: 14px;
|
| 96 |
}
|
| 97 |
.back-btn {
|
| 98 |
display: inline-block;
|
| 99 |
margin-top: 24px;
|
| 100 |
padding: 12px 24px;
|
| 101 |
-
background: #
|
| 102 |
color: white;
|
| 103 |
text-decoration: none;
|
| 104 |
border-radius: 8px;
|
| 105 |
font-weight: 500;
|
|
|
|
| 106 |
}
|
| 107 |
.back-btn:hover {
|
| 108 |
-
background: #
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
}
|
| 110 |
</style>
|
| 111 |
</head>
|
| 112 |
<body>
|
| 113 |
<div class="card">
|
| 114 |
-
<h1>
|
| 115 |
-
<p>
|
| 116 |
|
| 117 |
-
<h2>
|
| 118 |
<div class="steps">
|
| 119 |
<div class="step">
|
| 120 |
<div class="step-num">1</div>
|
| 121 |
<div class="step-text">
|
| 122 |
-
<strong>
|
| 123 |
-
|
| 124 |
<ul>
|
| 125 |
-
<li>
|
| 126 |
-
<li>
|
| 127 |
</ul>
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
<div class="step">
|
| 131 |
<div class="step-num">2</div>
|
| 132 |
<div class="step-text">
|
| 133 |
-
<strong>
|
| 134 |
-
|
|
|
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
<div class="step">
|
| 138 |
<div class="step-num">3</div>
|
| 139 |
<div class="step-text">
|
| 140 |
-
<strong>
|
| 141 |
-
|
|
|
|
| 142 |
</div>
|
| 143 |
</div>
|
| 144 |
</div>
|
| 145 |
|
| 146 |
<div class="note">
|
| 147 |
-
<strong>⚠️
|
| 148 |
-
|
| 149 |
</div>
|
| 150 |
|
| 151 |
-
<a href="/" class="back-btn">←
|
| 152 |
|
| 153 |
<div class="footer">
|
| 154 |
-
|
| 155 |
-
|
| 156 |
</div>
|
| 157 |
</div>
|
| 158 |
</body>
|
|
|
|
| 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">
|
| 6 |
+
<title>Cain's Office - Join Invitation</title>
|
| 7 |
<style>
|
| 8 |
body {
|
| 9 |
margin: 0;
|
| 10 |
padding: 40px;
|
| 11 |
+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
| 12 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 13 |
min-height: 100vh;
|
| 14 |
box-sizing: border-box;
|
| 15 |
+
color: #eee;
|
| 16 |
}
|
| 17 |
.card {
|
| 18 |
max-width: 800px;
|
| 19 |
margin: 0 auto;
|
| 20 |
+
background: rgba(44, 47, 58, 0.95);
|
| 21 |
padding: 48px;
|
| 22 |
border-radius: 16px;
|
| 23 |
+
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
| 24 |
+
border: 2px solid #e94560;
|
| 25 |
}
|
| 26 |
h1 {
|
| 27 |
margin-top: 0;
|
| 28 |
+
color: #ffd700;
|
| 29 |
font-size: 28px;
|
| 30 |
}
|
| 31 |
h2 {
|
| 32 |
margin-top: 32px;
|
| 33 |
+
color: #e94560;
|
| 34 |
font-size: 18px;
|
| 35 |
}
|
| 36 |
p, li {
|
| 37 |
+
color: #ccc;
|
| 38 |
line-height: 1.8;
|
| 39 |
}
|
| 40 |
.steps {
|
| 41 |
+
background: rgba(0,0,0,0.2);
|
| 42 |
padding: 24px;
|
| 43 |
border-radius: 12px;
|
| 44 |
margin: 16px 0;
|
|
|
|
| 55 |
width: 28px;
|
| 56 |
height: 28px;
|
| 57 |
border-radius: 50%;
|
| 58 |
+
background: #e94560;
|
| 59 |
color: white;
|
| 60 |
font-weight: 600;
|
| 61 |
display: flex;
|
|
|
|
| 68 |
flex: 1;
|
| 69 |
}
|
| 70 |
.step-text strong {
|
| 71 |
+
color: #fff;
|
| 72 |
}
|
| 73 |
+
.step-text code {
|
| 74 |
+
background: rgba(0,0,0,0.3);
|
| 75 |
+
padding: 2px 6px;
|
| 76 |
+
border-radius: 4px;
|
| 77 |
+
font-family: 'Courier New', monospace;
|
| 78 |
+
color: #ffd700;
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
.note {
|
| 81 |
margin-top: 24px;
|
| 82 |
padding: 16px;
|
| 83 |
+
border-left: 4px solid #ffd700;
|
| 84 |
+
background: rgba(255, 215, 0, 0.1);
|
| 85 |
border-radius: 0 8px 8px 0;
|
| 86 |
}
|
| 87 |
.note strong {
|
| 88 |
+
color: #ffd700;
|
| 89 |
}
|
| 90 |
.footer {
|
| 91 |
margin-top: 32px;
|
| 92 |
padding-top: 24px;
|
| 93 |
+
border-top: 1px solid rgba(255,255,255,0.1);
|
| 94 |
+
color: #888;
|
| 95 |
font-size: 14px;
|
| 96 |
}
|
| 97 |
.back-btn {
|
| 98 |
display: inline-block;
|
| 99 |
margin-top: 24px;
|
| 100 |
padding: 12px 24px;
|
| 101 |
+
background: #e94560;
|
| 102 |
color: white;
|
| 103 |
text-decoration: none;
|
| 104 |
border-radius: 8px;
|
| 105 |
font-weight: 500;
|
| 106 |
+
transition: background 0.2s;
|
| 107 |
}
|
| 108 |
.back-btn:hover {
|
| 109 |
+
background: #ff6b81;
|
| 110 |
+
}
|
| 111 |
+
ul {
|
| 112 |
+
margin: 8px 0;
|
| 113 |
+
padding-left: 20px;
|
| 114 |
}
|
| 115 |
</style>
|
| 116 |
</head>
|
| 117 |
<body>
|
| 118 |
<div class="card">
|
| 119 |
+
<h1>🤖 Cain's Office · Join Invitation</h1>
|
| 120 |
+
<p>Welcome to Cain's Pixel Office Dashboard!</p>
|
| 121 |
|
| 122 |
+
<h2>Join Steps (3 steps)</h2>
|
| 123 |
<div class="steps">
|
| 124 |
<div class="step">
|
| 125 |
<div class="step-num">1</div>
|
| 126 |
<div class="step-text">
|
| 127 |
+
<strong>Confirm Your Info</strong><br>
|
| 128 |
+
You should have received two things:
|
| 129 |
<ul>
|
| 130 |
+
<li>Invite link: <code>/join</code></li>
|
| 131 |
+
<li>One-time join key: <code>ocj_xxx</code></li>
|
| 132 |
</ul>
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
<div class="step">
|
| 136 |
<div class="step-num">2</div>
|
| 137 |
<div class="step-text">
|
| 138 |
+
<strong>Download the Push Script</strong><br>
|
| 139 |
+
Download <code>office-agent-push.py</code> from the <code>/static/</code> directory.<br>
|
| 140 |
+
Then fill in your join key and agent name.
|
| 141 |
</div>
|
| 142 |
</div>
|
| 143 |
<div class="step">
|
| 144 |
<div class="step-num">3</div>
|
| 145 |
<div class="step-text">
|
| 146 |
+
<strong>Run the Script</strong><br>
|
| 147 |
+
Run <code>python3 office-agent-push.py</code><br>
|
| 148 |
+
Your agent will automatically join the office and start pushing status updates!
|
| 149 |
</div>
|
| 150 |
</div>
|
| 151 |
</div>
|
| 152 |
|
| 153 |
<div class="note">
|
| 154 |
+
<strong>⚠️ Privacy Notice</strong><br>
|
| 155 |
+
Only status is pushed (idle/writing/researching/executing/syncing/error), no content or privacy data. Stop anytime.
|
| 156 |
</div>
|
| 157 |
|
| 158 |
+
<a href="/" class="back-btn">← Back to Cain's Office</a>
|
| 159 |
|
| 160 |
<div class="footer">
|
| 161 |
+
Cain's Office · Pixel Office Dashboard<br>
|
| 162 |
+
Powered by HuggingClaw
|
| 163 |
</div>
|
| 164 |
</div>
|
| 165 |
</body>
|
frontend/join.html
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
-
<html lang="
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
| 7 |
<style>
|
| 8 |
@font-face {
|
| 9 |
font-family: 'ArkPixel';
|
|
@@ -96,31 +96,31 @@
|
|
| 96 |
text-align: center;
|
| 97 |
line-height: 1.8;
|
| 98 |
}
|
| 99 |
-
.note a { word-break: break-all; }
|
| 100 |
</style>
|
| 101 |
</head>
|
| 102 |
<body>
|
| 103 |
-
<h1>
|
| 104 |
<div class="container">
|
| 105 |
<div class="form-group">
|
| 106 |
-
<label>
|
| 107 |
-
<input type="text" id="agentName" placeholder="
|
| 108 |
</div>
|
| 109 |
-
<!-- 状态与细节改为自动同步,不在 join 页面填写 -->
|
| 110 |
<div class="form-group">
|
| 111 |
-
<label>Agent
|
| 112 |
-
<input type="text" id="joinKey" placeholder="
|
| 113 |
</div>
|
| 114 |
-
<button id="joinBtn">
|
| 115 |
-
<button id="leaveBtn" style="margin-top:10px; background:#555; border-color:#555;">
|
| 116 |
<div id="status" class="status" style="display:none;"></div>
|
| 117 |
</div>
|
| 118 |
<div class="note">
|
| 119 |
-
⚠️
|
| 120 |
-
|
| 121 |
<br><br>
|
| 122 |
-
📌
|
| 123 |
-
<
|
|
|
|
| 124 |
</div>
|
| 125 |
|
| 126 |
<script>
|
|
@@ -140,11 +140,11 @@
|
|
| 140 |
const name = agentNameInput.value.trim();
|
| 141 |
const joinKey = joinKeyInput.value.trim();
|
| 142 |
if (!name) {
|
| 143 |
-
showStatus('
|
| 144 |
return;
|
| 145 |
}
|
| 146 |
if (!joinKey) {
|
| 147 |
-
showStatus('
|
| 148 |
return;
|
| 149 |
}
|
| 150 |
try {
|
|
@@ -155,19 +155,19 @@
|
|
| 155 |
});
|
| 156 |
const data = await response.json();
|
| 157 |
if (data.ok) {
|
| 158 |
-
showStatus('
|
| 159 |
} else {
|
| 160 |
-
showStatus(data.msg || '
|
| 161 |
}
|
| 162 |
} catch (e) {
|
| 163 |
-
showStatus('
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
| 167 |
async function leave() {
|
| 168 |
const name = agentNameInput.value.trim();
|
| 169 |
if (!name) {
|
| 170 |
-
showStatus('
|
| 171 |
return;
|
| 172 |
}
|
| 173 |
try {
|
|
@@ -178,12 +178,12 @@
|
|
| 178 |
});
|
| 179 |
const data = await response.json();
|
| 180 |
if (data.ok) {
|
| 181 |
-
showStatus('
|
| 182 |
} else {
|
| 183 |
-
showStatus(data.msg || '
|
| 184 |
}
|
| 185 |
} catch (e) {
|
| 186 |
-
showStatus('
|
| 187 |
}
|
| 188 |
}
|
| 189 |
|
|
|
|
| 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">
|
| 6 |
+
<title>Join Cain's Office</title>
|
| 7 |
<style>
|
| 8 |
@font-face {
|
| 9 |
font-family: 'ArkPixel';
|
|
|
|
| 96 |
text-align: center;
|
| 97 |
line-height: 1.8;
|
| 98 |
}
|
| 99 |
+
.note a { word-break: break-all; color: #ffd700; }
|
| 100 |
</style>
|
| 101 |
</head>
|
| 102 |
<body>
|
| 103 |
+
<h1>🤖 Join Cain's Office</h1>
|
| 104 |
<div class="container">
|
| 105 |
<div class="form-group">
|
| 106 |
+
<label>Your Name (shown in office)</label>
|
| 107 |
+
<input type="text" id="agentName" placeholder="e.g., HelperBot" maxlength="20">
|
| 108 |
</div>
|
|
|
|
| 109 |
<div class="form-group">
|
| 110 |
+
<label>Agent Join Key (one-time)</label>
|
| 111 |
+
<input type="text" id="joinKey" placeholder="Enter your join key (ocj_xxx)" maxlength="64">
|
| 112 |
</div>
|
| 113 |
+
<button id="joinBtn">Join Office</button>
|
| 114 |
+
<button id="leaveBtn" style="margin-top:10px; background:#555; border-color:#555;">Leave Office</button>
|
| 115 |
<div id="status" class="status" style="display:none;"></div>
|
| 116 |
</div>
|
| 117 |
<div class="note">
|
| 118 |
+
⚠️ Note: Join page only needs name + one-time join key<br>
|
| 119 |
+
Status will be automatically synced by your agent later
|
| 120 |
<br><br>
|
| 121 |
+
📌 Need an invite? <a href="/invite">Get invite link here</a>
|
| 122 |
+
<br><br>
|
| 123 |
+
📌 Download agent script: <a href="/static/office-agent-push.py">office-agent-push.py</a>
|
| 124 |
</div>
|
| 125 |
|
| 126 |
<script>
|
|
|
|
| 140 |
const name = agentNameInput.value.trim();
|
| 141 |
const joinKey = joinKeyInput.value.trim();
|
| 142 |
if (!name) {
|
| 143 |
+
showStatus('Please enter your name first~', false);
|
| 144 |
return;
|
| 145 |
}
|
| 146 |
if (!joinKey) {
|
| 147 |
+
showStatus('Please enter the Agent join key~', false);
|
| 148 |
return;
|
| 149 |
}
|
| 150 |
try {
|
|
|
|
| 155 |
});
|
| 156 |
const data = await response.json();
|
| 157 |
if (data.ok) {
|
| 158 |
+
showStatus('Joined successfully! You will appear in the office. ✨', true);
|
| 159 |
} else {
|
| 160 |
+
showStatus(data.msg || 'Join failed', false);
|
| 161 |
}
|
| 162 |
} catch (e) {
|
| 163 |
+
showStatus('Network error, please try again', false);
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
| 167 |
async function leave() {
|
| 168 |
const name = agentNameInput.value.trim();
|
| 169 |
if (!name) {
|
| 170 |
+
showStatus('Please enter the name you want to leave with~', false);
|
| 171 |
return;
|
| 172 |
}
|
| 173 |
try {
|
|
|
|
| 178 |
});
|
| 179 |
const data = await response.json();
|
| 180 |
if (data.ok) {
|
| 181 |
+
showStatus('Left the office 👋', true);
|
| 182 |
} else {
|
| 183 |
+
showStatus(data.msg || 'Leave failed', false);
|
| 184 |
}
|
| 185 |
} catch (e) {
|
| 186 |
+
showStatus('Network error, please try again', false);
|
| 187 |
}
|
| 188 |
}
|
| 189 |
|