import gradio as gr import pandas as pd from pymongo import MongoClient from typing import List, Dict, Any, Optional, Tuple from datetime import datetime import json import re class ConversationAnalysisUI: """Gradio UI for displaying conversation analysis results.""" def __init__(self): # Use keshavchhaparia MongoDB instance (same as RAG system) self.mongodb_uri = "mongodb+srv://keshavchhaparia:bUSBXeVCGWDyQhDG@saaslabs.awtivxf.mongodb.net/" self.database_name = "second_brain_course" self.collection_name = "test_intercom_data" self.setup_mongodb() self.setup_ui() def setup_mongodb(self): """Initialize MongoDB connection.""" try: self.client = MongoClient(self.mongodb_uri) self.db = self.client[self.database_name] self.collection = self.db[self.collection_name] print(f"✅ Connected to MongoDB: {self.database_name}.{self.collection_name}") except Exception as e: print(f"❌ MongoDB connection failed: {e}") raise def load_conversations(self, quality_min: float = 0.0, quality_max: float = 1.0, sentiment: str = "All", search_text: str = "", limit: int = 100) -> pd.DataFrame: """Load and filter conversations.""" try: # Build query query = { 'conversation_analysis': {'$exists': True, '$ne': None}, 'content_quality_score': {'$gte': quality_min, '$lte': quality_max} } # Add sentiment filter if sentiment != "All": query['conversation_analysis.aggregated_marketing_insights.quotes.sentiment'] = sentiment # Add text search if search_text: query['$or'] = [ {'content': {'$regex': search_text, '$options': 'i'}}, {'conversation_analysis.aggregated_contextual_summary': {'$regex': search_text, '$options': 'i'}} ] # Fetch documents docs = list(self.collection.find(query).limit(limit)) # Convert to DataFrame data = [] seen_conversation_ids = set() for doc in docs: conversation_id = doc.get('metadata', {}).get('properties', {}).get('conversation_id', 'N/A') # Skip duplicates if conversation_id in seen_conversation_ids: continue seen_conversation_ids.add(conversation_id) analysis = doc.get('conversation_analysis', {}) insights = analysis.get('aggregated_marketing_insights', {}) quotes = insights.get('quotes', []) # Extract primary sentiment primary_sentiment = quotes[0].get('sentiment', 'Unknown') if quotes else 'Unknown' # Format date created_at = analysis.get('created_at', '') if isinstance(created_at, str): try: # Parse and format date dt = datetime.fromisoformat(created_at.replace('Z', '+00:00')) formatted_date = dt.strftime('%b %d, %Y %H:%M') except: formatted_date = created_at elif hasattr(created_at, 'strftime'): formatted_date = created_at.strftime('%b %d, %Y %H:%M') else: formatted_date = str(created_at) # Get full summary without truncation full_summary = analysis.get('aggregated_contextual_summary', 'No summary available') # Get a simple insights summary for the table marketing_insights = analysis.get('aggregated_marketing_insights', {}) insights_count = 0 if isinstance(marketing_insights, dict): quotes_count = len(marketing_insights.get('quotes', [])) findings_count = len(marketing_insights.get('key_findings', [])) insights_count = quotes_count + findings_count insights_text = f"{insights_count} insights available" if insights_count > 0 else "No insights available" data.append({ 'conversation_id': conversation_id, 'quality_score': round(doc.get('content_quality_score', 0.0), 2), 'sentiment': primary_sentiment, 'summary': full_summary, 'insights': insights_text, 'date': formatted_date }) return pd.DataFrame(data) except Exception as e: print(f"❌ Error loading conversations: {e}") return pd.DataFrame() def get_conversation_details(self, conversation_id: str) -> str: """Get detailed analysis for a specific conversation.""" try: doc = self.collection.find_one({ 'metadata.properties.conversation_id': conversation_id, 'conversation_analysis': {'$exists': True} }) if not doc: return "
❌ Conversation not found
" analysis = doc.get('conversation_analysis', {}) insights = analysis.get('aggregated_marketing_insights', {}) # Format the HTML content html_content = f"""{analysis.get('aggregated_contextual_summary', 'No summary available')}
Quote {i}: "{quote.get('quote', '')}"
Context: {quote.get('context', '')}
Sentiment: {quote.get('sentiment', 'Unknown')}
Finding {i}: {finding.get('finding', '')}
Evidence: {finding.get('evidence', '')}
Impact: {finding.get('impact', 'Unknown')}
{follow_up_email}
❌ Error loading conversation details: {e}
" def setup_ui(self): """Setup the Gradio interface.""" with gr.Blocks( title="Conversation Analysis Dashboard", theme=gr.themes.Soft(), css=""" .conversation-details { max-width: 100%; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: white; color: #333; } .section { margin: 20px 0; padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #ffffff; color: #333; } .content-box { background-color: #f8f9fa; padding: 15px; border-radius: 5px; border: 1px solid #dee2e6; margin: 10px 0; color: #333; } .quote-item, .finding-item { margin: 10px 0; padding: 10px; background-color: #f8f9fa; border-radius: 5px; border-left: 4px solid #007bff; color: #333; } .sentiment-positive { background-color: #d4edda; color: #155724; padding: 2px 8px; border-radius: 4px; font-weight: bold; display: inline-block; } .sentiment-negative { background-color: #f8d7da; color: #721c24; padding: 2px 8px; border-radius: 4px; font-weight: bold; display: inline-block; } .sentiment-neutral { background-color: #d1ecf1; color: #0c5460; padding: 2px 8px; border-radius: 4px; font-weight: bold; display: inline-block; } .sentiment-confused { background-color: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 4px; font-weight: bold; display: inline-block; } .impact-high { background-color: #f8d7da; color: #721c24; padding: 2px 8px; border-radius: 4px; font-weight: bold; display: inline-block; } .impact-medium { background-color: #fff3cd; color: #856404; padding: 2px 8px; border-radius: 4px; font-weight: bold; display: inline-block; } .impact-low { background-color: #d4edda; color: #155724; padding: 2px 8px; border-radius: 4px; font-weight: bold; display: inline-block; } .quality-high { color: #28a745; font-weight: bold; } .quality-medium { color: #ffc107; font-weight: bold; } .quality-low { color: #dc3545; font-weight: bold; } """ ) as self.interface: gr.Markdown("# 🎯 Conversation Analysis Dashboard") gr.Markdown("Analyze customer conversations with AI-powered insights, summaries, and follow-up emails.") # Filters with gr.Row(): with gr.Column(scale=2): quality_range = gr.Slider( minimum=0.0, maximum=1.0, value=[0.0, 1.0], label="Quality Score Range", step=0.01 ) with gr.Column(scale=1): sentiment_filter = gr.Dropdown( choices=["All", "Positive", "Negative", "Neutral", "Confused"], value="All", label="Sentiment Filter" ) with gr.Column(scale=1): search_text = gr.Textbox( placeholder="Search conversations...", label="Search" ) with gr.Column(scale=1): refresh_btn = gr.Button("🔄 Refresh", variant="primary") # Main table with gr.Row(): conversations_df = gr.Dataframe( headers=["Conversation ID", "Quality", "Sentiment", "Summary", "Insights Count", "Date"], datatype=["str", "number", "str", "str", "str", "str"], interactive=False, label="Conversations", wrap=True, # Enable text wrapping max_height=600 # Set max height for scrolling ) # Detail view with gr.Row(): with gr.Column(): detail_view = gr.HTML( value="Select a conversation from the table above to view detailed analysis
", label="Conversation Details" ) # Event handlers def refresh_data(quality_range, sentiment, search): if isinstance(quality_range, (list, tuple)) and len(quality_range) == 2: quality_min, quality_max = quality_range else: quality_min, quality_max = 0.0, 1.0 df = self.load_conversations(quality_min, quality_max, sentiment, search, limit=1000) return df def on_table_select(evt: gr.SelectData): if evt.index[0] is not None: try: # Get the conversation ID from the selected row # We need to get the current dataframe from the table current_df = self.load_conversations() if not current_df.empty and evt.index[0] < len(current_df): conversation_id = current_df.iloc[evt.index[0]]['conversation_id'] return self.get_conversation_details(conversation_id) else: return "Please refresh the data first
" except Exception as e: return f"Error: {e}
" return "Please select a conversation from the table
" refresh_btn.click( fn=refresh_data, inputs=[quality_range, sentiment_filter, search_text], outputs=[conversations_df] ) conversations_df.select( fn=on_table_select, outputs=[detail_view] ) # Load initial data when the page loads def load_initial_data(): return self.load_conversations(limit=1000) # Load more conversations # Set initial data using the interface's load event self.interface.load(load_initial_data, outputs=[conversations_df]) def launch(self, **kwargs): """Launch the Gradio interface.""" self.interface.launch(**kwargs)