import streamlit as st # Load environment variables from .env file import env_loader # Configure page FIRST - before any other Streamlit commands st.set_page_config(page_title="AI Loan Assistant - Credit Pre-Assessment", layout="wide") # Hide Streamlit branding for anonymous review (CSS + JavaScript) st.markdown(""" """, unsafe_allow_html=True) # ===== QUALTRICS/PROLIFIC INTEGRATION (robust final) ===== import time from urllib.parse import unquote, urlparse, parse_qsl, urlencode, urlunparse def _get_query_params(): try: # Streamlit ≥1.32 return dict(st.query_params) except Exception: try: # Older Streamlit return st.experimental_get_query_params() except Exception: return {} def _as_str(v): if isinstance(v, list): return v[0] if v else "" return v if isinstance(v, str) else "" def _is_safe_return(ru: str) -> bool: """Allow https/http + any *.qualtrics.com netloc (handles regional subdomains).""" if not ru: return False try: d = unquote(ru) # tolerate missing scheme (rare). Qualtrics links should always be https if not d.startswith(("http://", "https://")): d = "https://" + d p = urlparse(d) return (p.scheme in ("http", "https")) and ("qualtrics.com" in p.netloc) except Exception: return False def _build_final_return(done=True): """ Start with the encoded Qualtrics 'return' URL, decode once, ensure it points to Qualtrics, then append pid/cond/done IFF missing. """ rr = st.session_state.get("return_raw", "") if not rr or not _is_safe_return(rr): return None decoded = unquote(rr) # normalize scheme if missing (defensive) if not decoded.startswith(("http://", "https://")): decoded = "https://" + decoded p = urlparse(decoded) q = dict(parse_qsl(p.query, keep_blank_values=True)) # only add if not already present pid_ss = st.session_state.get("pid", "") cond_ss = st.session_state.get("cond", "") prolific_pid_ss = st.session_state.get("prolific_pid", "") if "pid" not in q and pid_ss: q["pid"] = pid_ss if "cond" not in q and cond_ss: q["cond"] = cond_ss if "PROLIFIC_PID" not in q and prolific_pid_ss: q["PROLIFIC_PID"] = prolific_pid_ss if "done" not in q: q["done"] = "1" if done else "0" return urlunparse(p._replace(query=urlencode(q, doseq=True))) # -------------- read & persist params once -------------- _qs = _get_query_params() _pid_in = _as_str(_qs.get("pid", "")) _cond_in = _as_str(_qs.get("cond", "")) _ret_in = _as_str(_qs.get("return", "")) # Prolific standard parameter _prolific_pid = _as_str(_qs.get("PROLIFIC_PID", "")) if "pid" not in st.session_state and _pid_in: st.session_state.pid = _pid_in if "cond" not in st.session_state and _cond_in: st.session_state.cond = _cond_in if "return_raw" not in st.session_state and _ret_in: st.session_state.return_raw = _ret_in # Store Prolific ID separately for research tracking if "prolific_pid" not in st.session_state and _prolific_pid: st.session_state.prolific_pid = _prolific_pid # boolean flag for UI (sticky footer etc.) st.session_state.has_return_url = bool(st.session_state.get("return_raw", "")) # always recompute # one-shot redirect latch if "_returned" not in st.session_state: st.session_state._returned = False def back_to_survey(done_flag=True): """Single exit path. Call on button click or timeout.""" if st.session_state._returned: return final = _build_final_return(done=done_flag) if not final: st.warning("Return link missing or invalid. Please use your browser Back button.") return st.session_state._returned = True # immediate redirect – robust & no loops st.markdown(f'', unsafe_allow_html=True) st.stop() # handle previously latched redirect (e.g., if Streamlit re-renders mid-redirect) if st.session_state.get("_returned"): final = _build_final_return(done=True) if final: st.markdown(f'', unsafe_allow_html=True) st.stop() # set the 3-minute deadline once and track start time if "deadline_ts" not in st.session_state: st.session_state.deadline_ts = time.time() + 180 st.session_state.start_time = time.time() # Track when user started # fire auto-return when time is up (exactly once) if time.time() >= st.session_state.deadline_ts: back_to_survey(done_flag=True) # expose the function for UI buttons st.session_state.back_to_survey = back_to_survey # Prevent restart via browser refresh/back ONLY if user had already started # Check if this is a fresh session (first visit) vs a refresh (had chat history) if "loan_assistant" not in st.session_state and st.session_state.get("return_raw"): # Only redirect if they had already started (had chat history marker) if st.session_state.get("application_started", False): # User refreshed or went back after starting - redirect to survey back_to_survey(done_flag=True) # ===== END QUALTRICS/PROLIFIC INTEGRATION ===== # Now import everything else from agent import Agent from nlu import NLU from answer import Answers from github_saver import save_to_github from loan_assistant import LoanAssistant from ab_config import config from shap_visualizer import display_shap_explanation, explain_shap_visualizations from data_logger import init_logger from xai_methods import get_friendly_feature_name import os import pandas as pd # Initialize data logger logger = init_logger() # Define field options for quick selection (based on actual Adult dataset analysis) field_options = { 'workclass': ['Private', 'Self-emp-not-inc', 'Self-emp-inc', 'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked', '?'], 'education': ['Bachelors', 'HS-grad', 'Masters', 'Some-college', 'Assoc-acdm', 'Assoc-voc', '11th', '9th', '10th', '12th', '7th-8th', 'Doctorate', '1st-4th', '5th-6th', 'Preschool', 'Prof-school'], 'marital_status': ['Married-civ-spouse', 'Divorced', 'Never-married', 'Separated', 'Widowed', 'Married-spouse-absent', 'Married-AF-spouse'], 'occupation': ['Tech-support', 'Craft-repair', 'Other-service', 'Sales', 'Exec-managerial', 'Prof-specialty', 'Handlers-cleaners', 'Machine-op-inspct', 'Adm-clerical', 'Farming-fishing', 'Armed-Forces', 'Priv-house-serv', 'Protective-serv', 'Transport-moving', '?'], 'sex': ['Male', 'Female'], 'race': ['Black', 'Asian-Pac-Islander', 'Amer-Indian-Eskimo', 'White', 'Other'], 'native_country': ['United-States', 'Cambodia', 'Canada', 'China', 'Columbia', 'Cuba', 'Dominican-Republic', 'Ecuador', 'El-Salvador', 'England', 'France', 'Germany', 'Greece', 'Guatemala', 'Haiti', 'Holand-Netherlands', 'Honduras', 'Hong', 'Hungary', 'India', 'Iran', 'Ireland', 'Italy', 'Jamaica', 'Japan', 'Laos', 'Mexico', 'Nicaragua', 'Outlying-US(Guam-USVI-etc)', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Puerto-Rico', 'Scotland', 'South', 'Taiwan', 'Thailand', 'Trinadad&Tobago', 'Vietnam', 'Yugoslavia', '?'], 'relationship': ['Wife', 'Own-child', 'Husband', 'Not-in-family', 'Other-relative', 'Unmarried'] } # Str

