Skip to content

Commit ceb4a0b

Browse files
authored
Merge pull request #19 from Daylily-Informatics/feature/gui-enhancements
feat: Full GUI Enhancement for Rank 4/5 Voice Agent Testing
2 parents 2a9a72f + 2b53a33 commit ceb4a0b

File tree

8 files changed

+1061
-3
lines changed

8 files changed

+1061
-3
lines changed

client/gui.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,14 @@ def _secure_log_filename(name: str) -> str:
5151
_HAS_TRANSCRIBE_STREAMING = False
5252

5353

54+
from fastapi.staticfiles import StaticFiles
55+
5456
app = FastAPI(title="marvain — Agent Manager")
5557
templates = Jinja2Templates(directory="client/templates")
5658

59+
# Mount static files directory
60+
app.mount("/static", StaticFiles(directory="client/static"), name="static")
61+
5762
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
5863

5964
SAMCONFIG_PATH = Path("samconfig.toml")
@@ -966,6 +971,18 @@ def chat_page(request: Request):
966971
return templates.TemplateResponse("chat.html", {"request": request, "state": STATE, "stack_name": label})
967972

968973

974+
@app.get("/speakers")
975+
def speakers_page(request: Request):
976+
"""Speaker management page."""
977+
return templates.TemplateResponse("speakers.html", {"request": request, "state": STATE})
978+
979+
980+
@app.get("/memories")
981+
def memories_page(request: Request):
982+
"""Memory visualization page."""
983+
return templates.TemplateResponse("memories.html", {"request": request, "state": STATE})
984+
985+
969986
@app.post("/api/send_message")
970987
async def send_message(request: Request):
971988
if not STATE.selected_endpoint:
@@ -1264,6 +1281,135 @@ async def get_conversation_context(session_id: Optional[str] = None):
12641281
return {"context": [], "error": str(e)}
12651282

12661283

