Spaces:
Sleeping
Sleeping
""" | |
GlycoAI - AI-Powered Glucose Insights | |
Main Gradio application with both demo users AND real Dexcom OAuth | |
""" | |
import gradio as gr | |
import plotly.graph_objects as go | |
import plotly.express as px | |
from datetime import datetime, timedelta | |
import pandas as pd | |
from typing import Optional, Tuple, List | |
import logging | |
import os | |
import webbrowser | |
import urllib.parse | |
# Load environment variables from .env file | |
from dotenv import load_dotenv | |
load_dotenv() | |
# Import the Mistral chat class and unified data manager | |
from mistral_chat import GlucoBuddyMistralChat, validate_environment | |
from unified_data_manager import UnifiedDataManager | |
# Setup logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# Import our custom functions | |
from apifunctions import ( | |
DexcomAPI, | |
GlucoseAnalyzer, | |
DEMO_USERS, | |
format_glucose_data_for_display | |
) | |
# Import real Dexcom OAuth (now working!) | |
try: | |
from dexcom_real_auth_system import DexcomRealAPI | |
REAL_OAUTH_AVAILABLE = True | |
logger.info("β Real Dexcom OAuth available") | |
except ImportError as e: | |
REAL_OAUTH_AVAILABLE = False | |
logger.warning(f"β οΈ Real Dexcom OAuth not available: {e}") | |
class GlucoBuddyApp: | |
"""Main application class for GlucoBuddy with demo users AND real OAuth""" | |
def __init__(self): | |
# Validate environment before initializing | |
if not validate_environment(): | |
raise ValueError("Environment validation failed - check your .env file or environment variables") | |
# Single data manager for consistency | |
self.data_manager = UnifiedDataManager() | |
# Chat interface (will use data manager's context) | |
self.mistral_chat = GlucoBuddyMistralChat() | |
# Real OAuth API (if available) | |
self.real_api = DexcomRealAPI(environment="sandbox") if REAL_OAUTH_AVAILABLE else None | |
# UI state | |
self.chat_history = [] | |
self.current_user_type = None # "demo" or "real" | |
def select_demo_user(self, user_key: str) -> Tuple[str, str]: | |
"""Handle demo user selection and load data consistently""" | |
if user_key not in DEMO_USERS: | |
return "β Invalid user selection", gr.update(visible=False) | |
try: | |
# Load data through unified manager | |
load_result = self.data_manager.load_user_data(user_key) | |
if not load_result['success']: | |
return f"β {load_result['message']}", gr.update(visible=False) | |
user = self.data_manager.current_user | |
self.current_user_type = "demo" | |
# Update Mistral chat with the same context | |
self._sync_chat_with_data_manager() | |
# Clear chat history when switching users | |
self.chat_history = [] | |
self.mistral_chat.clear_conversation() | |
return ( | |
f"Connected: {user.name} ({user.device_type}) - DEMO DATA - Click 'Load Data' to begin", | |
gr.update(visible=True) | |
) | |
except Exception as e: | |
logger.error(f"Demo user selection failed: {str(e)}") | |
return f"β Connection failed: {str(e)}", gr.update(visible=False) | |
def start_real_oauth(self) -> str: | |
"""Start real Dexcom OAuth process""" | |
if not REAL_OAUTH_AVAILABLE: | |
return """ | |
β **Real Dexcom OAuth Not Available** | |
The real authentication module is not properly configured. | |
Please ensure: | |
1. dexcom_real_auth_system.py exists and imports correctly | |
2. You have valid Dexcom developer credentials | |
3. All dependencies are installed | |
For now, please use the demo users above for instant access to realistic glucose data. | |
""" | |
try: | |
# Start OAuth flow | |
success = self.real_api.start_oauth_flow() | |
if success: | |
return f""" | |
π **Real Dexcom OAuth Started** | |
**SIMPLIFIED PROCESS FOR PORT 7860:** | |
1. β Browser should have opened to Dexcom login page | |
2. π Log in with your **real Dexcom account credentials** | |
- For sandbox testing: `[email protected]` / `Dexcom123!` | |
3. β Authorize GlycoAI to access your data | |
4. β **You will get a 404 error - THIS IS EXPECTED!** | |
5. π **Copy ONLY the authorization code** from the URL | |
**Example callback URL:** | |
`http://localhost:7860/callback?code=ABC123XYZ&state=...` | |
**Copy just this part:** `ABC123XYZ` | |
**Why simpler?** Your token generator script works perfectly with just the code, so we're using the same approach! | |
""" | |
else: | |
return "β Failed to start OAuth process. Check console for details." | |
except Exception as e: | |
logger.error(f"OAuth start error: {e}") | |
return f"β OAuth error: {str(e)}" | |
def complete_real_oauth(self, auth_code_input: str) -> Tuple[str, str]: | |
"""Complete real OAuth with authorization code (like the working script)""" | |
if not REAL_OAUTH_AVAILABLE: | |
return "β Real OAuth not available", gr.update(visible=False) | |
if not auth_code_input or not auth_code_input.strip(): | |
return "β Please paste the authorization code", gr.update(visible=False) | |
try: | |
# Clean up the input - handle both full URLs and just codes | |
auth_code = self._extract_auth_code(auth_code_input.strip()) | |
if not auth_code: | |
return "β No authorization code found in input", gr.update(visible=False) | |
logger.info(f"Processing authorization code: {auth_code[:20]}...") | |
# Use the same method as the working script - direct token exchange | |
success = self.real_api.exchange_code_for_tokens(auth_code) | |
if success: | |
logger.info("β Token exchange successful") | |
# Load real data into data manager | |
real_data_result = self._load_real_dexcom_data() | |
if real_data_result['success']: | |
self.current_user_type = "real" | |
# Update chat context | |
self._sync_chat_with_data_manager() | |
# Clear chat history for new user | |
self.chat_history = [] | |
self.mistral_chat.clear_conversation() | |
return ( | |
f"β Connected: Real Dexcom User - LIVE DATA - Click 'Load Data' to begin", | |
gr.update(visible=True) | |
) | |
else: | |
return f"β Data loading failed: {real_data_result['message']}", gr.update(visible=False) | |
else: | |
return "β Token exchange failed - check the authorization code", gr.update(visible=False) | |
except Exception as e: | |
logger.error(f"OAuth completion error: {e}") | |
return f"β OAuth completion failed: {str(e)}", gr.update(visible=False) | |
def _extract_auth_code(self, input_text: str) -> str: | |
"""Extract authorization code from various input formats""" | |
try: | |
# If it's a full URL, parse it | |
if input_text.startswith('http'): | |
parsed_url = urllib.parse.urlparse(input_text) | |
query_params = urllib.parse.parse_qs(parsed_url.query) | |
if 'code' in query_params: | |
return query_params['code'][0] | |
else: | |
logger.warning(f"No 'code' parameter found in URL: {input_text}") | |
return "" | |
else: | |
# Assume it's just the authorization code | |
# Remove any "code=" prefix if present | |
if input_text.startswith('code='): | |
return input_text[5:] | |
else: | |
return input_text | |
except Exception as e: | |
logger.error(f"Error extracting auth code: {e}") | |
return "" | |
def _load_real_dexcom_data(self) -> dict: | |
"""Load real Dexcom data through the unified data manager""" | |
try: | |
# Get data range | |
data_range = self.real_api.get_data_range() | |
# Get glucose data (last 14 days) | |
end_time = datetime.now() | |
start_time = end_time - timedelta(days=14) | |
egv_data = self.real_api.get_egv_data( | |
start_date=start_time.isoformat(), | |
end_date=end_time.isoformat() | |
) | |
# Get events data | |
events_data = self.real_api.get_events_data( | |
start_date=start_time.isoformat(), | |
end_date=end_time.isoformat() | |
) | |
# Create a real user profile | |
from dataclasses import dataclass | |
class RealDexcomUser: | |
name: str = "Real Dexcom User" | |
age: int = 0 | |
device_type: str = "Real Dexcom Device" | |
username: str = "authenticated_user" | |
password: str = "oauth_token" | |
description: str = "Authenticated real Dexcom user with live data" | |
diabetes_type: str = "Real Patient" | |
years_with_diabetes: int = 0 | |
typical_glucose_pattern: str = "real_data" | |
real_user = RealDexcomUser() | |
# Process the real data through unified data manager | |
# Convert to format expected by data manager | |
formatted_data = { | |
"data_range": data_range, | |
"egv_data": egv_data, | |
"events_data": events_data, | |
"source": "real_dexcom_api" | |
} | |
# Load into data manager | |
self.data_manager.current_user = real_user | |
self.data_manager.processed_glucose_data = pd.DataFrame(egv_data) if egv_data else pd.DataFrame() | |
if not self.data_manager.processed_glucose_data.empty: | |
# Process timestamps | |
self.data_manager.processed_glucose_data['systemTime'] = pd.to_datetime( | |
self.data_manager.processed_glucose_data['systemTime'] | |
) | |
self.data_manager.processed_glucose_data['displayTime'] = pd.to_datetime( | |
self.data_manager.processed_glucose_data['displayTime'] | |
) | |
self.data_manager.processed_glucose_data['value'] = pd.to_numeric( | |
self.data_manager.processed_glucose_data['value'], errors='coerce' | |
) | |
# Calculate stats | |
from apifunctions import GlucoseAnalyzer | |
self.data_manager.calculated_stats = GlucoseAnalyzer.calculate_basic_stats( | |
self.data_manager.processed_glucose_data | |
) | |
self.data_manager.identified_patterns = GlucoseAnalyzer.identify_patterns( | |
self.data_manager.processed_glucose_data | |
) | |
logger.info(f"Loaded real Dexcom data: {len(egv_data)} glucose readings") | |
return { | |
'success': True, | |
'message': f'Loaded {len(egv_data)} real glucose readings' | |
} | |
except Exception as e: | |
logger.error(f"Failed to load real Dexcom data: {e}") | |
return { | |
'success': False, | |
'message': f'Failed to load real data: {str(e)}' | |
} | |
def load_glucose_data(self) -> Tuple[str, go.Figure]: | |
"""Load and display glucose data using unified manager""" | |
if not self.data_manager.current_user: | |
return "Please select a user first (demo or real Dexcom)", None | |
try: | |
# For real users, we already loaded the data during OAuth | |
if self.current_user_type == "real": | |
if self.data_manager.processed_glucose_data.empty: | |
return "No real glucose data available", None | |
else: | |
# For demo users, force reload data to ensure freshness | |
load_result = self.data_manager.load_user_data( | |
self._get_current_user_key(), | |
force_reload=True | |
) | |
if not load_result['success']: | |
return load_result['message'], None | |
# Get unified stats | |
stats = self.data_manager.get_stats_for_ui() | |
chart_data = self.data_manager.get_chart_data() | |
# Sync chat with fresh data | |
self._sync_chat_with_data_manager() | |
if chart_data is None or chart_data.empty: | |
return "No glucose data available", None | |
# Build data summary with CONSISTENT metrics | |
user = self.data_manager.current_user | |
data_points = stats.get('total_readings', 0) | |
avg_glucose = stats.get('average_glucose', 0) | |
std_glucose = stats.get('std_glucose', 0) | |
min_glucose = stats.get('min_glucose', 0) | |
max_glucose = stats.get('max_glucose', 0) | |
time_in_range = stats.get('time_in_range_70_180', 0) | |
time_below_range = stats.get('time_below_70', 0) | |
time_above_range = stats.get('time_above_180', 0) | |
gmi = stats.get('gmi', 0) | |
cv = stats.get('cv', 0) | |
# Calculate date range | |
end_date = datetime.now() | |
start_date = end_date - timedelta(days=14) | |
# Determine data source | |
data_source = "REAL DEXCOM DATA" if self.current_user_type == "real" else "DEMO DATA" | |
data_summary = f""" | |
## π Data Summary for {user.name} | |
### Basic Information | |
β’ **Data Type:** {data_source} | |
β’ **Analysis Period:** {start_date.strftime('%B %d, %Y')} to {end_date.strftime('%B %d, %Y')} (14 days) | |
β’ **Total Readings:** {data_points:,} glucose measurements | |
β’ **Device:** {user.device_type} | |
β’ **Data Source:** {stats.get('data_source', 'unknown').upper()} | |
### Glucose Statistics | |
β’ **Average Glucose:** {avg_glucose:.1f} mg/dL | |
β’ **Standard Deviation:** {std_glucose:.1f} mg/dL | |
β’ **Coefficient of Variation:** {cv:.1f}% | |
β’ **Glucose Range:** {min_glucose:.0f} - {max_glucose:.0f} mg/dL | |
β’ **GMI (Glucose Management Indicator):** {gmi:.1f}% | |
### Time in Range Analysis | |
β’ **Time in Range (70-180 mg/dL):** {time_in_range:.1f}% | |
β’ **Time Below Range (<70 mg/dL):** {time_below_range:.1f}% | |
β’ **Time Above Range (>180 mg/dL):** {time_above_range:.1f}% | |
### Clinical Targets | |
β’ **Target Time in Range:** >70% (Current: {time_in_range:.1f}%) | |
β’ **Target Time Below Range:** <4% (Current: {time_below_range:.1f}%) | |
β’ **Target CV:** <36% (Current: {cv:.1f}%) | |
### Data Validation | |
β’ **In Range Count:** {stats.get('in_range_count', 0)} readings | |
β’ **Below Range Count:** {stats.get('below_range_count', 0)} readings | |
β’ **Above Range Count:** {stats.get('above_range_count', 0)} readings | |
β’ **Total Verified:** {stats.get('in_range_count', 0) + stats.get('below_range_count', 0) + stats.get('above_range_count', 0)} readings | |
### 14-Day Analysis Benefits | |
β’ **Enhanced Pattern Recognition:** Captures full weekly cycles and variations | |
β’ **Improved Trend Analysis:** Identifies consistent patterns vs. one-time events | |
β’ **Better Clinical Insights:** More reliable data for healthcare decisions | |
β’ **AI Consistency:** Same data used for chat analysis and UI display | |
### Authentication Status | |
β’ **User Type:** {self.current_user_type.upper() if self.current_user_type else 'Unknown'} | |
β’ **OAuth Status:** {'β Authenticated with real Dexcom account' if self.current_user_type == 'real' else 'π Using demo data for testing'} | |
""" | |
chart = self.create_glucose_chart() | |
return data_summary, chart | |
except Exception as e: | |
logger.error(f"Failed to load glucose data: {str(e)}") | |
return f"Failed to load glucose data: {str(e)}", None | |
def _sync_chat_with_data_manager(self): | |
"""Ensure chat uses the same data as the UI""" | |
try: | |
# Get context from unified data manager | |
context = self.data_manager.get_context_for_agent() | |
# Update chat's internal data to match | |
if not context.get("error"): | |
self.mistral_chat.current_user = self.data_manager.current_user | |
self.mistral_chat.current_glucose_data = self.data_manager.processed_glucose_data | |
self.mistral_chat.current_stats = self.data_manager.calculated_stats | |
self.mistral_chat.current_patterns = self.data_manager.identified_patterns | |
logger.info(f"Synced chat with data manager - TIR: {self.data_manager.calculated_stats.get('time_in_range_70_180', 0):.1f}%") | |
except Exception as e: | |
logger.error(f"Failed to sync chat with data manager: {e}") | |
def _get_current_user_key(self) -> str: | |
"""Get the current user key""" | |
if not self.data_manager.current_user: | |
return "" | |
# Find the key for current user | |
for key, user in DEMO_USERS.items(): | |
if user == self.data_manager.current_user: | |
return key | |
return "" | |
def get_template_prompts(self) -> List[str]: | |
"""Get template prompts based on current user data""" | |
if not self.data_manager.current_user or not self.data_manager.calculated_stats: | |
return [ | |
"What should I know about managing my diabetes?", | |
"How can I improve my glucose control?" | |
] | |
stats = self.data_manager.calculated_stats | |
time_in_range = stats.get('time_in_range_70_180', 0) | |
time_below_70 = stats.get('time_below_70', 0) | |
templates = [] | |
if time_in_range < 70: | |
templates.append(f"My time in range is {time_in_range:.1f}% which is below the 70% target. What specific strategies can help me improve it?") | |
else: | |
templates.append(f"My time in range is {time_in_range:.1f}% which meets the target. How can I maintain this level of control?") | |
if time_below_70 > 4: | |
templates.append(f"I'm experiencing {time_below_70:.1f}% time below 70 mg/dL. What can I do to prevent these low episodes?") | |
else: | |
templates.append("What are the best practices for preventing hypoglycemia in my situation?") | |
# Add data source specific template | |
if self.current_user_type == "real": | |
templates.append("This is my real Dexcom data. What insights can you provide about my actual glucose patterns?") | |
else: | |
templates.append("Based on this demo data, what would you recommend for someone with similar patterns?") | |
return templates | |
def chat_with_mistral(self, message: str, history: List) -> Tuple[str, List]: | |
"""Handle chat interaction with Mistral using unified data""" | |
if not message.strip(): | |
return "", history | |
if not self.data_manager.current_user: | |
response = "Please select a user first (demo or real Dexcom) to get personalized insights about glucose data." | |
history.append([message, response]) | |
return "", history | |
try: | |
# Ensure chat is synced with latest data | |
self._sync_chat_with_data_manager() | |
# Send message to Mistral chat | |
result = self.mistral_chat.chat_with_mistral(message) | |
if result['success']: | |
response = result['response'] | |
# Add data consistency note | |
validation = self.data_manager.validate_data_consistency() | |
if validation.get('valid'): | |
data_age = validation.get('data_age_minutes', 0) | |
if data_age > 10: # Warn if data is old | |
response += f"\n\nπ *Note: Analysis based on data from {data_age} minutes ago. Reload data for most current insights.*" | |
# Add data source context | |
data_type = "real Dexcom data" if self.current_user_type == "real" else "demo data" | |
if self.current_user_type == "real": | |
response += f"\n\nπ *This analysis is based on your real Dexcom data from your authenticated account.*" | |
else: | |
response += f"\n\nπ *This analysis is based on demo data for testing purposes.*" | |
# Add context note if no user data was included | |
if not result.get('context_included', True): | |
response += f"\n\nπ‘ *For more personalized advice, make sure your glucose data is loaded.*" | |
else: | |
response = f"I apologize, but I encountered an error: {result.get('error', 'Unknown error')}. Please try again or rephrase your question." | |
history.append([message, response]) | |
return "", history | |
except Exception as e: | |
logger.error(f"Chat error: {str(e)}") | |
error_response = f"I apologize, but I encountered an error while processing your question: {str(e)}. Please try rephrasing your question." | |
history.append([message, error_response]) | |
return "", history | |
def use_template_prompt(self, template_text: str) -> str: | |
"""Use a template prompt in the chat""" | |
return template_text | |
def clear_chat_history(self) -> List: | |
"""Clear chat history""" | |
self.chat_history = [] | |
self.mistral_chat.clear_conversation() | |
return [] | |
def create_glucose_chart(self) -> Optional[go.Figure]: | |
"""Create an interactive glucose chart using unified data""" | |
chart_data = self.data_manager.get_chart_data() | |
if chart_data is None or chart_data.empty: | |
return None | |
fig = go.Figure() | |
# Color code based on glucose ranges | |
colors = [] | |
for value in chart_data['value']: | |
if value < 70: | |
colors.append('#E74C3C') # Red for low | |
elif value > 180: | |
colors.append('#F39C12') # Orange for high | |
else: | |
colors.append('#27AE60') # Green for in range | |
fig.add_trace(go.Scatter( | |
x=chart_data['systemTime'], | |
y=chart_data['value'], | |
mode='lines+markers', | |
name='Glucose', | |
line=dict(color='#2E86AB', width=2), | |
marker=dict(size=4, color=colors), | |
hovertemplate='<b>%{y} mg/dL</b><br>%{x}<extra></extra>' | |
)) | |
# Add target range shading | |
fig.add_hrect( | |
y0=70, y1=180, | |
fillcolor="rgba(39, 174, 96, 0.1)", | |
layer="below", | |
line_width=0, | |
annotation_text="Target Range", | |
annotation_position="top left" | |
) | |
# Add reference lines | |
fig.add_hline(y=70, line_dash="dash", line_color="#E67E22", | |
annotation_text="Low (70 mg/dL)", annotation_position="right") | |
fig.add_hline(y=180, line_dash="dash", line_color="#E67E22", | |
annotation_text="High (180 mg/dL)", annotation_position="right") | |
fig.add_hline(y=54, line_dash="dot", line_color="#E74C3C", | |
annotation_text="Severe Low (54 mg/dL)", annotation_position="right") | |
fig.add_hline(y=250, line_dash="dot", line_color="#E74C3C", | |
annotation_text="Severe High (250 mg/dL)", annotation_position="right") | |
# Get current stats for title | |
stats = self.data_manager.get_stats_for_ui() | |
tir = stats.get('time_in_range_70_180', 0) | |
data_type = "REAL" if self.current_user_type == "real" else "DEMO" | |
fig.update_layout( | |
title={ | |
'text': f"14-Day Glucose Trends - {self.data_manager.current_user.name} ({data_type} DATA - TIR: {tir:.1f}%)", | |
'x': 0.5, | |
'xanchor': 'center' | |
}, | |
xaxis_title="Time", | |
yaxis_title="Glucose (mg/dL)", | |
hovermode='x unified', | |
height=500, | |
showlegend=False, | |
plot_bgcolor='rgba(0,0,0,0)', | |
paper_bgcolor='rgba(0,0,0,0)', | |
font=dict(size=12), | |
margin=dict(l=60, r=60, t=80, b=60) | |
) | |
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)') | |
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)') | |
return fig | |
def create_interface(): | |
"""Create the Gradio interface with demo users AND real OAuth""" | |
app = GlucoBuddyApp() | |
custom_css = """ | |
.main-header { | |
text-align: center; | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
padding: 2rem; | |
border-radius: 10px; | |
margin-bottom: 2rem; | |
} | |
.load-data-section { | |
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
border-radius: 15px; | |
padding: 2rem; | |
margin: 1.5rem 0; | |
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37); | |
backdrop-filter: blur(4px); | |
border: 1px solid rgba(255, 255, 255, 0.18); | |
} | |
.prominent-button { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
border: none !important; | |
border-radius: 15px !important; | |
padding: 1.5rem 3rem !important; | |
font-size: 1.2rem !important; | |
font-weight: bold !important; | |
color: white !important; | |
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4) !important; | |
transition: all 0.3s ease !important; | |
min-height: 80px !important; | |
text-align: center !important; | |
} | |
.prominent-button:hover { | |
transform: translateY(-2px) !important; | |
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.6) !important; | |
} | |
.real-oauth-button { | |
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important; | |
color: white !important; | |
border: none !important; | |
font-weight: bold !important; | |
} | |
""" | |
with gr.Blocks( | |
title="GlycoAI - AI Glucose Insights", | |
theme=gr.themes.Soft(), | |
css=custom_css | |
) as interface: | |
# Header | |
with gr.Row(): | |
with gr.Column(): | |
gr.HTML(""" | |
<div class="main-header"> | |
<div style="display: flex; align-items: center; justify-content: center; gap: 1rem;"> | |
<div style="width: 60px; height: 60px; background: white; border-radius: 50%; display: flex; align-items: center; justify-content: center;"> | |
<span style="color: #667eea; font-size: 24px; font-weight: bold;">π©Ί</span> | |
</div> | |
<div> | |
<h1 style="margin: 0; font-size: 2.5rem; color: white;">GlycoAI</h1> | |
<p style="margin: 0; font-size: 1.2rem; color: white; opacity: 0.9;">AI-Powered Glucose Chatbot</p> | |
</div> | |
</div> | |
<p style="margin-top: 1rem; font-size: 1rem; color: white; opacity: 0.8;"> | |
Connect your Dexcom CGM data OR try demo users and chat with AI for personalized glucose insights | |
</p> | |
</div> | |
""") | |
# User Selection Section | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("### π₯ Choose Your Data Source") | |
gr.Markdown("Select demo users for instant testing OR authenticate with your real Dexcom account") | |
# Demo Users Section | |
with gr.Group(): | |
gr.Markdown("#### π Demo Users (Instant Access)") | |
gr.Markdown("*Realistic demo data for testing GlycoAI's capabilities*") | |
with gr.Row(): | |
sarah_btn = gr.Button( | |
"Sarah Thompson\n(G7 Mobile - Stable Control)", | |
variant="secondary", | |
size="lg" | |
) | |
marcus_btn = gr.Button( | |
"Marcus Rodriguez\n(ONE+ Mobile - Type 2)", | |
variant="secondary", | |
size="lg" | |
) | |
jennifer_btn = gr.Button( | |
"Jennifer Chen\n(G6 Mobile - Athletic)", | |
variant="secondary", | |
size="lg" | |
) | |
robert_btn = gr.Button( | |
"Robert Williams\n(G6 Receiver - Experienced)", | |
variant="secondary", | |
size="lg" | |
) | |
# Real OAuth Section (if available) | |
if REAL_OAUTH_AVAILABLE: | |
with gr.Group(): | |
gr.Markdown("#### π Real Dexcom Authentication") | |
gr.Markdown("*Connect your actual Dexcom account for live glucose data analysis*") | |
with gr.Row(): | |
real_oauth_btn = gr.Button( | |
"π Connect Real Dexcom Account\n(OAuth Authentication)", | |
variant="primary", | |
size="lg", | |
elem_classes=["real-oauth-button"] | |
) | |
oauth_instructions = gr.Markdown( | |
"Click above to start real Dexcom authentication", | |
visible=True | |
) | |
with gr.Row(): | |
auth_code_input = gr.Textbox( | |
label="Authorization Code (from callback URL after 404 error)", | |
placeholder="ABC123XYZ... (just the code part, not the full URL)", | |
lines=2, | |
visible=False | |
) | |
complete_oauth_btn = gr.Button( | |
"β Complete OAuth", | |
variant="primary", | |
visible=False | |
) | |
else: | |
with gr.Group(): | |
gr.Markdown("#### π Real Dexcom Authentication (Unavailable)") | |
gr.Markdown("*Real OAuth is not configured. Please check your setup.*") | |
gr.Button( | |
"π Real OAuth Not Available\n(Check Configuration)", | |
variant="secondary", | |
size="lg", | |
interactive=False | |
) | |
# Create dummy variables for consistency | |
oauth_instructions = gr.Markdown("Real OAuth not available") | |
auth_code_input = gr.Textbox(visible=False) | |
complete_oauth_btn = gr.Button(visible=False) | |
# Connection Status | |
with gr.Row(): | |
connection_status = gr.Textbox( | |
label="Current User", | |
value="No user selected", | |
interactive=False, | |
container=True | |
) | |
# Section Divider | |
gr.HTML('<div style="height: 2px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 1px; margin: 2rem 0;"></div>') | |
# PROMINENT CENTRALIZED DATA LOADING SECTION | |
with gr.Group(visible=False) as main_interface: | |
# PROMINENT LOAD BUTTON - Centered and Large | |
with gr.Row(): | |
with gr.Column(scale=1): | |
pass # Left spacer | |
with gr.Column(scale=2): | |
load_data_btn = gr.Button( | |
"π LOAD 14-DAY GLUCOSE DATA\nπ Start Analysis & Enable AI Chat", | |
elem_classes=["prominent-button"], | |
size="lg" | |
) | |
with gr.Column(scale=1): | |
pass # Right spacer | |
# Section Divider | |
gr.HTML('<div style="height: 2px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 1px; margin: 2rem 0;"></div>') | |
# Main Content Tabs | |
with gr.Tabs(): | |
# Glucose Chart Tab | |
with gr.TabItem("π Glucose Chart"): | |
with gr.Column(): | |
gr.Markdown("### π Interactive 14-Day Glucose Analysis") | |
gr.Markdown("*Load your data using the button above to see your comprehensive glucose trends*") | |
glucose_chart = gr.Plot( | |
label="Interactive 14-Day Glucose Trends", | |
container=True | |
) | |
# Chat Tab | |
with gr.TabItem("π¬ Chat with AI"): | |
with gr.Column(): | |
gr.Markdown("### π€ Chat with GlycoAI about your glucose data") | |
gr.Markdown("*π Load your data using the button above to enable personalized AI insights*") | |
# Template Prompts | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("**π‘ Quick Start Templates:**") | |
with gr.Row(): | |
template1_btn = gr.Button( | |
"π― Analyze My 14-Day Patterns", | |
variant="secondary", | |
size="sm" | |
) | |
template2_btn = gr.Button( | |
"β‘ Improve My Control", | |
variant="secondary", | |
size="sm" | |
) | |
template3_btn = gr.Button( | |
"π½οΈ Meal Management Tips", | |
variant="secondary", | |
size="sm" | |
) | |
# Chat Interface | |
chatbot = gr.Chatbot( | |
label="π¬ Chat with GlycoAI (Demo + Real Data)", | |
height=500, | |
show_label=True, | |
container=True, | |
bubble_full_width=False, | |
avatar_images=(None, "π©Ί") | |
) | |
# Chat Input | |
with gr.Row(): | |
chat_input = gr.Textbox( | |
placeholder="Ask me about your glucose patterns, trends, or management strategies...", | |
label="Your Question", | |
lines=2, | |
scale=4 | |
) | |
send_btn = gr.Button( | |
"Send π¬", | |
variant="primary", | |
scale=1 | |
) | |
# Chat Controls | |
with gr.Row(): | |
clear_chat_btn = gr.Button( | |
"ποΈ Clear Chat", | |
variant="secondary", | |
size="sm" | |
) | |
gr.Markdown("*AI responses are for informational purposes only. Always consult your healthcare provider.*") | |
# Data Overview Tab | |
with gr.TabItem("π Data Overview"): | |
with gr.Column(): | |
gr.Markdown("### π Comprehensive Data Analysis") | |
gr.Markdown("*Load your data using the button above to see detailed glucose statistics*") | |
data_display = gr.Markdown("Click 'Load 14-Day Glucose Data' above to see your comprehensive analysis", container=True) | |
# Event Handlers | |
def handle_demo_user_selection(user_key): | |
status, interface_visibility = app.select_demo_user(user_key) | |
return status, interface_visibility, [] | |
def handle_load_data(): | |
overview, chart = app.load_glucose_data() | |
return overview, chart | |
def get_template_prompt(template_type): | |
templates = app.get_template_prompts() | |
if template_type == 1: | |
return templates[0] if templates else "Can you analyze my recent glucose patterns and give me insights?" | |
elif template_type == 2: | |
return templates[1] if len(templates) > 1 else "What can I do to improve my diabetes management based on my data?" | |
else: | |
return "What are some meal management strategies for better glucose control?" | |
def handle_chat_submit(message, history): | |
return app.chat_with_mistral(message, history) | |
def handle_enter_key(message, history): | |
if message.strip(): | |
return app.chat_with_mistral(message, history) | |
return "", history | |
# Connect Event Handlers for Demo Users | |
sarah_btn.click( | |
lambda: handle_demo_user_selection("sarah_g7"), | |
outputs=[connection_status, main_interface, chatbot] | |
) | |
marcus_btn.click( | |
lambda: handle_demo_user_selection("marcus_one"), | |
outputs=[connection_status, main_interface, chatbot] | |
) | |
jennifer_btn.click( | |
lambda: handle_demo_user_selection("jennifer_g6"), | |
outputs=[connection_status, main_interface, chatbot] | |
) | |
robert_btn.click( | |
lambda: handle_demo_user_selection("robert_receiver"), | |
outputs=[connection_status, main_interface, chatbot] | |
) | |
# Connect Event Handlers for Real OAuth (if available) | |
if REAL_OAUTH_AVAILABLE: | |
real_oauth_btn.click( | |
app.start_real_oauth, | |
outputs=[oauth_instructions] | |
).then( | |
lambda: (gr.update(visible=True), gr.update(visible=True)), | |
outputs=[auth_code_input, complete_oauth_btn] | |
) | |
complete_oauth_btn.click( | |
app.complete_real_oauth, | |
inputs=[auth_code_input], | |
outputs=[connection_status, main_interface] | |
).then( | |
lambda: [], # Clear chatbot | |
outputs=[chatbot] | |
) | |
# PROMINENT DATA LOADING - Single button updates all views | |
load_data_btn.click( | |
handle_load_data, | |
outputs=[data_display, glucose_chart] | |
) | |
# Chat Handlers | |
send_btn.click( | |
handle_chat_submit, | |
inputs=[chat_input, chatbot], | |
outputs=[chat_input, chatbot] | |
) | |
chat_input.submit( | |
handle_enter_key, | |
inputs=[chat_input, chatbot], | |
outputs=[chat_input, chatbot] | |
) | |
# Template Button Handlers | |
template1_btn.click( | |
lambda: get_template_prompt(1), | |
outputs=[chat_input] | |
) | |
template2_btn.click( | |
lambda: get_template_prompt(2), | |
outputs=[chat_input] | |
) | |
template3_btn.click( | |
lambda: get_template_prompt(3), | |
outputs=[chat_input] | |
) | |
# Clear Chat | |
clear_chat_btn.click( | |
app.clear_chat_history, | |
outputs=[chatbot] | |
) | |
# Footer | |
with gr.Row(): | |
gr.HTML(f""" | |
<div style="text-align: center; padding: 2rem; margin-top: 2rem; border-top: 1px solid #dee2e6; color: #6c757d;"> | |
<p><strong>β οΈ Important Medical Disclaimer</strong></p> | |
<p>GlycoAI is for informational and educational purposes only. Always consult your healthcare provider | |
before making any changes to your diabetes management plan. This tool does not replace professional medical advice.</p> | |
<p style="margin-top: 1rem; font-size: 0.9rem;"> | |
π Your data is processed securely and not stored permanently. | |
π‘ Powered by Dexcom API integration and Mistral AI.<br> | |
{"π Demo data available instantly β’ π Real OAuth: " + ("Available" if REAL_OAUTH_AVAILABLE else "Not configured")} | |
</p> | |
</div> | |
""") | |
return interface | |
def main(): | |
"""Main function to launch the application""" | |
print("π Starting GlycoAI - AI-Powered Glucose Insights (Demo + Real OAuth)...") | |
# Check OAuth availability | |
oauth_status = "β Available" if REAL_OAUTH_AVAILABLE else "β Not configured" | |
print(f"π Real Dexcom OAuth: {oauth_status}") | |
# Validate environment before starting | |
print("π Validating environment configuration...") | |
if not validate_environment(): | |
print("β Environment validation failed!") | |
print("Please check your .env file or environment variables.") | |
return | |
print("β Environment validation passed!") | |
try: | |
# Create and launch the interface | |
demo = create_interface() | |
print("π― GlycoAI is starting with enhanced features...") | |
print("π Features: Demo users + Real OAuth, unified data management, consistent metrics") | |
print("π Demo users: 4 realistic profiles for instant testing") | |
if REAL_OAUTH_AVAILABLE: | |
print("π Real OAuth: Available - connect your actual Dexcom account") | |
else: | |
print("π Real OAuth: Not configured - demo users only") | |
# Launch with custom settings | |
demo.launch( | |
server_name="0.0.0.0", # Allow external access | |
server_port=7860, # Your port | |
share=True, # Set to True for public sharing (tunneling) | |
debug=os.getenv("DEBUG", "false").lower() == "true", | |
show_error=True, # Show errors in the interface | |
auth=None, # No authentication required | |
favicon_path=None, # Use default favicon | |
ssl_verify=False # Disable SSL verification for development | |
) | |
except Exception as e: | |
logger.error(f"Failed to launch GlycoAI application: {e}") | |
print(f"β Error launching application: {e}") | |
# Provide helpful error information | |
if "environment" in str(e).lower(): | |
print("\nπ‘ Environment troubleshooting:") | |
print("1. Check if .env file exists with MISTRAL_API_KEY") | |
print("2. Verify your API key is valid") | |
print("3. For Hugging Face Spaces, check Repository secrets") | |
else: | |
print("\nπ‘ Try checking:") | |
print("1. All dependencies are installed: pip install -r requirements.txt") | |
print("2. Port 7860 is available") | |
print("3. Check the logs above for specific error details") | |
raise | |
if __name__ == "__main__": | |
# Setup logging configuration | |
log_level = os.getenv("LOG_LEVEL", "INFO") | |
logging.basicConfig( | |
level=getattr(logging, log_level.upper()), | |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
handlers=[ | |
logging.FileHandler('glycoai.log'), | |
logging.StreamHandler() | |
] | |
) | |
# Run the main application | |
main() |