Hi! I'm Luna

amlit compatibility function def st_rerun(): """Compatibility function for Streamlit rerun across versions""" if hasattr(st, 'rerun'): st.rerun() else: st.experimental_rerun() # Custom CSS for better appearance with chat bubbles st.markdown(""" """, unsafe_allow_html=True) def initialize_system(): """Initialize the agent and all components""" try: agent = Agent() answers = Answers( list_node=[], clf=agent.clf, clf_display=agent.clf_display, current_instance=agent.current_instance, question=None, l_exist_classes=agent.l_exist_classes, l_exist_features=agent.l_exist_features, l_instances=agent.l_instances, data=agent.data, df_display_instance=agent.df_display_instance, predicted_class=agent.predicted_class, preprocessor=agent.preprocessor ) return agent, answers except Exception as e: st.error(f"Failed to initialize system: {str(e)}") st.error("Please check the console for more details.") import traceback st.code(traceback.format_exc()) # Return None values to prevent further errors return None, None # Initialize system if 'agent' not in st.session_state: st.session_state.agent, st.session_state.answers = initialize_system() # Check if initialization was successful if st.session_state.agent is None: st.error("System initialization failed. Please check the error messages above and try refreshing the page.") st.stop() agent = st.session_state.agent answers = st.session_state.answers # Initialize loan assistant if 'loan_assistant' not in st.session_state: st.session_state.loan_assistant = LoanAssistant(agent) st.session_state.chat_history = [] # App header st.title("🏦 AI Loan Assistant - Credit Pre-Assessment") # Assistant Introduction (A/B testing) assistant_avatar = config.get_assistant_avatar() if assistant_avatar and os.path.exists(assistant_avatar): import base64 with open(assistant_avatar, "rb") as f: avatar_pic_b64 = base64.b64encode(f.read()).decode() st.markdown(f"""
{config.assistant_name}

Hi! I'm {config.assistant_name}

{config.assistant_intro}

""", unsafe_allow_html=True) else: # Fallback without image st.markdown(f"""
{config.assistant_name[0]}

Hi! I'm {config.assistant_name}

{config.assistant_intro}

""", unsafe_allow_html=True) # Single conversational interface st.markdown("---") # Sidebar - keep minimal to avoid distracting from experimental task with st.sidebar: # No restart option - users should complete one application per session # Explanation style is controlled by the experimental condition, not user choice # A/B Testing Debug Info (only for development/testing - hidden from users) # Uncomment the lines below only when debugging A/B testing locally # if config.show_debug_info and os.getenv('HICXAI_DEBUG_MODE', 'false').lower() == 'true': # What‑if Lab (shown after user asks what-if in counterfactual HIGH anthropomorphism conditions only) if config.show_counterfactual and config.show_anthropomorphic and getattr(st.session_state.loan_assistant, 'show_what_if_lab', False): st.markdown("---") st.subheader("🧪 What‑if Lab") st.caption("Adjust inputs to see how the predicted probability changes.") # Prepare a baseline instance from current app state if available app_state = st.session_state.loan_assistant.application def default(v, fallback): return v if v is not None else fallback # Core numerics age = st.slider("Age", min_value=17, max_value=90, value=int(default(app_state.age, 35))) hours = st.slider("Hours per week", min_value=1, max_value=99, value=int(default(app_state.hours_per_week, 40))) gain = st.number_input("Capital Gain", min_value=0, max_value=99999, step=100, value=int(default(app_state.capital_gain, 0))) loss = st.number_input("Capital Loss", min_value=0, max_value=4356, step=50, value=int(default(app_state.capital_loss, 0))) # Categorical selectors using known field options edu = st.selectbox("Education", options=field_options['education'], index=field_options['education'].index(default(app_state.education, 'HS-grad'))) occ = st.selectbox("Occupation", options=field_options['occupation'], index=field_options['occupation'].index(default(app_state.occupation, 'Sales'))) workclass = st.selectbox("Workclass", options=field_options['workclass'], index=field_options['workclass'].index(default(app_state.workclass, 'Private'))) marital = st.selectbox("Marital Status", options=field_options['marital_status'], index=field_options['marital_status'].index(default(app_state.marital_status, 'Never-married'))) relationship = st.selectbox("Relationship", options=field_options['relationship'], index=field_options['relationship'].index(default(app_state.relationship, 'Not-in-family'))) sex = st.selectbox("Sex", options=field_options['sex'], index=field_options['sex'].index(default(app_state.sex, 'Male'))) race = st.selectbox("Race", options=field_options['race'], index=field_options['race'].index(default(app_state.race, 'White'))) country = st.selectbox("Native Country", options=field_options['native_country'], index=field_options['native_country'].index(default(app_state.native_country, 'United-States'))) # Build a hypothetical instance and predict try: # Start from existing application dict (fill minimal defaults) hypo = app_state.to_dict() hypo['age'] = age hypo['hours_per_week'] = hours hypo['education'] = edu hypo['occupation'] = occ hypo['workclass'] = workclass hypo['marital_status'] = marital hypo['relationship'] = relationship hypo['sex'] = sex hypo['race'] = race hypo['native_country'] = country hypo['capital_gain'] = gain hypo['capital_loss'] = loss if hypo.get('education_num') is None: edu_map = { 'Preschool': 1, '1st-4th': 2, '5th-6th': 3, '7th-8th': 4, '9th': 5, '10th': 6, '11th': 7, '12th': 8, 'HS-grad': 9, 'Some-college': 10, 'Assoc-voc': 11, 'Assoc-acdm': 12, 'Bachelors': 13, 'Masters': 14, 'Prof-school': 15, 'Doctorate': 16 } hypo['education_num'] = edu_map.get(edu, 9) # Ensure required fields have plausible defaults hypo.setdefault('workclass', 'Private') hypo.setdefault('marital_status', 'Never-married') hypo.setdefault('relationship', 'Not-in-family') hypo.setdefault('race', 'White') hypo.setdefault('sex', 'Male') hypo.setdefault('capital_gain', 0) hypo.setdefault('capital_loss', 0) hypo.setdefault('native_country', 'United-States') import pandas as pd app_df = pd.DataFrame([hypo]) app_df['income'] = '<=50K' # dummy from preprocessing import preprocess_adult processed = preprocess_adult(app_df) X = processed.drop('income', axis=1) # Align with training features train_df = pd.concat([agent.data['X_display'], agent.data['y_display']], axis=1) train_df_processed = preprocess_adult(train_df) expected = train_df_processed.drop('income', axis=1).columns.tolist() for col in expected: if col not in X.columns: X[col] = 0 X = X[expected] # Predict probability if available prob = None if hasattr(agent.clf_display, 'predict_proba'): p = agent.clf_display.predict_proba(X) # Assume class index 1 corresponds to '>50K' prob = float(p[0][1]) if p.shape[1] > 1 else float(p[0][0]) st.metric(label="Estimated P(>50K)", value=f"{(prob if prob is not None else 0.5)*100:.1f}%") # Optional: refresh SHAP visuals for hypo profile (textual SHAP for now) # We keep visuals in the main flow; here we just indicate changes st.caption("Adjust inputs to explore their impact. Use chat for detailed explanations and visuals.") except Exception as e: st.caption(f"What‑if Lab unavailable: {e}") # Otherwise, no What‑if panel is shown until triggered by user # st.markdown("---") # st.markdown("**🧪 Debug Info**") # st.markdown(f"Version: **{config.version}**") # st.markdown(f"Assistant: **{config.assistant_name}**") # st.markdown(f"SHAP Visuals: **{config.show_shap_visualizations}**") # Chat interface - Display chat history with enhanced bubbles st.markdown('
', unsafe_allow_html=True) for i, (user_msg, assistant_msg) in enumerate(st.session_state.chat_history): # User message (right side, blue bubble) if user_msg: st.markdown(f"""
You
{user_msg}
""", unsafe_allow_html=True) # Assistant message with profile picture (left side, white bubble) if assistant_msg: assistant_avatar = config.get_assistant_avatar() if assistant_avatar and os.path.exists(assistant_avatar): import base64 with open(assistant_avatar, "rb") as f: avatar_pic_b64 = base64.b64encode(f.read()).decode() avatar_pic_element = f'{config.assistant_name}' else: avatar_pic_element = f'
{config.assistant_name[0]}
' st.markdown(f"""
{avatar_pic_element}
{assistant_msg}
""", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # Initialize with welcome message if len(st.session_state.chat_history) == 0: welcome_msg = st.session_state.loan_assistant.handle_message("hello") st.session_state.chat_history.append((None, welcome_msg)) st_rerun() # Chat input (form enables Enter-to-send and clears on submit automatically) # Check if current field has clickable options for placeholder current_field = getattr(st.session_state.loan_assistant, 'current_field', None) if current_field and current_field in field_options: placeholder_text = "💬 Type your answer or use the clickable buttons below..." else: placeholder_text = "Type your message to Luna..." with st.form("chat_form", clear_on_submit=True): col1, col2 = st.columns([5, 1]) with col1: user_message = st.text_input("Message to Luna", key="user_input", placeholder=placeholder_text, label_visibility="collapsed") with col2: send_button = st.form_submit_button("Send", use_container_width=True) # Add helper text for clickable features if current_field and current_field in field_options: st.markdown('
👆 Use the clickable buttons below for faster selection!
', unsafe_allow_html=True) # Show clickable options right after chat input (for immediate visibility) if current_field and current_field in field_options: st.markdown("---") st.markdown(f"### 🎯 Quick Select: {current_field.replace('_', ' ').title()}") st.markdown("**💡 Click any option below instead of typing:**") st.markdown('
', unsafe_allow_html=True) options = field_options[current_field] # Create buttons in rows with enhanced styling cols_per_row = 4 if len(options) > 8 else 3 for i in range(0, len(options), cols_per_row): cols = st.columns(cols_per_row) for j, option in enumerate(options[i:i+cols_per_row]): with cols[j]: # Get friendly name for display friendly_option = get_friendly_feature_name(f"{current_field}_{option}") # If no mapping found, clean up the technical name if friendly_option.startswith(current_field.title()): friendly_option = option.replace('-', ' ').replace('_', ' ') # Enhanced button styling based on option type if option == "Other": button_text = f"🔄 {friendly_option}" button_type = "primary" elif option == "?": button_text = f"❓ Unknown/Prefer not to say" button_type = "primary" elif option in ["Male", "Female"]: button_text = f"👤 {friendly_option}" button_type = "secondary" elif option == "United-States": button_text = f"🇺🇸 {friendly_option}" button_type = "primary" elif option in ["Private", "Self-emp-not-inc", "Self-emp-inc"]: button_text = f"💼 {friendly_option}" button_type = "secondary" elif "gov" in option.lower(): button_text = f"🏛️ {friendly_option}" button_type = "secondary" else: button_text = f"✨ {friendly_option}" button_type = "secondary" if st.button(button_text, key=f"option_top_{current_field}_{option}", use_container_width=True, type=button_type): st.session_state.option_clicked = option st_rerun() st.markdown('
', unsafe_allow_html=True) st.markdown("*💬 Or you can still type your answer in the chat box above*") # Process user input if send_button and user_message: # Mark that user has started the application st.session_state.application_started = True # Log interaction if logger: current_field = getattr(st.session_state.loan_assistant, 'current_field', None) logger.log_interaction("user_message", { "field": current_field, "input_method": "typed", "content": user_message, "conversation_state": st.session_state.loan_assistant.conversation_state.value }) # Handle the message through loan assistant assistant_response = st.session_state.loan_assistant.handle_message(user_message) # Log assistant response if logger: logger.log_interaction("assistant_response", { "content": assistant_response }) # Add to chat history (form clears input on submit) st.session_state.chat_history.append((user_message, assistant_response)) st_rerun() # Handle option clicks if 'option_clicked' in st.session_state and st.session_state.option_clicked: option_value = st.session_state.option_clicked # Mark that user has started the application st.session_state.application_started = True # Log interaction if logger: current_field = getattr(st.session_state.loan_assistant, 'current_field', None) logger.log_interaction("user_message", { "field": current_field, "input_method": "clicked", "content": option_value, "conversation_state": st.session_state.loan_assistant.conversation_state.value }) assistant_response = st.session_state.loan_assistant.handle_message(option_value) # Log assistant response if logger: logger.log_interaction("assistant_response", { "content": assistant_response }) # Add to chat history st.session_state.chat_history.append((option_value, assistant_response)) st.session_state.option_clicked = None # Reset st_rerun() # Persistent SHAP visuals section: render when feature_importance explanation is enabled if config.show_shap_visualizations: shap_data = getattr(st.session_state.loan_assistant, 'last_shap_result', None) if shap_data: st.markdown("---") st.subheader("🔎 Visual Explanations") display_shap_explanation(shap_data) explain_shap_visualizations() # Quick reply buttons based on current state st.markdown("---") st.markdown("**Quick Replies:**") current_state = st.session_state.loan_assistant.conversation_state.value if current_state == 'greeting': col1, col2, col3 = st.columns(3) with col1: if st.button("👋 Start Application", key="quick_start"): response = st.session_state.loan_assistant.handle_message("start") st.session_state.chat_history.append(("start", response)) st_rerun() elif current_state == 'collecting_info': col1, col2, col3 = st.columns(3) with col1: if st.button("Check Progress", key="quick_progress"): if logger: logger.log_interaction("progress_check", {}) response = st.session_state.loan_assistant.handle_message("review") st.session_state.chat_history.append(("check progress", response)) st_rerun() with col2: if st.button("Help", key="quick_help"): if logger: logger.log_interaction("help_click", {}) # Get context-aware help current_field = getattr(st.session_state.loan_assistant, 'current_field', None) if current_field: help_msg = st.session_state.loan_assistant._get_field_help(current_field) help_msg += f"\n\n💡 **You can also:**\n• Say 'review' to see your progress\n• Click the quick-select buttons below\n• Ask for specific examples" else: help_msg = ("I'm collecting information for your loan application. Please answer the questions " "as accurately as possible. You can say 'review' to see your progress.") st.session_state.chat_history.append(("help", help_msg)) st_rerun() elif current_state == 'complete': # Only show What-If button in Condition 4 (HIGH anthropomorphism + counterfactual) if config.show_counterfactual and config.show_anthropomorphic: col1, col2 = st.columns(2) with col1: if st.button("Explain Decision", key="quick_explain", use_container_width=True): if logger: logger.log_interaction("explanation_request", {"type": "decision_explanation"}) response = st.session_state.loan_assistant.handle_message("explain") st.session_state.chat_history.append(("explain", response)) st_rerun() with col2: if st.button("🔧 What If Analysis", key="quick_whatif", use_container_width=True): # Turn on What‑if Lab and prompt guidance try: st.session_state.loan_assistant.show_what_if_lab = True except Exception: pass response = "What‑if Lab enabled in the sidebar. Adjust Age, Hours, Education, or Occupation to see how the probability changes." st.session_state.chat_history.append(("what if analysis", response)) st_rerun() else: # Show only Explain button for other conditions if st.button("Explain Decision", key="quick_explain", use_container_width=True): if logger: logger.log_interaction("explanation_request", {"type": "decision_explanation"}) response = st.session_state.loan_assistant.handle_message("explain") st.session_state.chat_history.append(("explain", response)) st_rerun() # Clickable Options for Current Field (if collecting info) if current_state == 'collecting_info' and hasattr(st.session_state.loan_assistant, 'current_field') and st.session_state.loan_assistant.current_field: current_field = st.session_state.loan_assistant.current_field if current_field in field_options: st.markdown("---") st.markdown(f"### 🎯 Quick Select: {current_field.replace('_', ' ').title()}") st.markdown("**💡 Click any option below instead of typing:**") st.markdown('
', unsafe_allow_html=True) options = field_options[current_field] # Create buttons in rows with enhanced styling cols_per_row = 4 if len(options) > 8 else 3 for i in range(0, len(options), cols_per_row): cols = st.columns(cols_per_row) for j, option in enumerate(options[i:i+cols_per_row]): with cols[j]: # Enhanced button styling based on option type # Get friendly name for display friendly_option = get_friendly_feature_name(f"{current_field}_{option}") # If no mapping found, use the option as-is if friendly_option.startswith(current_field.title()): friendly_option = option.replace('-', ' ').replace('_', ' ') if option == "Other": button_text = f"🔄 {friendly_option}" button_type = "primary" elif option == "?": button_text = f"❓ Unknown/Prefer not to say" button_type = "primary" elif option in ["Male", "Female"]: button_text = f"👤 {friendly_option}" button_type = "secondary" elif option == "United-States": button_text = f"🇺🇸 {friendly_option}" button_type = "primary" elif option in ["Private", "Self-emp-not-inc", "Self-emp-inc"]: button_text = f"💼 {friendly_option}" button_type = "secondary" elif "gov" in option.lower(): button_text = f"🏛️ {friendly_option}" button_type = "secondary" else: button_text = f"✨ {friendly_option}" button_type = "secondary" if st.button(button_text, key=f"option_{current_field}_{option}", use_container_width=True, type=button_type): st.session_state.option_clicked = option st_rerun() st.markdown('
', unsafe_allow_html=True) st.markdown("*💬 Or you can still type your answer in the chat box above*") # Feedback section (appears after application is complete) if current_state == 'complete' and len(st.session_state.chat_history) > 5: st.markdown("---") st.markdown("### 📝 Your Feedback") st.markdown("Help us improve by sharing your experience:") with st.form("feedback_form"): col1, col2 = st.columns(2) with col1: rating = st.select_slider( "How would you rate your experience?", options=[1, 2, 3, 4, 5], value=3, format_func=lambda x: "⭐" * x ) ease_of_use = st.radio( "Was the application process easy to understand?", ["Very Easy", "Easy", "Neutral", "Difficult", "Very Difficult"] ) with col2: explanation_clarity = st.radio( "Were the AI explanations helpful?", ["Very Helpful", "Helpful", "Neutral", "Not Helpful", "Confusing"] ) would_recommend = st.radio( "Would you recommend this service?", ["Definitely", "Probably", "Maybe", "Probably Not", "Definitely Not"] ) feedback_text = st.text_area( "Additional comments (optional):", placeholder="“What feature would help you most next time?”\n“What would make this agent's explanations more useful?”..." ) submitted = st.form_submit_button("Submit Feedback 🚀") if submitted: # Calculate completion percentage completion = st.session_state.loan_assistant.application.calculate_completion() feedback_data = { "rating": rating, "ease_of_use": ease_of_use, "explanation_clarity": explanation_clarity, "would_recommend": would_recommend, "additional_comments": feedback_text, "conversation_length": len(st.session_state.chat_history), "completion_percentage": completion, # A/B Testing metadata "ab_version": config.version, "session_id": config.session_id, "assistant_name": config.assistant_name, "had_shap_visualizations": config.show_shap_visualizations, "timestamp": pd.Timestamp.now().isoformat() } # Log feedback to data logger if logger: logger.set_feedback(feedback_data) # Save feedback try: # Try GitHub first (if configured) github_token = os.getenv('GITHUB_TOKEN') github_repo = os.getenv('GITHUB_REPO', 'your-username/your-repo') if github_token: import json timestamp = pd.Timestamp.now().strftime('%Y%m%d_%H%M%S') filename = f"feedback/session_{config.session_id}_{timestamp}.json" success = save_to_github( repo=github_repo, path=filename, content=json.dumps(feedback_data, indent=2), commit_message=f"User feedback - {config.version} - {timestamp}", github_token=github_token ) if success: st.success("Thank you for your feedback! 🎉") st.session_state.feedback_submitted = True else: raise Exception("GitHub save failed") else: raise Exception("No GitHub token configured") except Exception as e: st.warning("Feedback saved locally. Thank you!") st.session_state.feedback_submitted = True # Fallback: save to local file import json os.makedirs('feedback', exist_ok=True) timestamp = pd.Timestamp.now().strftime('%Y%m%d_%H%M%S') filename = f"feedback/session_{config.session_id}_{timestamp}.json" with open(filename, "w") as f: f.write(json.dumps(feedback_data, indent=2)) # Show "Continue to survey" button OUTSIDE the form (alternate after feedback) # Only show after 2 minutes to ensure user engagement if st.session_state.get("feedback_submitted", False) and st.session_state.get("return_raw"): elapsed_time = time.time() - st.session_state.get("start_time", time.time()) if elapsed_time >= 120: # 2 minutes = 120 seconds st.markdown("---") if st.button("✅ Continue to survey", type="primary", use_container_width=True, key="feedback_return"): back_to_survey() else: remaining = int(120 - elapsed_time) st.markdown("---") st.info(f"⏱️ Please interact with the application. Continue button will appear in {remaining} seconds.") # Footer with dataset information st.markdown("---") st.markdown("""

🏦 AI Loan Assistant

🔬 Algorithm trained on the Adult (Census Income) dataset with 32,561 records from the UCI Machine Learning Repository

""", unsafe_allow_html=True) # Expandable dataset details with st.expander("📊 Dataset Information - Adult Census Income Dataset"): st.markdown(""" **Dataset Overview:** The Adult Census Income Dataset is a popular benchmark dataset from the UCI Machine Learning Repository, sometimes referred to as the Census Income or Adult dataset. It includes **32,561 records** and **15 attributes**, each representing a person's social, employment, and demographic information. The dataset originates from the U.S. Census database from 1994. **Prediction Task:** The main goal is to determine whether an individual makes more than $50,000 per year based on their attributes. The income is the target variable with two possible classes: - **≤50K**: Income less than or equal to $50,000 - **>50K**: Income greater than $50,000 **Dataset Features:** The dataset contains both qualitative and numerical attributes: - **Age**: Numerical value indicating person's age - **Workclass**: Type of employment (Private sector, Self-employed, Federal/Local/State government, etc.) - **Education / Education-num**: Highest education level (High school graduate, Bachelor's, Master's, Doctorate, etc.) - **Marital-status**: Marital status (Married, Divorced, Never married, Separated, Widowed, etc.) - **Occupation**: Work area (Professional, Sales, Administrative, Tech support, Management, etc.) - **Relationship**: Family role (Husband, Wife, Own-child, Not-in-family, Other-relative, Unmarried) - **Race**: Ethnic background (White, Asian-Pacific Islander, Indigenous American, Black, Other) - **Sex**: Gender (Male, Female) - **Capital-gain / Capital-loss**: Investment gains or losses - **Hours-per-week**: Number of working hours per week - **Native-country**: Country of origin (42 countries including United States, Canada, Mexico, Philippines, India, China, Germany, England, and many others) - **Income**: Target label (≤50K or >50K) **Model Performance:** Our trained RandomForest classifier achieves **85.94% accuracy** on this dataset. """) # A/B Testing Debug Info (only for development - hidden from users) # Only show when HICXAI_DEBUG_MODE environment variable is set to 'true' if os.getenv('HICXAI_DEBUG_MODE', 'false').lower() == 'true': st.markdown("---") st.markdown("### 🧪 A/B Testing Information (Debug Mode)") col1, col2, col3 = st.columns(3) with col1: st.markdown(f"**Version:** {config.version}") st.markdown(f"**Session ID:** {config.session_id}") with col2: st.markdown(f"**Assistant:** {config.assistant_name}") st.markdown(f"**SHAP Visuals:** {config.show_shap_visualizations}") with col3: st.markdown(f"**Concurrent Testing:** ✅ Enabled") st.markdown(f"**User Isolation:** ✅ Session-based") # Sticky return footer (only show after 2 minutes of engagement) if st.session_state.get("return_raw"): elapsed_time = time.time() - st.session_state.get("start_time", time.time()) if elapsed_time >= 60: # 1 minute = 60 seconds st.markdown("---") col_a, col_b = st.columns([3, 1]) with col_a: remaining = max(0, int(st.session_state.deadline_ts - time.time())) m, s = divmod(remaining, 60) st.caption(f"⏱️ Up to {m}:{s:02d} remaining. You can return anytime.") with col_b: if st.button("✅ Continue to survey", type="primary", use_container_width=True, key="footer_return"): back_to_survey() else: # Show countdown until button appears st.markdown("---") wait_time = int(60 - elapsed_time) m, s = divmod(wait_time, 60) remaining_deadline = max(0, int(st.session_state.deadline_ts - time.time())) md, sd = divmod(remaining_deadline, 60) st.caption(f"⏱️ Session time: up to {md}:{sd:02d} remaining • Continue button appears in: {m}:{s:02d}")