navigation / main.py
MFF212's picture
Update main.py
2520301 verified
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
@app.websocket("/ws/{client_id}")
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
@app.get("/transcripts")
async def get_transcripts():
"""Get all stored transcripts"""
return {
"transcripts": manager.get_transcripts(),
"count": len(manager.get_transcripts()),
"timestamp": datetime.now().isoformat()
}
@app.delete("/transcripts")
async def clear_transcripts():
"""Clear all stored transcripts"""
manager.clear_transcripts()
return {
"message": "All transcripts cleared",
"timestamp": datetime.now().isoformat()
}
@app.get("/transcripts/count")
async def get_transcript_count():
"""Get the count of stored transcripts"""
return {
"count": len(manager.get_transcripts()),
"timestamp": datetime.now().isoformat()
}
# Health check endpoint
@app.get("/health")
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
@app.get("/", response_class=HTMLResponse)
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>
"""