1284+
@app.post("/api/speakers/enroll")
1285+
async def enroll_speaker(request: Request):
1286+
"""Enroll a new speaker with voice samples."""
1287+
from fastapi import UploadFile
1288+
import io
1289+
1290+
table = _agent_state_table()
1291+
if not table:
1292+
return JSONResponse(content={"error": "No table configured"}, status_code=400)
1293+
1294+
agent_id = STATE.selected_agent_id or "agent1"
1295+
1296+
try:
1297+
form = await request.form()
1298+
speaker_name = form.get("speaker_name", "").strip()
1299+
if not speaker_name:
1300+
return JSONResponse(content={"error": "Speaker name is required"}, status_code=400)
1301+
1302+
# Get audio files
1303+
audio_files = form.getlist("audio_files")
1304+
voice_samples = []
1305+
1306+
for i, audio_file in enumerate(audio_files):
1307+
if hasattr(audio_file, "read"):
1308+
content = await audio_file.read()
1309+
# Store as base64 (for simplicity; in production, upload to S3)
1310+
import base64
1311+
voice_samples.append({
1312+
"index": i,
1313+
"size": len(content),
1314+
"data_b64": base64.b64encode(content).decode("utf-8")[:1000], # Truncate for storage
1315+
})
1316+
1317+
# Generate speaker ID
1318+
speaker_id = f"spk_{speaker_name.lower().replace(' ', '_')}_{uuid4().hex[:8]}"
1319+
1320+
# Store in DynamoDB
1321+
pk = f"AGENT#{agent_id}#VOICE"
1322+
item = {
1323+
"pk": pk,
1324+
"sk": speaker_id,
1325+
"speaker_id": speaker_id,
1326+
"speaker_name": speaker_name,
1327+
"enrollment_status": "enrolled",
1328+
"first_seen_ts": datetime.utcnow().isoformat(),
1329+
"last_seen_ts": datetime.utcnow().isoformat(),
1330+
"interaction_count": 0,
1331+
"voice_samples": voice_samples,
1332+
}
1333+
table.put_item(Item=item)
1334+
1335+
return {
1336+
"success": True,
1337+
"speaker_id": speaker_id,
1338+
"speaker_name": speaker_name,
1339+
"samples_count": len(voice_samples),
1340+
}
1341+
1342+
except Exception as e:
1343+
logging.error("enroll_speaker failed: %s", e)
1344+
return JSONResponse(content={"error": str(e)}, status_code=500)
1345+
1346+
1347+
@app.get("/api/tool_status")
1348+
async def get_tool_status():
1349+
"""Get status of recent tool executions from the agent state."""
1350+
table = _agent_state_table()
1351+
if not table:
1352+
return {"tools": [], "error": "No table configured"}
1353+
1354+
agent_id = STATE.selected_agent_id or "agent1"
1355+
1356+
try:
1357+
pk = f"AGENT#{agent_id}"
1358+
resp = table.query(
1359+
KeyConditionExpression=Key("pk").eq(pk),
1360+
ScanIndexForward=False,
1361+
Limit=50,
1362+
)
1363+
1364+
# Filter for events that include tool executions
1365+
tool_events = []
1366+
for item in resp.get("Items", []):
1367+
payload = item.get("payload", {})
1368+
if isinstance(payload, str):
1369+
try:
1370+
payload = json.loads(payload)
1371+
except Exception:
1372+
payload = {}
1373+
1374+
# Check if this event has tool info
1375+
if payload.get("tool_calls") or item.get("source") == "tool":
1376+
tool_events.append({
1377+
"ts": item.get("ts"),
1378+
"session_id": item.get("session_id"),
1379+
"tool_calls": payload.get("tool_calls", []),
1380+
"iterations": payload.get("iterations", 1),
1381+
})
1382+
1383+
return {
1384+
"tools": tool_events[:20],
1385+
"total": len(tool_events),
1386+
"agent_id": agent_id,
1387+
}
1388+
1389+
except ClientError as e:
1390+
logging.error("get_tool_status failed: %s", e)
1391+
return {"tools": [], "error": str(e)}
1392+
1393+
1394+
@app.delete("/api/speakers/{speaker_id}")
1395+
async def delete_speaker(speaker_id: str):
1396+
"""Delete a speaker from the registry."""
1397+
table = _agent_state_table()
1398+
if not table:
1399+
return JSONResponse(content={"error": "No table configured"}, status_code=400)
1400+
1401+
agent_id = STATE.selected_agent_id or "agent1"
1402+
1403+
try:
1404+
pk = f"AGENT#{agent_id}#VOICE"
1405+
table.delete_item(Key={"pk": pk, "sk": speaker_id})
1406+
return {"success": True, "speaker_id": speaker_id}
1407+
1408+
except ClientError as e:
1409+
logging.error("delete_speaker failed: %s", e)
1410+
return JSONResponse(content={"error": str(e)}, status_code=500)
1411+
1412+
12671413
# ─────────────────────────────────────────────────────────────────────────────
12681414

12691415

