Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse | |
| import json | |
| import logging | |
| from typing import Dict, List | |
| from datetime import datetime | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI(title="Transcript WebSocket Backend", version="1.0.0") | |
| # Add CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], # In production, specify your frontend URL | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Store active connections and transcripts | |
| class ConnectionManager: | |
| def __init__(self): | |
| self.active_connections: List[WebSocket] = [] | |
| self.transcripts: List[Dict] = [] | |
| async def connect(self, websocket: WebSocket): | |
| await websocket.accept() | |
| self.active_connections.append(websocket) | |
| logger.info(f"Client connected. Total connections: {len(self.active_connections)}") | |
| def disconnect(self, websocket: WebSocket): | |
| if websocket in self.active_connections: | |
| self.active_connections.remove(websocket) | |
| logger.info(f"Client disconnected. Total connections: {len(self.active_connections)}") | |
| async def send_personal_message(self, message: str, websocket: WebSocket): | |
| try: | |
| await websocket.send_text(message) | |
| except Exception as e: | |
| logger.error(f"Error sending message: {e}") | |
| self.disconnect(websocket) | |
| async def broadcast(self, message: str): | |
| disconnected = [] | |
| for connection in self.active_connections: | |
| try: | |
| await connection.send_text(message) | |
| except Exception as e: | |
| logger.error(f"Error broadcasting message: {e}") | |
| disconnected.append(connection) | |
| # Remove disconnected connections | |
| for connection in disconnected: | |
| self.disconnect(connection) | |
| def add_transcript(self, transcript_data: Dict): | |
| """Add a transcript entry with timestamp""" | |
| transcript_entry = { | |
| "timestamp": datetime.now().isoformat(), | |
| "type": transcript_data.get("type", "unknown"), | |
| "data": transcript_data.get("data", {}), | |
| "raw": transcript_data | |
| } | |
| self.transcripts.append(transcript_entry) | |
| logger.info(f"Added transcript: {transcript_entry['type']}") | |
| def get_transcripts(self) -> List[Dict]: | |
| """Get all transcripts""" | |
| return self.transcripts | |
| def clear_transcripts(self): | |
| """Clear all transcripts""" | |
| self.transcripts.clear() | |
| logger.info("Cleared all transcripts") | |
| manager = ConnectionManager() | |
| # WebSocket endpoint | |
| async def websocket_endpoint(websocket: WebSocket, client_id: str): | |
| await manager.connect(websocket) | |
| try: | |
| while True: | |
| # Receive message from frontend | |
| data = await websocket.receive_text() | |
| message = json.loads(data) | |
| logger.info(f"Received from client {client_id}: {message}") | |
| # Handle different message types | |
| message_type = message.get("type", "unknown") | |
| if message_type in ["ontranscript", "onresponsetext", "onfunction", "onerror", "onclose", "onopen", "onready", "onsessionended"]: | |
| # Store transcript/response data | |
| manager.add_transcript(message) | |
| # Broadcast to all connected clients (for real-time viewing) | |
| await manager.broadcast(json.dumps({ | |
| "type": "transcript_update", | |
| "data": { | |
| "timestamp": datetime.now().isoformat(), | |
| "message_type": message_type, | |
| "content": message.get("data", {}), | |
| "client_id": client_id | |
| } | |
| })) | |
| # Send acknowledgment back to sender | |
| await manager.send_personal_message( | |
| json.dumps({ | |
| "type": "acknowledgment", | |
| "data": { | |
| "status": "received", | |
| "message_type": message_type, | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| }), | |
| websocket | |
| ) | |
| elif message_type == "get_transcripts": | |
| # Send all stored transcripts | |
| await manager.send_personal_message( | |
| json.dumps({ | |
| "type": "transcripts_data", | |
| "data": { | |
| "transcripts": manager.get_transcripts(), | |
| "count": len(manager.get_transcripts()) | |
| } | |
| }), | |
| websocket | |
| ) | |
| elif message_type == "clear_transcripts": | |
| # Clear all transcripts | |
| manager.clear_transcripts() | |
| await manager.broadcast(json.dumps({ | |
| "type": "transcripts_cleared", | |
| "data": { | |
| "timestamp": datetime.now().isoformat(), | |
| "message": "All transcripts cleared" | |
| } | |
| })) | |
| else: | |
| # Handle unknown message types | |
| logger.warning(f"Unknown message type: {message_type}") | |
| await manager.send_personal_message( | |
| json.dumps({ | |
| "type": "error", | |
| "data": { | |
| "message": f"Unknown message type: {message_type}", | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| }), | |
| websocket | |
| ) | |
| except WebSocketDisconnect: | |
| logger.info(f"Client {client_id} disconnected") | |
| manager.disconnect(websocket) | |
| except Exception as e: | |
| logger.error(f"Error in WebSocket connection: {e}") | |
| manager.disconnect(websocket) | |
| # REST API endpoints for transcript management | |
| async def get_transcripts(): | |
| """Get all stored transcripts""" | |
| return { | |
| "transcripts": manager.get_transcripts(), | |
| "count": len(manager.get_transcripts()), | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| async def clear_transcripts(): | |
| """Clear all stored transcripts""" | |
| manager.clear_transcripts() | |
| return { | |
| "message": "All transcripts cleared", | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| async def get_transcript_count(): | |
| """Get the count of stored transcripts""" | |
| return { | |
| "count": len(manager.get_transcripts()), | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| # Health check endpoint | |
| async def health_check(): | |
| return { | |
| "status": "healthy", | |
| "connections": len(manager.active_connections), | |
| "transcripts_count": len(manager.get_transcripts()), | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| # Root endpoint with real-time transcript viewer | |
| async def root(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>AirFGPL Transcripts</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <style> | |
| :root { | |
| --primary-color: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --success-color: #10b981; | |
| --error-color: #ef4444; | |
| --warning-color: #f59e0b; | |
| --background: #ffffff; | |
| --surface: #f8fafc; | |
| --border: #e2e8f0; | |
| --text-primary: #1e293b; | |
| --text-secondary: #64748b; | |
| --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); | |
| --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: var(--text-primary); | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 100%; | |
| margin: 0 auto; | |
| padding: 1rem; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 1rem; | |
| color: white; | |
| } | |
| .header h1 { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| } | |
| .status-bar { | |
| background: var(--background); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| box-shadow: var(--shadow-lg); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| } | |
| .status { | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .connected { | |
| background-color: var(--success-color); | |
| color: white; | |
| } | |
| .disconnected { | |
| background-color: var(--error-color); | |
| color: white; | |
| } | |
| .transcripts { | |
| flex: 1; | |
| background: var(--background); | |
| border-radius: 12px; | |
| padding: 1rem; | |
| box-shadow: var(--shadow-lg); | |
| overflow-y: auto; | |
| max-height: calc(100vh - 300px); | |
| } | |
| .transcript-item { | |
| margin: 0.75rem 0; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| border-left: 4px solid var(--primary-color); | |
| background: var(--surface); | |
| transition: all 0.3s ease; | |
| } | |
| .transcript-item:hover { | |
| transform: translateX(4px); | |
| box-shadow: var(--shadow); | |
| } | |
| .transcript-item.user { | |
| border-left-color: var(--success-color); | |
| background: rgba(16, 185, 129, 0.1); | |
| } | |
| .transcript-item.agent { | |
| border-left-color: var(--primary-color); | |
| background: rgba(99, 102, 241, 0.1); | |
| } | |
| .transcript-item.error { | |
| border-left-color: var(--error-color); | |
| background: rgba(239, 68, 68, 0.1); | |
| } | |
| .transcript-item.function { | |
| border-left-color: var(--warning-color); | |
| background: rgba(245, 158, 11, 0.1); | |
| } | |
| .timestamp { | |
| font-size: 0.75rem; | |
| color: var(--text-secondary); | |
| margin-bottom: 0.5rem; | |
| font-weight: 500; | |
| } | |
| .type { | |
| font-weight: 700; | |
| margin-bottom: 0.5rem; | |
| color: var(--text-primary); | |
| font-size: 0.9rem; | |
| } | |
| .content { | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| font-size: 0.85rem; | |
| line-height: 1.5; | |
| color: var(--text-primary); | |
| background: rgba(255, 255, 255, 0.5); | |
| padding: 0.5rem; | |
| border-radius: 4px; | |
| border: 1px solid var(--border); | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-secondary); | |
| font-style: italic; | |
| } | |
| /* Mobile Optimizations */ | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 0.5rem; | |
| } | |
| .header h1 { | |
| font-size: 1.25rem; | |
| } | |
| .status-bar { | |
| flex-direction: column; | |
| align-items: stretch; | |
| gap: 0.75rem; | |
| } | |
| .status { | |
| justify-content: center; | |
| } | |
| .transcripts { | |
| max-height: calc(100vh - 300px); | |
| padding: 0.75rem; | |
| } | |
| .transcript-item { | |
| margin: 0.5rem 0; | |
| padding: 0.75rem; | |
| } | |
| .content { | |
| font-size: 0.8rem; | |
| padding: 0.5rem; | |
| } | |
| #destination-iframe { | |
| height: 300px; | |
| } | |
| } | |
| /* Scrollbar Styling */ | |
| .transcripts::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .transcripts::-webkit-scrollbar-track { | |
| background: var(--surface); | |
| border-radius: 3px; | |
| } | |
| .transcripts::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 3px; | |
| } | |
| .transcripts::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-secondary); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>π€ AirFGPL Transcripts</h1> | |
| </div> | |
| <div class="status-bar"> | |
| <div id="status" class="status disconnected"> | |
| <span>π΄</span> | |
| <span>Disconnected</span> | |
| </div> | |
| </div> | |
| <div id="transcripts" class="transcripts"> | |
| <div class="empty-state"> | |
| <p>π No transcripts yet</p> | |
| <p>Connect your AirFGPL app to see real-time updates</p> | |
| </div> | |
| </div> | |
| <!-- Destination iframe --> | |
| <div id="destination-container" style="display: none; margin-top: 1rem;"> | |
| <h3 style="color: white; margin-bottom: 0.5rem;">π― Destination Guide</h3> | |
| <iframe | |
| id="destination-iframe" | |
| style="width: 100%; height: 400px; border: none; border-radius: 12px; background: white;" | |
| title="Destination Guide" | |
| ></iframe> | |
| </div> | |
| </div> | |
| <script> | |
| const ws = new WebSocket('ws://localhost:8000/ws/viewer'); | |
| const statusDiv = document.getElementById('status'); | |
| const transcriptsDiv = document.getElementById('transcripts'); | |
| ws.onopen = function() { | |
| statusDiv.innerHTML = '<span>π’</span><span>Connected</span>'; | |
| statusDiv.className = 'status connected'; | |
| }; | |
| ws.onmessage = function(event) { | |
| const message = JSON.parse(event.data); | |
| if (message.type === 'transcript_update') { | |
| addTranscriptItem(message.data); | |
| } else if (message.type === 'transcripts_data') { | |
| displayTranscripts(message.data.transcripts); | |
| } else if (message.type === 'transcripts_cleared') { | |
| transcriptsDiv.innerHTML = '<div class="empty-state"><p>ποΈ All transcripts cleared</p></div>'; | |
| } | |
| }; | |
| ws.onclose = function() { | |
| statusDiv.innerHTML = '<span>π΄</span><span>Disconnected</span>'; | |
| statusDiv.className = 'status disconnected'; | |
| }; | |
| function addTranscriptItem(data) { | |
| const item = document.createElement('div'); | |
| item.className = `transcript-item ${getTypeClass(data.message_type)}`; | |
| const timestamp = new Date(data.timestamp).toLocaleTimeString(); | |
| const type = data.message_type.replace('on', '').toUpperCase(); | |
| item.innerHTML = ` | |
| <div class="timestamp">[${timestamp}] Client: ${data.client_id}</div> | |
| <div class="type">${type}</div> | |
| <div class="content">${JSON.stringify(data.content, null, 2)}</div> | |
| `; | |
| transcriptsDiv.appendChild(item); | |
| transcriptsDiv.scrollTop = transcriptsDiv.scrollHeight; | |
| // Check for "guide you to the" in agent responses | |
| if (data.message_type === 'onresponsetext' && data.content && data.content.text) { | |
| const text = data.content.text.toLowerCase(); | |
| console.log(`π Checking text: "${text}"`); | |
| if (text.includes('guide you to the')) { | |
| console.log(`β Found "guide you to the" in text`); | |
| const match = text.match(/guide you to the\s+(\w+)/i); | |
| if (match) { | |
| const nextWord = match[1]; | |
| console.log(`π― Detected destination: ${nextWord}`); | |
| loadDestinationInIframe(nextWord); | |
| } else { | |
| console.log(`β No destination word found after "guide you to the"`); | |
| // Try alternative pattern for "medical center" | |
| const altMatch = text.match(/guide you to the\s+(\w+\s+\w+)/i); | |
| if (altMatch) { | |
| const altWord = altMatch[1].split(' ')[0]; // Take first word | |
| console.log(`π― Detected alternative destination: ${altWord}`); | |
| loadDestinationInIframe(altWord); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function displayTranscripts(transcripts) { | |
| transcriptsDiv.innerHTML = ''; | |
| if (transcripts.length === 0) { | |
| transcriptsDiv.innerHTML = '<div class="empty-state"><p>π No transcripts available</p></div>'; | |
| return; | |
| } | |
| transcripts.forEach(transcript => { | |
| const item = document.createElement('div'); | |
| item.className = `transcript-item ${getTypeClass(transcript.type)}`; | |
| const timestamp = new Date(transcript.timestamp).toLocaleTimeString(); | |
| const type = transcript.type.replace('on', '').toUpperCase(); | |
| item.innerHTML = ` | |
| <div class="timestamp">[${timestamp}]</div> | |
| <div class="type">${type}</div> | |
| <div class="content">${JSON.stringify(transcript.data, null, 2)}</div> | |
| `; | |
| transcriptsDiv.appendChild(item); | |
| }); | |
| transcriptsDiv.scrollTop = transcriptsDiv.scrollHeight; | |
| } | |
| function getTypeClass(type) { | |
| if (type.includes('transcript')) return 'user'; | |
| if (type.includes('responsetext')) return 'agent'; | |
| if (type.includes('function')) return 'function'; | |
| if (type.includes('error')) return 'error'; | |
| return ''; | |
| } | |
| function loadDestinationInIframe(destination) { | |
| const container = document.getElementById('destination-container'); | |
| const iframe = document.getElementById('destination-iframe'); | |
| if (container && iframe) { | |
| const url = `http://13.126.242.31:8080/?source=gate-a1&destination=${encodeURIComponent(destination)}`; | |
| iframe.src = url; | |
| container.style.display = 'block'; | |
| console.log(`π Loading destination: ${url}`); | |
| // Scroll to the iframe | |
| container.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |