Spaces:
Sleeping
Sleeping
| import json | |
| import os | |
| import re | |
| import threading | |
| import time | |
| from typing import Any, Dict, List | |
| import gradio as gr | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from core.bias_detector import BiasDetector | |
| from core.counterfactuals import CounterfactualGenerator | |
| from core.explainer import SHAPExplainer | |
| from export.csv_export import export_results_to_csv, save_results_csv | |
| from export.json_export import export_results_to_json, save_results_json | |
| class BiasGuardPro: | |
| def __init__(self, model_path: str = None): | |
| """High-level analyzer that wires detector, explainer and counterfactuals. | |
| This class exposes the main `analyze_text` method used by the UI and | |
| batch runner. It auto-detects a model path when none is provided and | |
| initializes the core components. | |
| Args: | |
| model_path: Optional path or model id. If None, will attempt to | |
| auto-detect local model artifacts. | |
| """ | |
| print("🚀 Initializing BiasGuard Pro...") | |
| if model_path is None: | |
| model_path = self._auto_detect_model_path() | |
| print(f"📁 Using model path: {model_path}") | |
| self.detector = BiasDetector(model_path) | |
| self.explainer = SHAPExplainer(model_path) | |
| self.counterfactuals = CounterfactualGenerator() | |
| print("✅ BiasGuard Pro initialized successfully!\n") | |
| def _select_top_biased_words(shap_scores: List, k: int = 3) -> List[str]: | |
| """Select up to k truly "biased" terms from SHAP results. | |
| SHAP values here explain the model's bias probability (class 1). | |
| Positive contributions increase the bias probability; negative | |
| contributions decrease it (i.e., are "unbiasing"). | |
| """ | |
| # Only exclude explicitly neutral connector terms we've observed | |
| # showing up as "Key Biased Terms". | |
| excluded_terms = {"because"} | |
| selected: List[str] = [] | |
| seen = set() | |
| # `shap_scores` is already sorted by absolute impact (Word Impact order). | |
| # We keep that order and pick the first k tokens that increase bias. | |
| for word, score in shap_scores or []: | |
| if not isinstance(word, str): | |
| continue | |
| cleaned = word.strip() | |
| if not cleaned: | |
| continue | |
| lower = cleaned.lower() | |
| # Keep only tokens that push towards the biased class. | |
| if float(score) <= 0.0: | |
| continue | |
| # Drop explicitly excluded neutral connector terms. | |
| if lower in excluded_terms: | |
| continue | |
| # Drop tokens that don't contain letters (punctuation/artifacts). | |
| if not re.search(r"[a-zA-Z]", cleaned): | |
| continue | |
| if lower in seen: | |
| continue | |
| seen.add(lower) | |
| selected.append(cleaned) | |
| if len(selected) >= k: | |
| break | |
| return selected | |
| def _auto_detect_model_path(self) -> str: | |
| """Attempt to find local model artifacts and return a suitable path. | |
| Returns a model path to use with the underlying detector. If no | |
| local files are found the default transformer identifier is returned. | |
| """ | |
| possible_paths = [".", "./models", "./model"] | |
| model_extensions = (".safetensors", ".bin", ".json") | |
| for path in possible_paths: | |
| if os.path.exists(path): | |
| files = os.listdir(path) | |
| if any(f.endswith(model_extensions) for f in files): | |
| print(f"✅ Found model files in: {path}") | |
| return path | |
| print("⚠️ No local model files found. Using default model...") | |
| return "distilbert-base-uncased" | |
| def analyze_text(self, text: str) -> Dict: | |
| """Analyze a single text for bias. | |
| Runs prediction, SHAP explanation and counterfactual generation and | |
| returns a dictionary with results and lightweight profiling timings. | |
| Args: | |
| text: Input string to analyze. | |
| Returns: | |
| Dict containing analysis results such as 'bias_probability', | |
| 'shap_scores', 'counterfactuals' and 'timings'. | |
| """ | |
| print(f"🔍 Analyzing: '{text}'") | |
| timings = {} | |
| t0 = time.perf_counter() | |
| # Run bias prediction and SHAP explainer in parallel to overlap latency | |
| from concurrent.futures import ThreadPoolExecutor | |
| with ThreadPoolExecutor(max_workers=2) as exc: | |
| fut_bias = exc.submit(self.detector.predict_bias, text) | |
| fut_shap = exc.submit(self.explainer.get_shap_values, text) | |
| t_start = time.perf_counter() | |
| bias_result = fut_bias.result() | |
| timings["predict_bias"] = time.perf_counter() - t_start | |
| t_start = time.perf_counter() | |
| shap_results = fut_shap.result() | |
| timings["get_shap_values"] = time.perf_counter() - t_start | |
| top_biased_words = self._select_top_biased_words(shap_results, k=3) | |
| print(f" Key biased words: {top_biased_words}") | |
| bias_label, bias_types = self.explainer.classify_bias( | |
| text, | |
| bias_result.get("bias_probability", 0.0) or 0.0, | |
| shap_results, | |
| ) | |
| t_start = time.perf_counter() | |
| counterfactuals = self.counterfactuals.generate_counterfactuals( | |
| text, | |
| shap_results, | |
| bias_label=bias_label, | |
| bias_types=bias_types, | |
| num_alternatives=3, | |
| ) | |
| timings["generate_counterfactuals"] = time.perf_counter() - t_start | |
| timings["analyze_text_total"] = time.perf_counter() - t0 | |
| return { | |
| "text": text, | |
| "bias_probability": bias_result.get("bias_probability"), | |
| "bias_class": bias_label, | |
| "confidence": bias_result.get("confidence"), | |
| "top_biased_words": top_biased_words, | |
| "shap_scores": shap_results[:10], | |
| "counterfactuals": counterfactuals, | |
| "timestamp": time.time(), | |
| "timings": timings, | |
| } | |
| class BiasGuardDashboard: | |
| def __init__(self, lazy_load: bool = True): | |
| """Create the dashboard wrapper that builds a Gradio UI around the | |
| BiasGuardPro analyzer and provides helpers for batch processing. | |
| """ | |
| print("🎨 Initializing BiasGuard Pro Dashboard...") | |
| self._lazy_load = bool(lazy_load) | |
| self._analyzer_lock = threading.Lock() | |
| self.analyzer = None if self._lazy_load else BiasGuardPro() | |
| self.analysis_history = [] | |
| self.last_batch_results: List[Dict] = [] | |
| self._jobs: Dict[str, Dict] = {} | |
| self.sample_texts = [ | |
| "Women should be nurses because they are compassionate.", | |
| "Men are naturally better at engineering roles.", | |
| "The female secretary was very emotional today.", | |
| ] | |
| print("✅ Dashboard initialized successfully!") | |
| def _ensure_analyzer(self): | |
| """Initialize the heavy analyzer lazily (thread-safe). | |
| On Spaces, eager model loading can cause long startup times and | |
| 'Starting...' timeouts. This ensures we only download/load models | |
| when a user actually runs an analysis. | |
| """ | |
| if self.analyzer is not None: | |
| return | |
| with self._analyzer_lock: | |
| if self.analyzer is None: | |
| print("⏳ Lazy-loading BiasGuard Pro models...") | |
| self.analyzer = BiasGuardPro() | |
| def create_bias_meter(self, bias_prob: float) -> go.Figure: | |
| """Render a Plotly gauge indicating bias severity. | |
| Args: | |
| bias_prob: Float in [0,1] representing bias probability. | |
| Returns: | |
| Plotly Figure for embedding in the Gradio UI. | |
| """ | |
| if bias_prob > 0.7: | |
| bias_color = "#dc2626" | |
| bias_label = "HIGH BIAS" | |
| elif bias_prob > 0.4: | |
| bias_color = "#f59e0b" | |
| bias_label = "MODERATE BIAS" | |
| else: | |
| bias_color = "#10b981" | |
| bias_label = "LOW BIAS" | |
| fig = go.Figure( | |
| go.Indicator( | |
| mode="gauge+number", | |
| value=bias_prob, | |
| domain={"x": [0, 1], "y": [0, 1]}, | |
| title={ | |
| "text": f"<b>{bias_label}</b>", | |
| "font": {"size": 26, "color": bias_color}, | |
| }, | |
| number={ | |
| "font": {"size": 52, "color": bias_color}, | |
| "valueformat": ".2f", | |
| }, | |
| gauge={ | |
| "axis": { | |
| "range": [0, 1], | |
| "tickwidth": 2, | |
| "tickcolor": "#e5e7eb", | |
| "tickfont": {"size": 14}, | |
| }, | |
| "bar": {"color": bias_color, "thickness": 0.75}, | |
| "bgcolor": "white", | |
| "borderwidth": 3, | |
| "bordercolor": "#e5e7eb", | |
| "steps": [ | |
| {"range": [0, 0.4], "color": "#d1fae5"}, | |
| {"range": [0.4, 0.7], "color": "#fef3c7"}, | |
| {"range": [0.7, 1], "color": "#fee2e2"}, | |
| ], | |
| "threshold": { | |
| "line": {"color": bias_color, "width": 5}, | |
| "thickness": 0.8, | |
| "value": bias_prob, | |
| }, | |
| }, | |
| ) | |
| ) | |
| fig.update_layout( | |
| height=350, | |
| autosize=False, | |
| margin=dict(l=20, r=20, t=80, b=20), | |
| paper_bgcolor="white", | |
| plot_bgcolor="white", | |
| font={"family": "Inter, system-ui, sans-serif", "size": 14}, | |
| ) | |
| return fig | |
| def create_shap_chart(self, shap_scores: List) -> go.Figure: | |
| """Create a horizontal bar chart visualizing SHAP word impacts. | |
| Args: | |
| shap_scores: List of (word, score) tuples. | |
| Returns: | |
| Plotly Figure to show in the Word Impact tab. | |
| """ | |
| if not shap_scores: | |
| fig = go.Figure() | |
| fig.add_annotation( | |
| text="<b>No significant biased words detected</b>", | |
| xref="paper", | |
| yref="paper", | |
| x=0.5, | |
| y=0.5, | |
| xanchor="center", | |
| yanchor="middle", | |
| showarrow=False, | |
| font=dict(size=18, color="#64748b"), | |
| ) | |
| fig.update_layout( | |
| height=400, | |
| autosize=False, | |
| paper_bgcolor="white", | |
| plot_bgcolor="white", | |
| ) | |
| return fig | |
| words = [word for word, score in shap_scores[:8]] | |
| scores = [score for word, score in shap_scores[:8]] | |
| colors = [ | |
| "#dc2626" if s > 0.1 else "#f59e0b" if s > 0 else "#10b981" for s in scores | |
| ] | |
| fig = go.Figure( | |
| go.Bar( | |
| x=scores, | |
| y=words, | |
| orientation="h", | |
| marker=dict(color=colors, line=dict(width=0)), | |
| hovertemplate="<b>%{y}</b><br>Impact: %{x:.3f}<extra></extra>", | |
| ) | |
| ) | |
| fig.update_layout( | |
| title={ | |
| "text": "<b>Word Impact Analysis</b>", | |
| "x": 0.5, | |
| "xanchor": "center", | |
| "font": {"size": 20, "color": "#1e293b"}, | |
| }, | |
| xaxis=dict( | |
| title="<b>SHAP Score</b>", | |
| showgrid=True, | |
| gridwidth=1, | |
| gridcolor="#f1f5f9", | |
| zeroline=True, | |
| zerolinewidth=2, | |
| zerolinecolor="#cbd5e1", | |
| ), | |
| yaxis=dict(title="", showgrid=False), | |
| height=400, | |
| autosize=False, | |
| showlegend=False, | |
| plot_bgcolor="white", | |
| paper_bgcolor="white", | |
| margin=dict(l=120, r=30, t=60, b=50), | |
| font={"family": "Inter, system-ui, sans-serif", "size": 14}, | |
| ) | |
| return fig | |
| def highlight_biased_words(self, text: str, shap_scores: List) -> str: | |
| """Return an HTML string with biased words highlighted. | |
| Highlights are based on the absolute SHAP score and colored according | |
| to impact (high/medium/low). If no scores are present the original | |
| text is returned inside a styled container. | |
| """ | |
| if not shap_scores: | |
| return f""" | |
| <div style='padding: 24px; background: white; border-radius: 16px; border: 2px solid #e2e8f0; font-size: 16px; line-height: 1.8; color: #334155;'> | |
| {text} | |
| </div> | |
| """ | |
| word_scores = { | |
| word.lower(): abs(score) for word, score in shap_scores if score > 0 | |
| } | |
| words = text.split() | |
| highlighted_words = [] | |
| for word in words: | |
| clean_word = re.sub(r"[^\w]", "", word.lower()) | |
| if clean_word in word_scores: | |
| score = word_scores[clean_word] | |
| if score > 0.15: | |
| color = "#dc2626" | |
| label = "High" | |
| elif score > 0.05: | |
| color = "#f59e0b" | |
| label = "Medium" | |
| else: | |
| color = "#fbbf24" | |
| label = "Low" | |
| highlighted_words.append( | |
| f"<mark style='background: {color}; color: white; padding: 4px 10px; border-radius: 8px; font-weight: 600; margin: 0 2px;' title='{label} impact'>{word}</mark>" | |
| ) | |
| else: | |
| highlighted_words.append(word) | |
| highlighted_text = " ".join(highlighted_words) | |
| return f""" | |
| <div style='padding: 28px; background: white; border-radius: 20px; border: 2px solid #e2e8f0; font-size: 17px; line-height: 2; color: #1e293b; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);'> | |
| {highlighted_text} | |
| </div> | |
| """ | |
| def analyze_text_for_dashboard( | |
| self, text: str, progress=gr.Progress() | |
| ) -> Dict[str, Any]: | |
| """Wrapper used by the Gradio UI to show progress and return UI pieces. | |
| This method calls the analyzer and then builds figures/HTML snippets | |
| used in the dashboard. It accepts a Gradio Progress object to report | |
| staged progress to the front-end. | |
| """ | |
| if not text.strip(): | |
| return {"error": "Please enter text to analyze"} | |
| try: | |
| # Ensure heavy model components are loaded only when needed. | |
| progress(0.05, desc="Loading models...") | |
| self._ensure_analyzer() | |
| # Show progress | |
| ui_timings = {} | |
| progress(0.2, desc="Detecting bias...") | |
| t_sleep = time.perf_counter() | |
| time.sleep(0.3) | |
| ui_timings["progress_sleep_1"] = time.perf_counter() - t_sleep | |
| result = self.analyzer.analyze_text(text) | |
| progress(0.6, desc="Analyzing word impact...") | |
| t_sleep = time.perf_counter() | |
| time.sleep(0.3) | |
| ui_timings["progress_sleep_2"] = time.perf_counter() - t_sleep | |
| self.analysis_history.append(result) | |
| if len(self.analysis_history) > 10: | |
| self.analysis_history.pop(0) | |
| progress(0.8, desc="Generating alternatives...") | |
| t_sleep = time.perf_counter() | |
| time.sleep(0.2) | |
| ui_timings["progress_sleep_3"] = time.perf_counter() - t_sleep | |
| t_start = time.perf_counter() | |
| bias_meter = self.create_bias_meter(result["bias_probability"]) | |
| ui_timings["create_bias_meter"] = time.perf_counter() - t_start | |
| t_start = time.perf_counter() | |
| shap_chart = self.create_shap_chart(result["shap_scores"]) | |
| ui_timings["create_shap_chart"] = time.perf_counter() - t_start | |
| t_start = time.perf_counter() | |
| highlighted_text = self.highlight_biased_words(text, result["shap_scores"]) | |
| ui_timings["highlight_biased_words"] = time.perf_counter() - t_start | |
| # Create summary card with fixed font colors | |
| bias_level = ( | |
| "highly biased" | |
| if result["bias_probability"] > 0.7 | |
| else ( | |
| "moderately biased" | |
| if result["bias_probability"] > 0.4 | |
| else "relatively neutral" | |
| ) | |
| ) | |
| if result["bias_probability"] > 0.7: | |
| summary_gradient = "linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)" | |
| border_color = "#fca5a5" | |
| text_color = "#991b1b" | |
| emoji = "🚨" | |
| elif result["bias_probability"] > 0.4: | |
| summary_gradient = "linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)" | |
| border_color = "#fcd34d" | |
| text_color = "#92400e" | |
| emoji = "⚠️" | |
| else: | |
| summary_gradient = "linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)" | |
| border_color = "#6ee7b7" | |
| text_color = "#065f46" | |
| emoji = "✅" | |
| summary_html = f""" | |
| <div style='padding: 32px; background: {summary_gradient}; border-radius: 24px; border: 3px solid {border_color}; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);'> | |
| <div style='display: flex; align-items: center; gap: 16px; margin-bottom: 20px;'> | |
| <span style='font-size: 48px;'>{emoji}</span> | |
| <div> | |
| <h3 style='margin: 0; color: {text_color}; font-size: 24px; font-weight: 800;'>Analysis Complete</h3> | |
| <p style='margin: 4px 0 0 0; color: {text_color}; font-size: 16px; opacity: 0.8;'>Bias detection results</p> | |
| </div> | |
| </div> | |
| <div style='background: white; padding: 20px; border-radius: 16px; margin-bottom: 16px;'> | |
| <div style='font-size: 16px; color: #334155; margin-bottom: 12px;'> | |
| This text is <strong style='color: {text_color}; font-size: 18px;'>{bias_level}</strong> | |
| </div> | |
| <div style='display: flex; align-items: center; gap: 12px;'> | |
| <span style='font-size: 14px; color: #64748b; font-weight: 600;'>Bias Score:</span> | |
| <span style='font-size: 32px; font-weight: 900; color: {text_color};'>{result['bias_probability']:.3f}</span> | |
| </div> | |
| </div> | |
| <div style='display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 14px;'> | |
| <div style='background: rgba(255,255,255,0.7); padding: 12px; border-radius: 12px;'> | |
| <strong style='color: #1e293b;'>Classification:</strong> <span style='color: {text_color}; font-weight: 600;'>{result['bias_class']}</span> | |
| </div> | |
| <div style='background: rgba(255,255,255,0.7); padding: 12px; border-radius: 12px;'> | |
| <strong style='color: #1e293b;'>Confidence:</strong> <span style='color: {text_color}; font-weight: 600;'>{result['confidence']:.1%}</span> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| # Create counterfactuals HTML | |
| counterfactuals_html = "<div style='margin: 20px 0;'>" | |
| if result["counterfactuals"]: | |
| t_start = time.perf_counter() | |
| for i, cf in enumerate(result["counterfactuals"], 1): | |
| counterfactuals_html += f""" | |
| <div style='margin: 20px 0; padding: 24px; background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%); border-left: 5px solid #10b981; border-radius: 16px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);'> | |
| <div style='font-size: 13px; color: #047857; font-weight: 700; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em;'> | |
| ✨ Alternative {i} | |
| </div> | |
| <div style='font-size: 17px; color: #065f46; line-height: 1.7; font-weight: 500;'>{cf}</div> | |
| </div> | |
| """ | |
| ui_timings["build_counterfactuals_html"] = time.perf_counter() - t_start | |
| else: | |
| counterfactuals_html += """ | |
| <div style='padding: 32px; background: #f8fafc; border-radius: 20px; text-align: center; border: 3px dashed #cbd5e1;'> | |
| <div style='font-size: 56px; margin-bottom: 16px;'>💡</div> | |
| <div style='font-size: 16px; color: #64748b; font-weight: 600;'>No alternative suggestions available</div> | |
| </div> | |
| """ | |
| counterfactuals_html += "</div>" | |
| # Top words display | |
| top_words_html = "<div style='display: flex; flex-wrap: wrap; gap: 8px;'>" | |
| t_start = time.perf_counter() | |
| for word in result["top_biased_words"]: | |
| top_words_html += f""" | |
| <span style='padding: 8px 16px; background: linear-gradient(135deg, #fee2e2, #fecaca); color: #991b1b; border-radius: 12px; font-weight: 700; font-size: 14px; border: 2px solid #fca5a5;'> | |
| "{word}" | |
| </span> | |
| """ | |
| ui_timings["build_top_words_html"] = time.perf_counter() - t_start | |
| top_words_html += ( | |
| "</div>" | |
| if result["top_biased_words"] | |
| else "<div style='color: #94a3b8; font-style: italic;'>None detected</div>" | |
| ) | |
| progress(1.0, desc="Complete!") | |
| # Merge analyzer timings and ui timings for a quick profile | |
| profiler = {} | |
| if isinstance(result, dict) and "timings" in result: | |
| profiler.update( | |
| {f"analyzer.{k}": v for k, v in result["timings"].items()} | |
| ) | |
| profiler.update({f"ui.{k}": v for k, v in ui_timings.items()}) | |
| # Print a sorted timing report to console | |
| try: | |
| sorted_times = sorted( | |
| profiler.items(), key=lambda x: x[1], reverse=True | |
| ) | |
| print("\n📈 Profiling report (descending):") | |
| for name, dur in sorted_times: | |
| print(f" - {name}: {dur:.4f}s") | |
| except Exception: | |
| sorted_times = [] | |
| # Small HTML snippet to show top 5 timings in the UI | |
| profiling_html = "<div style='margin-top:12px; padding: 12px; border-radius: 12px; background: #f8fafc; border: 2px solid #e2e8f0; font-size:13px;'>" | |
| profiling_html += "<strong>⏱️ Timing Breakdown:</strong><br/>" | |
| try: | |
| for name, dur in sorted_times[:5]: | |
| profiling_html += f"<div style='display:flex; justify-content:space-between; gap:12px;'><span style='color:#334155;'>{name}</span><span style='font-weight:800;'>{dur:.3f}s</span></div>" | |
| except Exception: | |
| profiling_html += "<div>Profiling not available</div>" | |
| profiling_html += "</div>" | |
| return { | |
| "success": True, | |
| "bias_meter": bias_meter, | |
| "shap_chart": shap_chart, | |
| "highlighted_text": highlighted_text, | |
| "summary_html": summary_html, | |
| "counterfactuals_html": counterfactuals_html, | |
| "top_words_html": top_words_html, | |
| "bias_probability": result["bias_probability"], | |
| "bias_class": result["bias_class"], | |
| "profiling_html": profiling_html, | |
| "profiling_data": profiler, | |
| } | |
| except Exception as e: | |
| return {"error": f"Analysis failed: {str(e)}"} | |
| def create_dashboard(self): | |
| """Build and return the Gradio Blocks dashboard instance. | |
| This method constructs the entire UI and wires handlers. It returns | |
| the Gradio `Blocks` instance which can be launched by the caller. | |
| """ | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap'); | |
| :root { | |
| --background-fill-primary: #ffffff !important; | |
| --body-background-fill: #ffffff !important; | |
| --background-fill-secondary: #ffffff !important; | |
| --app-background: #ffffff !important; | |
| --body-text-color: #1e293b !important; | |
| --body-text-color-subdued: #64748b !important; | |
| /* Force light mode variables commonly used by Gradio */ | |
| --input-background-fill: #ffffff !important; | |
| --input-background-fill-hover: #ffffff !important; | |
| --input-background-fill-focus: #ffffff !important; | |
| --block-background-fill: #ffffff !important; | |
| --block-background-fill-hover: #ffffff !important; | |
| --panel-background-fill: #ffffff !important; | |
| } | |
| html { | |
| color-scheme: light !important; | |
| } | |
| input, textarea, select, button { | |
| color-scheme: light !important; | |
| } | |
| html, body, .gradio-root, .gradio-container { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 50%, #f3e8ff 100%) !important; | |
| min-height: 100vh !important; | |
| color: #1e293b !important; | |
| } | |
| :root .dark, .gradio-root.dark, .dark, .dark .gradio-root, .dark .gradio-container, [data-theme="dark"] { | |
| --background-fill-primary: #ffffff !important; | |
| --body-background-fill: #ffffff !important; | |
| --background-fill-secondary: #ffffff !important; | |
| --app-background: #ffffff !important; | |
| --body-text-color: #1e293b !important; | |
| --body-text-color-subdued: #64748b !important; | |
| /* Even if Gradio toggles dark, keep inputs/blocks white */ | |
| --input-background-fill: #ffffff !important; | |
| --input-background-fill-hover: #ffffff !important; | |
| --input-background-fill-focus: #ffffff !important; | |
| --block-background-fill: #ffffff !important; | |
| --block-background-fill-hover: #ffffff !important; | |
| --panel-background-fill: #ffffff !important; | |
| color-scheme: light !important; | |
| background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 50%, #f3e8ff 100%) !important; | |
| color: #1e293b !important; | |
| } | |
| * { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; | |
| } | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| margin: 0 auto !important; | |
| background: linear-gradient(135deg, #f8fafc 0%, #e0f2fe 50%, #f3e8ff 100%) !important; | |
| } | |
| .input-textarea, .output-textarea, textarea { | |
| background: white !important; | |
| color: #1e293b !important; | |
| } | |
| .prose { | |
| color: #1e293b !important; | |
| } | |
| .main-header { | |
| background: linear-gradient(135deg, #7c3aed 0%, #a855f7 50%, #ec4899 100%); | |
| padding: 48px 40px; | |
| border-radius: 28px; | |
| margin-bottom: 32px; | |
| box-shadow: 0 20px 60px rgba(124, 58, 237, 0.4); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .main-header h1 { | |
| color: white !important; | |
| } | |
| .main-header p { | |
| color: rgba(255,255,255,0.95) !important; | |
| } | |
| .main-header::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: radial-gradient(circle at 20% 50%, rgba(255,255,255,0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(255,255,255,0.1) 0%, transparent 50%); | |
| opacity: 0.6; | |
| } | |
| .input-card { | |
| background: white !important; | |
| border-radius: 24px; | |
| padding: 32px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| border: 2px solid #e2e8f0; | |
| } | |
| /* Force ALL children to white background */ | |
| .input-card *, | |
| .input-card > div, | |
| .input-card div, | |
| .input-card .gr-row, | |
| .input-card [class*="row"], | |
| .input-card [class*="container"], | |
| .input-card [class*="block"], | |
| .input-card [class*="group"] { | |
| background: white !important; | |
| color: #1e293b !important; | |
| } | |
| /* Ensure text elements have proper color */ | |
| .input-card h3, | |
| .input-card h4, | |
| .input-card p, | |
| .input-card span, | |
| .input-card label { | |
| color: #1e293b !important; | |
| background: transparent !important; | |
| } | |
| /* Keep buttons styled properly */ | |
| .input-card button { | |
| background: initial !important; | |
| } | |
| /* Keep textareas white with dark text */ | |
| .input-card textarea { | |
| background: white !important; | |
| color: #1e293b !important; | |
| } | |
| /* CRITICAL FIX: Force white background on ALL textareas */ | |
| textarea, | |
| .input-textarea textarea, | |
| .input-card textarea, | |
| .gr-textbox textarea, | |
| .gr-text-input textarea, | |
| textbox textarea, | |
| [class*="textbox"] textarea, | |
| [class*="input"] textarea { | |
| border-radius: 16px !important; | |
| border: 2px solid #e2e8f0 !important; | |
| padding: 18px !important; | |
| font-size: 15px !important; | |
| line-height: 1.7 !important; | |
| transition: all 0.3s ease !important; | |
| background: white !important; | |
| color: #1e293b !important; | |
| -webkit-text-fill-color: #1e293b !important; | |
| } | |
| /* Additional targeting for Gradio's internal wrapper classes */ | |
| .input-card .gr-box, | |
| .input-card .gr-form, | |
| .input-card [class*="block"], | |
| .input-card [class*="container"] { | |
| background: white !important; | |
| } | |
| /* Target the specific Gradio textarea wrapper */ | |
| label[for*="textbox"] + div, | |
| .gr-text-input, | |
| .gr-textbox { | |
| background: white !important; | |
| } | |
| textarea:focus, .input-textarea textarea:focus { | |
| border-color: #7c3aed !important; | |
| box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.1) !important; | |
| background: white !important; | |
| } | |
| textarea::placeholder { | |
| color: #94a3b8 !important; | |
| } | |
| /* FIX: Black bar between Quick Examples and buttons */ | |
| .input-card .gr-row, | |
| .input-card [class*="row"], | |
| .input-card > * { | |
| background: white !important; | |
| } | |
| /* Target Gradio's Row component specifically */ | |
| div[class*="row"] { | |
| background: transparent !important; | |
| } | |
| .input-card div[class*="row"] { | |
| background: white !important; | |
| } | |
| /* Enhanced Sample Button Styling */ | |
| .sample-btn, button.sample-btn { | |
| background: linear-gradient(135deg, #ede9fe, #ddd6fe) !important; | |
| color: #5b21b6 !important; | |
| border: 2px solid #c4b5fd !important; | |
| border-radius: 12px !important; | |
| padding: 10px 18px !important; | |
| font-weight: 600 !important; | |
| font-size: 13px !important; | |
| transition: all 0.3s ease !important; | |
| min-width: 220px !important; | |
| white-space: normal !important; | |
| word-wrap: break-word !important; | |
| text-align: left !important; | |
| line-height: 1.4 !important; | |
| height: auto !important; | |
| min-height: 48px !important; | |
| } | |
| .sample-btn:hover, button.sample-btn:hover { | |
| background: linear-gradient(135deg, #ddd6fe, #c4b5fd) !important; | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3) !important; | |
| } | |
| .primary-btn, button.primary-btn { | |
| background: linear-gradient(135deg, #7c3aed, #a855f7) !important; | |
| color: white !important; | |
| border: none !important; | |
| border-radius: 18px !important; | |
| padding: 16px 40px !important; | |
| font-size: 16px !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 0.02em !important; | |
| transition: all 0.3s ease !important; | |
| box-shadow: 0 10px 25px rgba(124, 58, 237, 0.4) !important; | |
| } | |
| .primary-btn:hover, button.primary-btn:hover { | |
| transform: translateY(-3px) scale(1.02) !important; | |
| box-shadow: 0 15px 35px rgba(124, 58, 237, 0.5) !important; | |
| } | |
| .secondary-btn, button.secondary-btn { | |
| background: #f1f5f9 !important; | |
| color: #475569 !important; | |
| border: 2px solid #cbd5e1 !important; | |
| border-radius: 14px !important; | |
| padding: 12px 24px !important; | |
| font-weight: 600 !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .secondary-btn:hover, button.secondary-btn:hover { | |
| background: #e2e8f0 !important; | |
| border-color: #94a3b8 !important; | |
| } | |
| .info-card { | |
| background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); | |
| border-radius: 24px; | |
| padding: 28px; | |
| border: 3px solid #fcd34d; | |
| box-shadow: 0 8px 20px rgba(245, 158, 11, 0.25); | |
| } | |
| .step-card { | |
| background: rgba(255,255,255,0.85); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| padding: 16px; | |
| margin: 12px 0; | |
| border: 2px solid rgba(255,255,255,0.5); | |
| } | |
| .results-section { | |
| animation: slideUp 0.6s ease-out; | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .tab-nav button { | |
| border-radius: 14px 14px 0 0 !important; | |
| font-weight: 600 !important; | |
| padding: 14px 28px !important; | |
| transition: all 0.3s ease !important; | |
| border: 2px solid transparent !important; | |
| font-size: 15px !important; | |
| background: #f8fafc !important; | |
| color: #475569 !important; | |
| } | |
| .tab-nav button[aria-selected="true"] { | |
| background: linear-gradient(135deg, #7c3aed, #a855f7) !important; | |
| color: white !important; | |
| border-bottom: 2px solid #7c3aed !important; | |
| } | |
| .tab-nav button:hover { | |
| background: #e2e8f0 !important; | |
| color: #1e293b !important; | |
| } | |
| .tab-nav button[aria-selected="true"]:hover { | |
| background: linear-gradient(135deg, #7c3aed, #a855f7) !important; | |
| color: white !important; | |
| } | |
| /* Enhanced tab styling for Detailed Analysis */ | |
| .tab-nav button { | |
| border-bottom: 3px solid transparent !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .tab-nav button[aria-selected="true"] { | |
| border-bottom: 3px solid #3b82f6 !important; | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(59, 130, 246, 0.05)) !important; | |
| font-weight: 700 !important; | |
| } | |
| .tab-nav button:hover { | |
| border-bottom: 3px solid rgba(59, 130, 246, 0.5) !important; | |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.05), rgba(59, 130, 246, 0.02)) !important; | |
| } | |
| .tabitem { | |
| background: white !important; | |
| border-radius: 0 0 20px 20px !important; | |
| padding: 24px !important; | |
| border-top: 2px solid #e2e8f0 !important; | |
| } | |
| .result-card { | |
| background: white; | |
| border-radius: 24px; | |
| padding: 28px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| border: 2px solid #e2e8f0; | |
| margin: 16px 0; | |
| } | |
| .plot-container { | |
| background: white !important; | |
| border-radius: 20px !important; | |
| padding: 16px !important; | |
| } | |
| .block.svelte-1svsvh2, .svelte-1svsvh2, .gradio-block, .gradio-block * { | |
| background: white !important; | |
| color: #1e293b !important; | |
| } | |
| /* Comprehensive text color overrides for white theme */ | |
| .gradio-container, .gradio-container *, | |
| .gr-form, .gr-form *, | |
| .gr-box, .gr-box *, | |
| .gr-panel, .gr-panel *, | |
| .gr-text, .gr-textbox, .gr-textbox *, | |
| .gr-button, .gr-button *, | |
| .gr-dropdown, .gr-dropdown *, | |
| .gr-radio, .gr-radio *, | |
| .gr-checkbox, .gr-checkbox *, | |
| .gr-slider, .gr-slider *, | |
| .gr-file, .gr-file *, | |
| .gr-upload, .gr-upload *, | |
| .gr-dataframe, .gr-dataframe *, | |
| .gr-html, .gr-html *, | |
| .gr-markdown, .gr-markdown *, | |
| .gr-json, .gr-json *, | |
| .gr-label, .gr-label *, | |
| .gr-plot, .gr-plot *, | |
| .gr-image, .gr-image *, | |
| .gr-audio, .gr-audio *, | |
| .gr-video, .gr-video *, | |
| .gr-gallery, .gr-gallery *, | |
| label, input, textarea, select, button, | |
| div, span, p, h1, h2, h3, h4, h5, h6 { | |
| color: #1e293b !important; | |
| } | |
| .block.svelte-1svsvh2 { | |
| position: relative !important; | |
| margin: 0 !important; | |
| box-shadow: var(--block-shadow, 0 4px 20px rgba(0,0,0,0.06)) !important; | |
| border-width: var(--block-border-width, 1px) !important; | |
| border-color: var(--block-border-color, #e2e8f0) !important; | |
| border-radius: var(--block-radius, 12px) !important; | |
| background: #ffffff !important; | |
| width: 100% !important; | |
| line-height: var(--line-sm, 1.2) !important; | |
| } | |
| button.svelte-i00v67.svelte-i00v67, | |
| button.svelte-i00v67, | |
| .gr-button, .gr-button *, | |
| .sample-btn, button.sample-btn { | |
| color: #0f172a !important; | |
| text-shadow: none !important; | |
| } | |
| .input-card, | |
| .input-card *:not(textarea):not(button) { | |
| background: white !important; | |
| color: #0f172a !important; | |
| } | |
| .input-card textarea { | |
| background: white !important; | |
| color: #1e293b !important; | |
| } | |
| /* Enhanced Batch Analysis Tab */ | |
| .batch-analysis-card { | |
| background: white !important; | |
| border-radius: 24px; | |
| padding: 0px; | |
| } | |
| .batch-header { | |
| background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); | |
| border-radius: 20px; | |
| padding: 32px; | |
| margin-bottom: 32px; | |
| border: 2px solid #cbd5e1; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.04); | |
| } | |
| .batch-section { | |
| background: white; | |
| border-radius: 16px; | |
| padding: 24px; | |
| margin-bottom: 24px; | |
| border: 2px solid #cbd5e1; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.03); | |
| } | |
| .batch-section:hover { | |
| border-color: #94a3b8; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.06); | |
| transition: all 0.3s ease; | |
| } | |
| .upload-zone { | |
| background: linear-gradient(135deg, #fafafa, #f5f5f5) !important; | |
| border: 2px dashed #94a3b8 !important; | |
| border-radius: 16px !important; | |
| padding: 32px !important; | |
| text-align: center !important; | |
| transition: all 0.3s ease !important; | |
| cursor: pointer !important; | |
| } | |
| .upload-zone:hover { | |
| background: linear-gradient(135deg, #f0f9ff, #e0f2fe) !important; | |
| border-color: #60a5fa !important; | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15) !important; | |
| } | |
| .progress-card { | |
| background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%); | |
| border-radius: 16px; | |
| padding: 24px; | |
| border: 2px solid #cbd5e1; | |
| margin-bottom: 16px; | |
| } | |
| /* Batch Analysis Input Fields Styling */ | |
| #batch-tab .gr-box, | |
| #batch-tab .gr-form, | |
| #batch-tab input[type="number"], | |
| #batch-tab input[type="text"], | |
| #batch-tab textarea { | |
| background: white !important; | |
| border: 2px solid #cbd5e1 !important; | |
| border-radius: 12px !important; | |
| padding: 14px 16px !important; | |
| font-size: 14px !important; | |
| color: #1e293b !important; | |
| font-weight: 500 !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| #batch-tab input[type="number"]:focus, | |
| #batch-tab input[type="text"]:focus, | |
| #batch-tab textarea:focus { | |
| background: white !important; | |
| border-color: #7c3aed !important; | |
| box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1) !important; | |
| outline: none !important; | |
| } | |
| #batch-tab input[type="number"]:disabled, | |
| #batch-tab input[type="text"]:disabled, | |
| #batch-tab textarea:disabled { | |
| background: #f8fafc !important; | |
| color: #64748b !important; | |
| border-color: #cbd5e1 !important; | |
| cursor: not-allowed !important; | |
| font-weight: 600 !important; | |
| } | |
| #batch-tab input::placeholder, | |
| #batch-tab textarea::placeholder { | |
| color: #94a3b8 !important; | |
| font-weight: 400 !important; | |
| } | |
| #batch-tab label { | |
| font-weight: 700 !important; | |
| color: #1e293b !important; | |
| font-size: 14px !important; | |
| margin-bottom: 8px !important; | |
| letter-spacing: 0.01em !important; | |
| } | |
| #batch-tab .gr-file { | |
| background: white !important; | |
| border: 2px solid #cbd5e1 !important; | |
| border-radius: 12px !important; | |
| padding: 12px !important; | |
| } | |
| /* Progress and Export Input Groups */ | |
| .progress-inputs, | |
| .export-inputs { | |
| background: #f8fafc; | |
| border-radius: 14px; | |
| padding: 20px; | |
| border: 2px solid #cbd5e1; | |
| margin-top: 12px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.05); | |
| } | |
| .progress-inputs > *, | |
| .export-inputs > * { | |
| margin-bottom: 12px; | |
| } | |
| .progress-inputs > *:last-child, | |
| .export-inputs > *:last-child { | |
| margin-bottom: 0; | |
| } | |
| .export-section { | |
| background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); | |
| border-radius: 16px; | |
| padding: 20px; | |
| border: 2px solid #fbbf24; | |
| } | |
| .batch-input-section { | |
| background: #f8fafc; | |
| border-radius: 16px; | |
| padding: 24px; | |
| border: 2px solid #cbd5e1; | |
| margin-bottom: 20px; | |
| } | |
| /* Section Headers in Batch Tab */ | |
| .batch-section-header { | |
| background: linear-gradient(135deg, #f8fafc, #f1f5f9); | |
| border-radius: 12px; | |
| padding: 16px 20px; | |
| margin-bottom: 16px; | |
| border-left: 4px solid #7c3aed; | |
| border: 2px solid #cbd5e1; | |
| border-left: 4px solid #7c3aed; | |
| } | |
| .batch-section-header h4 { | |
| margin: 0; | |
| font-size: 16px; | |
| font-weight: 800; | |
| color: #1e293b; | |
| } | |
| .batch-section-header p { | |
| margin: 4px 0 0 0; | |
| font-size: 13px; | |
| color: #64748b; | |
| } | |
| .legend-item { | |
| display: inline-flex; | |
| align-items: center; | |
| margin: 0 12px; | |
| padding: 6px 14px; | |
| border-radius: 10px; | |
| font-weight: 600; | |
| font-size: 13px; | |
| } | |
| .footer { | |
| margin-top: 48px; | |
| padding: 32px; | |
| background: linear-gradient(135deg, #f1f5f9, #e2e8f0); | |
| border-radius: 24px; | |
| text-align: center; | |
| } | |
| .footer a { | |
| color: #7c3aed !important; | |
| text-decoration: none !important; | |
| font-weight: 600 !important; | |
| transition: color 0.3s ease !important; | |
| } | |
| .footer a:hover { | |
| color: #5b21b6 !important; | |
| } | |
| input[type="number"] { | |
| background: white !important; | |
| color: #1e293b !important; | |
| border: 2px solid #e2e8f0 !important; | |
| border-radius: 12px !important; | |
| padding: 12px !important; | |
| } | |
| .input-textbox input, input[type="text"] { | |
| background: white !important; | |
| color: #1e293b !important; | |
| border: 2px solid #e2e8f0 !important; | |
| border-radius: 12px !important; | |
| padding: 12px !important; | |
| } | |
| .input-textbox input:focus, input[type="text"]:focus { | |
| border-color: #7c3aed !important; | |
| box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1) !important; | |
| } | |
| label { | |
| color: #334155 !important; | |
| font-weight: 600 !important; | |
| font-size: 14px !important; | |
| } | |
| /* Smooth scroll behavior */ | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| /* Loading spinner animation */ | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* | |
| Plotly/Gradio: prevent resize feedback loops that can cause plots to | |
| continuously grow in height (observed in Spaces). | |
| We clamp the plot containers to the intended fixed heights. | |
| */ | |
| #bias-meter-plot { | |
| height: 350px !important; | |
| max-height: 350px !important; | |
| overflow: hidden !important; | |
| } | |
| #shap-chart-plot { | |
| height: 400px !important; | |
| max-height: 400px !important; | |
| overflow: hidden !important; | |
| } | |
| #bias-meter-plot .js-plotly-plot, | |
| #bias-meter-plot .plotly-graph-div, | |
| #shap-chart-plot .js-plotly-plot, | |
| #shap-chart-plot .plotly-graph-div { | |
| height: 100% !important; | |
| max-height: 100% !important; | |
| } | |
| """ | |
| force_light_js = """ | |
| () => { | |
| let applying = false; | |
| const getElements = () => [ | |
| document.documentElement, | |
| document.body, | |
| document.querySelector('.gradio-root'), | |
| document.querySelector('.gradio-container') | |
| ].filter(Boolean); | |
| const applyLight = () => { | |
| if (applying) return; | |
| applying = true; | |
| try { | |
| for (const el of getElements()) { | |
| if (el.classList && el.classList.contains('dark')) { | |
| el.classList.remove('dark'); | |
| } | |
| // Avoid re-setting attributes/styles to the same value; | |
| // doing so can trigger MutationObserver loops. | |
| if (el.dataset && el.dataset.theme !== 'light') { | |
| el.dataset.theme = 'light'; | |
| } | |
| if (el.style && el.style.colorScheme !== 'light') { | |
| el.style.colorScheme = 'light'; | |
| } | |
| } | |
| } finally { | |
| applying = false; | |
| } | |
| }; | |
| applyLight(); | |
| const observer = new MutationObserver((mutations) => { | |
| for (const m of mutations) { | |
| if (m.type !== 'attributes') continue; | |
| const t = m.target; | |
| const attr = m.attributeName; | |
| if (attr === 'class' && t.classList && t.classList.contains('dark')) { | |
| applyLight(); | |
| return; | |
| } | |
| if (attr === 'data-theme' && t.getAttribute && t.getAttribute('data-theme') === 'dark') { | |
| applyLight(); | |
| return; | |
| } | |
| } | |
| }); | |
| observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); | |
| try { | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => applyLight()); | |
| } catch (e) { | |
| /* no-op */ | |
| } | |
| } | |
| """ | |
| # Gradio 6.x expects theme/css/js passed to launch() instead of Blocks(). | |
| # Store them so both Spaces entrypoint and local launcher can use them. | |
| self._launch_theme = gr.themes.Base() | |
| self._launch_css = custom_css | |
| self._launch_js = force_light_js | |
| with gr.Blocks(title="BiasGuard Pro") as demo: | |
| # Header | |
| gr.HTML( | |
| """ | |
| <div class='main-header'> | |
| <div style='position: relative; z-index: 1;'> | |
| <div style='display: flex; align-items: center; gap: 20px; margin-bottom: 12px;'> | |
| <div style='background: rgba(255,255,255,0.2); backdrop-filter: blur(10px); padding: 16px; border-radius: 20px;'> | |
| <span style='font-size: 48px;'>🛡️</span> | |
| </div> | |
| <div> | |
| <h1 style='margin: 0; font-size: 56px; font-weight: 900; color: white !important; text-shadow: 0 4px 12px rgba(0,0,0,0.2); letter-spacing: -0.02em;'> | |
| BiasGuard Pro | |
| </h1> | |
| <p style='margin: 8px 0 0 0; font-weight: 500; color: rgba(255,255,255,0.95) !important; font-size: 20px;'> | |
| AI-Powered Bias Detection & Mitigation | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(equal_height=False): | |
| # Main Content Area (Left) | |
| with gr.Column(scale=2): | |
| with gr.Group(elem_classes="input-card"): | |
| gr.HTML( | |
| """ | |
| <div style="background: white !important; padding: 0; margin-bottom: 20px;"> | |
| <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;"> | |
| <div style="font-size: 32px;">✨</div> | |
| <h3 style="margin: 0; font-size: 24px; font-weight: 800; color: #1e293b !important;">Analyze Text</h3> | |
| </div> | |
| <p style="margin: 0; color: #64748b !important; font-size: 14px;">Enter or paste text to detect gender bias</p> | |
| </div> | |
| """ | |
| ) | |
| text_input = gr.Textbox( | |
| label="", | |
| placeholder="Example: 'Women should be nurses because they are compassionate and caring...'", | |
| lines=6, | |
| max_lines=10, | |
| show_label=False, | |
| ) | |
| gr.HTML( | |
| "<div style='margin: 20px 0 12px 0; font-size: 13px; font-weight: 700; color: #475569;'>💡 Quick Examples:</div>" | |
| ) | |
| with gr.Row(): | |
| sample_btn_1 = gr.Button( | |
| self.sample_texts[0], | |
| size="sm", | |
| elem_classes="sample-btn", | |
| ) | |
| sample_btn_2 = gr.Button( | |
| self.sample_texts[1], | |
| size="sm", | |
| elem_classes="sample-btn", | |
| ) | |
| sample_btn_3 = gr.Button( | |
| self.sample_texts[2], | |
| size="sm", | |
| elem_classes="sample-btn", | |
| ) | |
| with gr.Row(): | |
| analyze_btn = gr.Button( | |
| "🔍 Analyze for Bias", | |
| variant="primary", | |
| size="lg", | |
| elem_classes="primary-btn", | |
| scale=3, | |
| ) | |
| clear_btn = gr.Button( | |
| "Clear", | |
| size="lg", | |
| elem_classes="secondary-btn", | |
| scale=1, | |
| ) | |
| # Sidebar (Right) | |
| with gr.Column(scale=1): | |
| gr.HTML( | |
| """ | |
| <div class='info-card'> | |
| <div style='display: flex; align-items: center; gap: 12px; margin-bottom: 20px;'> | |
| <span style='font-size: 32px;'>⚡</span> | |
| <h3 style='margin: 0; color: #78350f; font-size: 22px; font-weight: 800;'>How It Works</h3> | |
| </div> | |
| <div class='step-card'> | |
| <div style='display: flex; align-items: center; gap: 12px; margin-bottom: 8px;'> | |
| <div style='min-width: 36px; width: 36px; height: 36px; background: linear-gradient(135deg, #3b82f6, #06b6d4); border-radius: 10px; display: flex; align-items: center; justify-content: center; color: white; font-weight: 800; font-size: 16px;'>1</div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 700; color: #1e293b;'>Detection</h4> | |
| </div> | |
| <p style='margin: 0; font-size: 13px; color: #475569; line-height: 1.6;'>AI identifies gender stereotypes and biased language patterns</p> | |
| </div> | |
| <div class='step-card'> | |
| <div style='display: flex; align-items: center; gap: 12px; margin-bottom: 8px;'> | |
| <div style='min-width: 36px; width: 36px; height: 36px; background: linear-gradient(135deg, #a855f7, #ec4899); border-radius: 10px; display: flex; align-items: center; justify-content: center; color: white; font-weight: 800; font-size: 16px;'>2</div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 700; color: #1e293b;'>Explanation</h4> | |
| </div> | |
| <p style='margin: 0; font-size: 13px; color: #475569; line-height: 1.6;'>SHAP highlights problematic words and phrases</p> | |
| </div> | |
| <div class='step-card'> | |
| <div style='display: flex; align-items: center; gap: 12px; margin-bottom: 8px;'> | |
| <div style='min-width: 36px; width: 36px; height: 36px; background: linear-gradient(135deg, #10b981, #14b8a6); border-radius: 10px; display: flex; align-items: center; justify-content: center; color: white; font-weight: 800; font-size: 16px;'>3</div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 700; color: #1e293b;'>Mitigation</h4> | |
| </div> | |
| <p style='margin: 0; font-size: 13px; color: #475569; line-height: 1.6;'>Generates neutral, inclusive alternatives</p> | |
| </div> | |
| <div style='margin-top: 20px; padding-top: 16px; border-top: 3px solid #fcd34d;'> | |
| <p style='margin: 0; font-size: 13px; font-weight: 700; color: #78350f; display: flex; align-items: center; gap: 8px;'> | |
| <span style='font-size: 18px;'>✨</span> | |
| Powered by DistilBERT + SHAP | |
| </p> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| # Scroll anchor for results | |
| results_anchor = gr.HTML("<div id='results-anchor'></div>", visible=False) | |
| # Results Section | |
| with gr.Column( | |
| visible=False, elem_classes="results-section" | |
| ) as results_section: | |
| gr.HTML( | |
| "<div style='margin: 32px 0 24px 0;'><h2 style='font-size: 32px; font-weight: 900; color: #1e293b; margin: 0;'>📊 Analysis Results</h2></div>" | |
| ) | |
| # Summary and Bias Meter Row | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=1): | |
| bias_meter = gr.Plot(label="", elem_id="bias-meter-plot") | |
| with gr.Column(scale=2): | |
| summary_display = gr.HTML(label="") | |
| # Detailed Analysis Tabs | |
| gr.HTML( | |
| "<div style='margin: 32px 0 16px 0;'><h3 style='font-size: 24px; font-weight: 800; color: #1e293b; margin: 0;'>🔍 Detailed Analysis</h3></div>" | |
| ) | |
| with gr.Tabs() as tabs: | |
| with gr.TabItem("📊 Word Impact", id="word-tab"): | |
| gr.HTML( | |
| """ | |
| <div style='background: linear-gradient(135deg, #f8fafc, #f1f5f9); border-radius: 12px; padding: 16px 20px; margin-bottom: 16px; border-left: 4px solid #3b82f6;'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 24px;'>📊</span> | |
| <div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 800; color: #1e293b;'>Word Impact Analysis</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>SHAP values showing which words contribute most to bias detection</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| shap_chart = gr.Plot(label="", elem_id="shap-chart-plot") | |
| gr.HTML( | |
| "<div style='margin: 24px 0 12px 0;'><h4 style='font-size: 18px; font-weight: 700; color: #334155; margin: 0;'>🎯 Key Biased Terms</h4></div>" | |
| ) | |
| top_words_display = gr.HTML(label="") | |
| with gr.TabItem("📝 Highlighted Text", id="text-tab"): | |
| gr.HTML( | |
| """ | |
| <div style='background: linear-gradient(135deg, #f8fafc, #f1f5f9); border-radius: 12px; padding: 16px 20px; margin-bottom: 16px; border-left: 4px solid #f59e0b;'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 24px;'>📝</span> | |
| <div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 800; color: #1e293b;'>Original Text with Bias Indicators</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>Words are highlighted based on their contribution to bias</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| highlighted_text = gr.HTML(label="") | |
| gr.HTML( | |
| """ | |
| <div style='margin-top: 24px; padding: 20px; background: linear-gradient(135deg, #eff6ff, #dbeafe); border-radius: 16px; border-left: 5px solid #3b82f6;'> | |
| <strong style='color: #1e40af; font-size: 15px; display: block; margin-bottom: 12px;'>Legend:</strong> | |
| <div style='display: flex; flex-wrap: wrap; gap: 12px;'> | |
| <span class='legend-item' style='background: #dc2626; color: white;'>High Impact</span> | |
| <span class='legend-item' style='background: #f59e0b; color: white;'>Medium Impact</span> | |
| <span class='legend-item' style='background: #fbbf24; color: white;'>Low Impact</span> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.TabItem("🔄 Neutral Alternatives", id="alternatives-tab"): | |
| gr.HTML( | |
| """ | |
| <div style='background: linear-gradient(135deg, #f8fafc, #f1f5f9); border-radius: 12px; padding: 16px 20px; margin-bottom: 16px; border-left: 4px solid #10b981;'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 24px;'>🔄</span> | |
| <div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 800; color: #1e293b;'>Generated Suggestions</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>Bias-free alternatives that maintain your core message</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| counterfactuals_display = gr.HTML(label="") | |
| gr.HTML( | |
| """ | |
| <div style='margin-top: 24px; padding: 24px; background: linear-gradient(135deg, #e0f2fe, #bae6fd); border-radius: 20px; border-left: 5px solid #0284c7;'> | |
| <strong style='color: #075985; font-size: 16px; display: flex; align-items: center; gap: 8px; margin-bottom: 8px;'> | |
| <span style='font-size: 24px;'>💡</span> | |
| Pro Tip | |
| </strong> | |
| <p style='margin: 0; color: #0c4a6e; font-size: 14px; line-height: 1.7;'> | |
| These alternatives maintain your core message while removing gendered stereotypes. | |
| Use them to create more inclusive career recommendations. | |
| </p> | |
| </div> | |
| """ | |
| ) | |
| with gr.TabItem( | |
| "📚 Batch Analysis", | |
| id="batch-tab", | |
| elem_classes="batch-analysis-card", | |
| ): | |
| gr.HTML( | |
| """ | |
| <div class='batch-header'> | |
| <div style='text-align: center;'> | |
| <div style='display: inline-flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #7c3aed, #a855f7); width: 64px; height: 64px; border-radius: 20px; margin-bottom: 16px; box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);'> | |
| <span style='font-size: 36px;'>📦</span> | |
| </div> | |
| <h3 style='font-size: 28px; font-weight: 900; color: #1e293b; margin: 0 0 8px 0;'>Batch Processing</h3> | |
| <p style='margin: 0; font-size: 15px; color: #64748b; font-weight: 500;'>Process multiple texts simultaneously and compare group statistics</p> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # Input Section | |
| gr.HTML( | |
| """ | |
| <div class='batch-section-header'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 24px;'>📝</span> | |
| <div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 800; color: #1e293b;'>Text Input</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>Enter or paste your texts below (one per line)</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| batch_textarea = gr.Textbox( | |
| label="", | |
| lines=8, | |
| placeholder="Paste your texts here, one per line...\n\nExample:\nWomen are naturally better at nursing.\nMen excel in technical fields.\nThe female assistant was emotional.", | |
| elem_classes="batch-input", | |
| ) | |
| gr.HTML( | |
| """ | |
| <div style='margin: 24px 0; padding: 16px; background: #f8fafc; border-radius: 12px; text-align: center; border: 2px dashed #94a3b8;'> | |
| <div style='display: inline-flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 20px;'>📁</span> | |
| <span style='font-weight: 700; color: #64748b; font-size: 14px;'>OR UPLOAD A FILE</span> | |
| <span style='font-size: 20px;'>↓</span> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| file_upload = gr.File( | |
| label="", | |
| file_types=[".txt", ".csv", ".json"], | |
| elem_classes="upload-zone", | |
| ) | |
| # Group Comparison Section | |
| gr.HTML( | |
| """ | |
| <div class='batch-section-header' style='border-left-color: #f59e0b;'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 24px;'>🎯</span> | |
| <div> | |
| <h4 style='margin: 0; font-size: 16px; font-weight: 800; color: #1e293b;'>Group Comparison</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>Optional: Filter texts into groups for comparative analysis</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(): | |
| group_a_select = gr.Textbox( | |
| label="🔵 Group A Filter", | |
| placeholder="e.g., 'female' or 'women'", | |
| scale=1, | |
| ) | |
| group_b_select = gr.Textbox( | |
| label="🔴 Group B Filter", | |
| placeholder="e.g., 'male' or 'men'", | |
| scale=1, | |
| ) | |
| # Action Buttons | |
| gr.HTML("<div style='height: 20px;'></div>") | |
| with gr.Row(): | |
| run_batch_btn = gr.Button( | |
| "🚀 Start Batch Processing", | |
| variant="primary", | |
| size="lg", | |
| elem_classes="primary-btn", | |
| scale=2, | |
| ) | |
| refresh_status_btn = gr.Button( | |
| "🔄 Refresh Status", | |
| size="lg", | |
| elem_classes="secondary-btn", | |
| scale=1, | |
| ) | |
| with gr.Column(scale=1): | |
| # Progress Monitor | |
| gr.HTML( | |
| """ | |
| <div class='batch-section-header' style='border-left-color: #3b82f6; margin-bottom: 16px;'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 24px;'>⏱️</span> | |
| <div> | |
| <h4 style='font-size: 16px; font-weight: 800; color: #1e293b; margin: 0;'>Progress Monitor</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>Track your batch processing status in real-time</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Group(elem_classes="progress-inputs"): | |
| progress_bar = gr.Number( | |
| value=0, | |
| label="📊 Completion Percentage", | |
| precision=1, | |
| interactive=False, | |
| ) | |
| progress_text = gr.Textbox( | |
| interactive=False, | |
| label="📡 Current Status", | |
| value="Ready to process", | |
| ) | |
| job_id_text = gr.Textbox( | |
| interactive=False, | |
| label="🆔 Job ID", | |
| placeholder="Will appear after starting", | |
| ) | |
| # Export Section | |
| gr.HTML( | |
| """ | |
| <div class='batch-section-header' style='border-left-color: #10b981; margin-top: 24px; margin-bottom: 16px;'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 24px;'>💾</span> | |
| <div> | |
| <h4 style='font-size: 16px; font-weight: 800; color: #1e293b; margin: 0;'>Export Results</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>Save your analysis results</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Group(elem_classes="export-inputs"): | |
| save_path = gr.Textbox( | |
| label="📂 Save Path (optional)", | |
| placeholder="./exports/results.json", | |
| ) | |
| with gr.Row(): | |
| export_json_btn = gr.Button( | |
| "📄 Export JSON", | |
| size="sm", | |
| elem_classes="secondary-btn", | |
| ) | |
| export_csv_btn = gr.Button( | |
| "📊 Export CSV", | |
| size="sm", | |
| elem_classes="secondary-btn", | |
| ) | |
| export_download = gr.File( | |
| label="⬇️ Download File", interactive=False | |
| ) | |
| # Results Section | |
| gr.HTML( | |
| """ | |
| <div class='batch-section-header' style='border-left-color: #10b981; margin: 48px 0 20px 0;'> | |
| <div style='display: flex; align-items: center; gap: 10px;'> | |
| <span style='font-size: 28px;'>📊</span> | |
| <div> | |
| <h4 style='font-size: 20px; font-weight: 900; color: #1e293b; margin: 0;'>Batch Summary & Results</h4> | |
| <p style='margin: 4px 0 0 0; font-size: 13px; color: #64748b;'>Comprehensive analysis of all processed texts</p> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| batch_summary_html = gr.HTML() | |
| comparison_html = gr.HTML() | |
| # Loading and Error Display | |
| loading_display = gr.HTML(visible=False) | |
| error_display = gr.HTML(visible=False) | |
| # Footer | |
| gr.HTML( | |
| """ | |
| <div class='footer'> | |
| <h4 style='margin: 0 0 12px 0; color: #1e293b; font-size: 20px; font-weight: 800;'>BiasGuard Pro v1.0</h4> | |
| <p style='margin: 0 0 20px 0; color: #64748b; font-size: 15px;'> | |
| Building fairer AI systems through transparent bias detection and mitigation | |
| </p> | |
| <div style='display: flex; justify-content: center; gap: 32px; flex-wrap: wrap;'> | |
| <a href='#'>📄 Research Paper</a> | |
| <a href='#'>💻 GitHub</a> | |
| <a href='#'>📚 Documentation</a> | |
| <a href='#'>🤝 Contribute</a> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| # Event Handlers | |
| def update_display(text): | |
| """UI event handler: analyze input and update dashboard components. | |
| This inner function is bound to the analyze button and returns a | |
| mapping of outputs expected by Gradio. It performs input checks | |
| and delegates to `analyze_text_for_dashboard` for the heavy work. | |
| """ | |
| if not text.strip(): | |
| return { | |
| results_section: gr.update(visible=False), | |
| loading_display: gr.update(visible=False), | |
| error_display: gr.update( | |
| value="<div style='padding: 24px; background: linear-gradient(135deg, #fef3c7, #fde68a); border-left: 5px solid #f59e0b; border-radius: 16px; margin: 20px 0;'><strong style='color: #92400e; font-size: 16px;'>⚠️ No Input:</strong> <span style='color: #78350f; font-size: 15px; margin-left: 8px;'>Please enter text to analyze.</span></div>", | |
| visible=True, | |
| ), | |
| analyze_btn: gr.update( | |
| value="🔍 Analyze for Bias", interactive=True | |
| ), | |
| } | |
| result = self.analyze_text_for_dashboard(text) | |
| if "error" in result: | |
| return { | |
| results_section: gr.update(visible=False), | |
| loading_display: gr.update(visible=False), | |
| error_display: gr.update( | |
| value=f"<div style='padding: 24px; background: linear-gradient(135deg, #fee2e2, #fecaca); border-left: 5px solid #dc2626; border-radius: 16px; margin: 20px 0;'><strong style='color: #991b1b; font-size: 16px;'>❌ Error:</strong> <span style='color: #7f1d1d; font-size: 15px; margin-left: 8px;'>{result['error']}</span></div>", | |
| visible=True, | |
| ), | |
| analyze_btn: gr.update( | |
| value="🔍 Analyze for Bias", interactive=True | |
| ), | |
| } | |
| # Auto-scroll to results with improved targeting | |
| scroll_js = """ | |
| <script> | |
| setTimeout(function() { | |
| // Try multiple selectors to find the results section | |
| const resultsSection = document.querySelector('.results-section') || | |
| document.querySelector('[class*="results-section"]') || | |
| document.getElementById('results-anchor'); | |
| if (resultsSection) { | |
| resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| window.scrollBy(0, -20); // Add slight offset for better visibility | |
| } | |
| }, 600); | |
| </script> | |
| """ | |
| return { | |
| results_section: gr.update(visible=True), | |
| loading_display: gr.update(visible=False), | |
| error_display: gr.update(visible=False), | |
| bias_meter: result["bias_meter"], | |
| shap_chart: result["shap_chart"], | |
| highlighted_text: result["highlighted_text"], | |
| summary_display: result["summary_html"] + scroll_js, | |
| counterfactuals_display: result["counterfactuals_html"], | |
| top_words_display: result["top_words_html"], | |
| analyze_btn: gr.update( | |
| value="🔍 Analyze for Bias", interactive=True | |
| ), | |
| } | |
| def clear_inputs(): | |
| """Simple helper to clear the main text input (connected to Clear button).""" | |
| return "" | |
| def scroll_to_results(): | |
| """Trigger scroll after results are displayed. | |
| This placeholder exists so Gradio `.then()` can call a final | |
| function that performs client-side scrolling. Returns None to | |
| indicate no payload is required. | |
| """ | |
| return None | |
| # Connect analyze button with loading state | |
| analyze_btn.click( | |
| lambda: gr.update(value="⏳ Analyzing...", interactive=False), | |
| inputs=None, | |
| outputs=[analyze_btn], | |
| ).then( | |
| update_display, | |
| inputs=[text_input], | |
| outputs=[ | |
| results_section, | |
| loading_display, | |
| error_display, | |
| bias_meter, | |
| shap_chart, | |
| highlighted_text, | |
| summary_display, | |
| counterfactuals_display, | |
| top_words_display, | |
| analyze_btn, | |
| ], | |
| scroll_to_output=True, | |
| ) | |
| # Connect text input submit with loading state | |
| text_input.submit( | |
| lambda: gr.update(value="⏳ Analyzing...", interactive=False), | |
| inputs=None, | |
| outputs=[analyze_btn], | |
| ).then( | |
| update_display, | |
| inputs=[text_input], | |
| outputs=[ | |
| results_section, | |
| loading_display, | |
| error_display, | |
| bias_meter, | |
| shap_chart, | |
| highlighted_text, | |
| summary_display, | |
| counterfactuals_display, | |
| top_words_display, | |
| analyze_btn, | |
| ], | |
| scroll_to_output=True, | |
| ) | |
| clear_btn.click(clear_inputs, outputs=[text_input]) | |
| # Connect sample buttons | |
| sample_btn_1.click(lambda: self.sample_texts[0], outputs=text_input) | |
| sample_btn_2.click(lambda: self.sample_texts[1], outputs=text_input) | |
| sample_btn_3.click(lambda: self.sample_texts[2], outputs=text_input) | |
| # Batch processing handlers | |
| def parse_uploaded_file(file_obj): | |
| """Attempt to parse a user-uploaded file into a list of texts. | |
| Supports plain text (.txt), CSV with a 'text' column and simple | |
| JSON lists or lists of objects with a 'text' field. Returns an | |
| empty list on failure. | |
| """ | |
| if not file_obj: | |
| return [] | |
| try: | |
| import csv | |
| import io | |
| import json | |
| fname = file_obj.name if hasattr(file_obj, "name") else "" | |
| file_bytes = file_obj.read() | |
| text = ( | |
| file_bytes.decode("utf-8") | |
| if isinstance(file_bytes, (bytes, bytearray)) | |
| else str(file_bytes) | |
| ) | |
| if fname.endswith(".json"): | |
| data = json.loads(text) | |
| if isinstance(data, list): | |
| if all(isinstance(x, str) for x in data): | |
| return data | |
| else: | |
| return [ | |
| str(x.get("text", "")) | |
| for x in data | |
| if isinstance(x, dict) and "text" in x | |
| ] | |
| elif fname.endswith(".csv"): | |
| rows = [] | |
| reader = csv.DictReader(io.StringIO(text)) | |
| for r in reader: | |
| if "text" in r: | |
| rows.append(r["text"]) | |
| else: | |
| rows.append(",".join(r.values())) | |
| return rows | |
| else: | |
| return [l for l in text.splitlines() if l.strip()] | |
| except Exception: | |
| return [] | |
| def run_batch_background( | |
| textarea_value, file_obj, group_a_filter, group_b_filter, save_path_val | |
| ): | |
| """Kick off a background thread to analyze a batch of texts. | |
| This function prepares the inputs, registers a job id and starts a | |
| daemon thread that runs `analyze_batch`. It returns immediate UI | |
| state (job id and initial messages) while processing continues | |
| in the background. | |
| """ | |
| texts = [ | |
| l.strip() for l in (textarea_value or "").splitlines() if l.strip() | |
| ] | |
| uploaded = parse_uploaded_file(file_obj) | |
| if uploaded: | |
| texts.extend(uploaded) | |
| total = len(texts) | |
| if total == 0: | |
| return { | |
| progress_text: "⚠️ No texts provided", | |
| progress_bar: 0, | |
| job_id_text: "", | |
| batch_summary_html: "<div style='padding: 24px; background: linear-gradient(135deg, #fef3c7, #fde68a); border-left: 5px solid #f59e0b; border-radius: 16px;'><strong style='font-size: 16px; color: #92400e;'>⚠️ Warning:</strong> <span style='color: #78350f; margin-left: 8px;'>No texts provided for batch processing.</span></div>", | |
| comparison_html: "", | |
| } | |
| job_id = f"job_{int(time.time()*1000)}" | |
| self._jobs[job_id] = { | |
| "status": "queued", | |
| "total": total, | |
| "processed": 0, | |
| "results": [], | |
| "summary": None, | |
| "created_at": time.time(), | |
| } | |
| import threading | |
| def _worker(jid, texts_local, ga_filter, gb_filter, save_path_local): | |
| def progress_cb(processed, total_count): | |
| self._jobs[jid]["processed"] = processed | |
| self._jobs[jid]["status"] = ( | |
| "running" if processed < total_count else "finalizing" | |
| ) | |
| try: | |
| results = self.analyze_batch( | |
| texts_local, progress_callback=progress_cb | |
| ) | |
| self._jobs[jid]["results"] = results | |
| self.last_batch_results = results | |
| self._jobs[jid]["summary"] = self.summarize_batch(results) | |
| group_a = [] | |
| group_b = [] | |
| if ga_filter: | |
| group_a = [ | |
| r | |
| for r in results | |
| if ga_filter.lower() in r.get("text", "").lower() | |
| ] | |
| if gb_filter: | |
| group_b = [ | |
| r | |
| for r in results | |
| if gb_filter.lower() in r.get("text", "").lower() | |
| ] | |
| if group_a and group_b: | |
| self._jobs[jid]["comparison"] = self.compare_groups( | |
| group_a, group_b | |
| ) | |
| else: | |
| self._jobs[jid]["comparison"] = None | |
| if save_path_local: | |
| try: | |
| if save_path_local.endswith(".json"): | |
| save_results_json(save_path_local, results) | |
| elif save_path_local.endswith(".csv"): | |
| save_results_csv(save_path_local, results) | |
| self._jobs[jid]["export_path"] = save_path_local | |
| except Exception as e: | |
| self._jobs[jid]["export_error"] = str(e) | |
| self._jobs[jid]["status"] = "completed" | |
| except Exception as e: | |
| self._jobs[jid]["status"] = "failed" | |
| self._jobs[jid]["error"] = str(e) | |
| t = threading.Thread( | |
| target=_worker, | |
| args=(job_id, texts, group_a_filter, group_b_filter, save_path_val), | |
| daemon=True, | |
| ) | |
| t.start() | |
| return { | |
| progress_text: "🚀 Job queued successfully", | |
| progress_bar: 0, | |
| job_id_text: job_id, | |
| batch_summary_html: "<div style='padding: 24px; background: linear-gradient(135deg, #d1fae5, #a7f3d0); border-left: 5px solid #10b981; border-radius: 16px;'><strong style='font-size: 16px; color: #065f46;'>✅ Success:</strong> <span style='color: #047857; margin-left: 8px;'>Batch job started. Use 'Refresh Status' to check progress.</span></div>", | |
| comparison_html: "", | |
| } | |
| run_batch_btn.click( | |
| run_batch_background, | |
| inputs=[ | |
| batch_textarea, | |
| file_upload, | |
| group_a_select, | |
| group_b_select, | |
| save_path, | |
| ], | |
| outputs=[ | |
| progress_text, | |
| progress_bar, | |
| job_id_text, | |
| batch_summary_html, | |
| comparison_html, | |
| ], | |
| ) | |
| def poll_job_status(job_id): | |
| """Return current status and summary HTML for a background job. | |
| The function reads the job registry and formats an HTML summary | |
| and comparison block (if available) for display in the UI. | |
| """ | |
| if not job_id: | |
| return { | |
| progress_text: "⚠️ No job ID", | |
| progress_bar: 0, | |
| batch_summary_html: "", | |
| comparison_html: "", | |
| } | |
| job = self._jobs.get(job_id) | |
| if not job: | |
| return { | |
| progress_text: "❌ Job not found", | |
| progress_bar: 0, | |
| batch_summary_html: "", | |
| comparison_html: "", | |
| } | |
| pct = ( | |
| round((job.get("processed", 0) / job.get("total", 1)) * 100, 1) | |
| if job.get("total") | |
| else 0 | |
| ) | |
| summary_html = "" | |
| if job.get("summary"): | |
| s = job["summary"] | |
| summary_html = f""" | |
| <div style='padding: 28px; background: white; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border: 2px solid #e2e8f0; margin-top: 20px;'> | |
| <h4 style='margin: 0 0 20px 0; font-size: 20px; font-weight: 800; color: #1e293b;'>📊 Summary Statistics</h4> | |
| <div style='display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;'> | |
| <div style='padding: 20px; background: linear-gradient(135deg, #eff6ff, #dbeafe); border-radius: 16px; border: 2px solid #93c5fd;'> | |
| <div style='font-size: 13px; color: #1e40af; font-weight: 700; margin-bottom: 8px;'>Total Analyzed</div> | |
| <div style='font-size: 36px; color: #1e3a8a; font-weight: 900;'>{s.get('total', 0)}</div> | |
| </div> | |
| <div style='padding: 20px; background: linear-gradient(135deg, #fef3c7, #fde68a); border-radius: 16px; border: 2px solid #fcd34d;'> | |
| <div style='font-size: 13px; color: #92400e; font-weight: 700; margin-bottom: 8px;'>Avg Bias Score</div> | |
| <div style='font-size: 36px; color: #78350f; font-weight: 900;'>{s.get('avg_bias_probability', 0.0):.3f}</div> | |
| </div> | |
| </div> | |
| <div style='margin-top: 20px;'> | |
| <strong style='color: #334155; font-size: 15px;'>Class Distribution:</strong> | |
| <pre style='background: #f8fafc; padding: 16px; border-radius: 12px; margin-top: 12px; overflow-x: auto; border: 2px solid #e2e8f0; color: #1e293b;'>{json.dumps(s.get('class_counts', {}), indent=2)}</pre> | |
| </div> | |
| </div> | |
| """ | |
| compare_html = "" | |
| if job.get("comparison"): | |
| c = job["comparison"] | |
| compare_html = f""" | |
| <div style='padding: 28px; background: white; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border: 2px solid #e2e8f0; margin-top: 20px;'> | |
| <h4 style='margin: 0 0 20px 0; font-size: 20px; font-weight: 800; color: #1e293b;'>🔄 Group Comparison</h4> | |
| <pre style='background: #f8fafc; padding: 20px; border-radius: 12px; overflow-x: auto; border: 2px solid #e2e8f0; color: #1e293b;'>{json.dumps(c, indent=2)}</pre> | |
| </div> | |
| """ | |
| status_text = job.get("status", "unknown") | |
| status_emoji = ( | |
| "⏳" | |
| if status_text == "running" | |
| else ( | |
| "✅" | |
| if status_text == "completed" | |
| else "❌" if status_text == "failed" else "🔵" | |
| ) | |
| ) | |
| return { | |
| progress_text: f"{status_emoji} Status: {status_text}", | |
| progress_bar: pct, | |
| batch_summary_html: summary_html, | |
| comparison_html: compare_html, | |
| } | |
| refresh_status_btn.click( | |
| poll_job_status, | |
| inputs=[job_id_text], | |
| outputs=[ | |
| progress_text, | |
| progress_bar, | |
| batch_summary_html, | |
| comparison_html, | |
| ], | |
| ) | |
| def export_last_json(): | |
| """Export the most recent batch results to a JSON file and return | |
| the filename and a status message. | |
| """ | |
| try: | |
| if not self.last_batch_results: | |
| return None, "⚠️ No batch results to export" | |
| os.makedirs("./export", exist_ok=True) | |
| fname = f"./export/batch_results_{int(time.time())}.json" | |
| save_results_json(fname, self.last_batch_results) | |
| return fname, f"✅ Saved to {fname}" | |
| except Exception as e: | |
| return None, f"❌ Export failed: {e}" | |
| def export_last_csv(): | |
| """Export the most recent batch results to CSV and return status.""" | |
| try: | |
| if not self.last_batch_results: | |
| return None, "⚠️ No batch results to export" | |
| os.makedirs("./export", exist_ok=True) | |
| fname = f"./export/batch_results_{int(time.time())}.csv" | |
| save_results_csv(fname, self.last_batch_results) | |
| return fname, f"✅ Saved to {fname}" | |
| except Exception as e: | |
| return None, f"❌ Export failed: {e}" | |
| export_json_btn.click( | |
| export_last_json, outputs=[export_download, progress_text] | |
| ) | |
| export_csv_btn.click( | |
| export_last_csv, outputs=[export_download, progress_text] | |
| ) | |
| return demo | |
| def analyze_batch(self, texts: List[str], progress_callback=None) -> List[Dict]: | |
| """Analyze a list of texts and return per-text analysis results. | |
| This method attempts to use a batched prediction for speed and falls | |
| back to per-text analysis if an error occurs. An optional | |
| `progress_callback(processed, total)` may be provided to receive | |
| progress updates. | |
| Args: | |
| texts: List of input strings to analyze. | |
| progress_callback: Optional callable receiving (processed, total). | |
| Returns: | |
| List of result dicts in the same format as `analyze_text`. | |
| """ | |
| results = [] | |
| total = len(texts) | |
| self._ensure_analyzer() | |
| try: | |
| batch_preds = self.analyzer.detector.predict_batch_batched( | |
| [t for t in texts] | |
| ) | |
| for i, (t, pred) in enumerate(zip(texts, batch_preds), 1): | |
| try: | |
| res = self.analyzer.analyze_text(t) | |
| res["bias_probability"] = pred.get( | |
| "bias_probability", res.get("bias_probability") | |
| ) | |
| res["confidence"] = pred.get("confidence", res.get("confidence")) | |
| except Exception: | |
| res = { | |
| "text": t, | |
| "bias_probability": pred.get("bias_probability", 0.0), | |
| "bias_class": pred.get("classification", "UNKNOWN"), | |
| "confidence": pred.get("confidence", 0.0), | |
| "top_biased_words": [], | |
| "shap_scores": [], | |
| "counterfactuals": [], | |
| "timestamp": time.time(), | |
| } | |
| results.append(res) | |
| if progress_callback: | |
| try: | |
| progress_callback(i, total) | |
| except Exception: | |
| pass | |
| except Exception: | |
| for i, t in enumerate(texts, 1): | |
| try: | |
| res = self.analyzer.analyze_text(t) | |
| results.append(res) | |
| except Exception as e: | |
| results.append( | |
| {"text": t, "error": str(e), "timestamp": time.time()} | |
| ) | |
| if progress_callback: | |
| try: | |
| progress_callback(i, total) | |
| except Exception: | |
| pass | |
| return results | |
| def summarize_batch(self, results: List[Dict]) -> Dict[str, Any]: | |
| """Summarize batch results with aggregate statistics. | |
| Produces total count, average bias probability, class counts and top | |
| biased words frequency for quick reporting in the UI. | |
| """ | |
| total = len(results) | |
| if total == 0: | |
| return {} | |
| bias_probs = [ | |
| r.get("bias_probability", 0.0) for r in results if "bias_probability" in r | |
| ] | |
| classes = {} | |
| top_words = {} | |
| for r in results: | |
| cls = r.get("bias_class", "UNKNOWN") | |
| classes[cls] = classes.get(cls, 0) + 1 | |
| for w in r.get("top_biased_words", []): | |
| top_words[w] = top_words.get(w, 0) + 1 | |
| avg_bias = sum(bias_probs) / len(bias_probs) if bias_probs else 0.0 | |
| return { | |
| "total": total, | |
| "avg_bias_probability": avg_bias, | |
| "class_counts": classes, | |
| "top_words_frequency": sorted( | |
| top_words.items(), key=lambda x: x[1], reverse=True | |
| )[:20], | |
| } | |
| def compare_groups( | |
| self, group_a: List[Dict], group_b: List[Dict] | |
| ) -> Dict[str, Any]: | |
| """Compute simple group-level statistics and difference in bias. | |
| Returns a dict containing per-group counts and average bias score as | |
| well as the delta (group_a_avg - group_b_avg). | |
| """ | |
| def stats(group): | |
| probs = [ | |
| r.get("bias_probability", 0.0) for r in group if "bias_probability" in r | |
| ] | |
| avg = sum(probs) / len(probs) if probs else 0.0 | |
| return {"count": len(group), "avg_bias": avg} | |
| a_stats = stats(group_a) | |
| b_stats = stats(group_b) | |
| delta = a_stats["avg_bias"] - b_stats["avg_bias"] | |
| return {"group_a": a_stats, "group_b": b_stats, "avg_bias_delta": delta} | |
| def main(): | |
| """Entry point to launch the BiasGuard Pro dashboard. | |
| Constructs the dashboard and launches the Gradio server. This function | |
| is executed when running the module as a script. | |
| """ | |
| print("🚀 Launching BiasGuard Pro Dashboard (Enhanced UI)...") | |
| dashboard = BiasGuardDashboard() | |
| demo = dashboard.create_dashboard() | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| share=True, | |
| debug=True, | |
| show_error=True, | |
| ssr_mode=False, | |
| theme=getattr(dashboard, "_launch_theme", None), | |
| css=getattr(dashboard, "_launch_css", None), | |
| js=getattr(dashboard, "_launch_js", None), | |
| ) | |
| if __name__ == "__main__": | |
| main() | |