client/static/css/panels.css

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/* Speaker Panel Styles */
2+
.speaker-panel { background: #fff; border-radius: 10px; }
3+
.speaker-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
4+
.speaker-panel-header h3 { margin: 0; font-size: 1.1em; }
5+
.speaker-list { max-height: 300px; overflow-y: auto; }
6+
.speaker-item { display: flex; gap: 10px; padding: 8px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: background 0.2s; }
7+
.speaker-item:hover { background: #f5f5f5; }
8+
.speaker-item.selected { background: #e3f2fd; border-color: #90caf9; }
9+
.speaker-avatar { width: 36px; height: 36px; border-radius: 50%; background: #90caf9; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: bold; }
10+
.speaker-info { flex: 1; }
11+
.speaker-name { font-weight: 500; }
12+
.speaker-meta { font-size: 0.85em; color: #666; display: flex; gap: 8px; }
13+
.no-speakers { color: #999; text-align: center; padding: 20px; }
14+
15+
/* Speaker Detail */
16+
.speaker-detail { background: #fafafa; border-radius: 8px; padding: 12px; margin-top: 12px; }
17+
.detail-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
18+
.detail-header h4 { margin: 0; }
19+
.btn-back { background: none; border: none; cursor: pointer; font-size: 1em; padding: 4px 8px; }
20+
.btn-back:hover { background: #eee; border-radius: 4px; }
21+
.detail-section { margin-bottom: 16px; }
22+
.detail-section h5 { margin: 0 0 8px 0; font-size: 0.95em; color: #555; }
23+
.detail-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; font-size: 0.9em; }
24+
25+
/* Enrollment Modal */
26+
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
27+
.modal-content { background: #fff; border-radius: 12px; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; }
28+
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #eee; }
29+
.modal-header h3 { margin: 0; }
30+
.btn-close { background: none; border: none; font-size: 1.5em; cursor: pointer; color: #666; }
31+
.modal-body { padding: 16px; }
32+
.modal-footer { padding: 16px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 10px; }
33+
.form-group { margin-bottom: 16px; }
34+
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; }
35+
.form-group input[type="text"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; }
36+
.recording-controls { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
37+
.recording-status { color: #e53935; font-size: 0.9em; }
38+
.recording-status.active { animation: pulse 1s infinite; }
39+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
40+
.samples-list { display: flex; flex-direction: column; gap: 8px; }
41+
.sample-item { display: flex; align-items: center; gap: 10px; padding: 8px; background: #f5f5f5; border-radius: 8px; }
42+
.sample-item audio { height: 30px; flex: 1; }
43+
.progress-bar { height: 8px; background: #eee; border-radius: 4px; overflow: hidden; margin-bottom: 6px; }
44+
.progress-fill { height: 100%; background: #4caf50; transition: width 0.3s; }
45+
46+
/* Memory Panel Styles */
47+
.memory-panel { background: #fff; border-radius: 10px; }
48+
.memory-panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
49+
.memory-panel-header h3 { margin: 0; font-size: 1.1em; }
50+
.memory-filters { display: flex; gap: 10px; margin-bottom: 12px; }
51+
.memory-filters select, .memory-filters input { padding: 8px; border: 1px solid #ddd; border-radius: 8px; }
52+
.memory-filters input { flex: 1; }
53+
.memory-stats { font-size: 0.85em; color: #666; margin-bottom: 10px; display: flex; gap: 12px; }
54+
.memory-list { max-height: 400px; overflow-y: auto; }
55+
56+
/* Memory Items */
57+
.memory-item { padding: 10px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
58+
.memory-item:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
59+
.memory-header { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; flex-wrap: wrap; }
60+
.memory-text { font-size: 0.95em; line-height: 1.4; }
61+
.memory-ts { font-size: 0.8em; color: #999; margin-top: 6px; }
62+
63+
/* Kind colors */
64+
.memory-kind-badge { padding: 2px 8px; border-radius: 12px; font-size: 0.75em; font-weight: 500; }
65+
.memory-kind-badge.fact, .memory-item.fact { background: #e3f2fd; }
66+
.memory-kind-badge.preference, .memory-item.preference { background: #fff3e0; }
67+
.memory-kind-badge.ai-insight, .memory-item.ai-insight { background: #f3e5f5; }
68+
.memory-kind-badge.relationship, .memory-item.relationship { background: #e8f5e9; }
69+
.memory-kind-badge.action, .memory-item.action { background: #fce4ec; }
70+
.memory-kind-badge.meta, .memory-item.meta { background: #f5f5f5; }
71+
.speaker-badge { font-size: 0.8em; color: #666; }
72+
.importance-badge { font-size: 0.75em; padding: 2px 6px; background: #ff9800; color: #fff; border-radius: 10px; }
73+
74+
/* Badge status colors */
75+
.badge { padding: 2px 8px; border-radius: 12px; font-size: 0.75em; }
76+
.badge.enrolled { background: #c8e6c9; color: #2e7d32; }
77+
.badge.pending { background: #fff9c4; color: #f57f17; }
78+
.badge.unknown { background: #eeeeee; color: #616161; }
79+
80+
/* Common */
81+
.btn { padding: 8px 14px; border: 1px solid #ddd; border-radius: 8px; background: #fff; cursor: pointer; transition: all 0.2s; }
82+
.btn:hover { border-color: #999; }
83+
.btn.btn-sm { padding: 4px 10px; font-size: 0.9em; }
84+
.btn.btn-primary { background: #1976d2; color: #fff; border-color: #1976d2; }
85+
.btn.btn-primary:hover { background: #1565c0; }
86+
.btn.btn-primary:disabled { background: #ccc; border-color: #ccc; cursor: not-allowed; }
87+
.btn.recording { background: #e53935; color: #fff; border-color: #e53935; }
88+
.no-data { color: #999; text-align: center; padding: 20px; }

0 commit comments

Comments
 (0)