{total_cases}
' f'Total Cases in View
{num_jurisdictions}
' f'Jurisdictions in View
{recent_value}
' f'{recent_label}
""" Streamlit front-end for the AI Litigation Tracker. This app: - Loads case-level summaries (and metadata) from data/summaries.csv - Lets users filter and explore AI-related litigation - Integrates optional RAG backends for case-specific and global Q&A """ import os from typing import Optional import calendar import pandas as pd import streamlit as st # ============================================================ # Config # ============================================================ APP_TITLE = "AI Litigation Tracker" CSV_PATH = os.path.join(os.path.dirname(__file__), "data", "summaries.csv") LOGO_DIR = os.path.join(os.path.dirname(__file__), "logos") LAWFARE_LOGO_PATH = os.path.join(LOGO_DIR, "lawfare_logo.png") VAILL_LOGO_PATH = os.path.join(LOGO_DIR, "vaill_logo.png") COURTLISTENER_LOGO_PATH = os.path.join(LOGO_DIR, "court_listener_logo.png") # Try to load RAG chains (optional backends) try: from rag.chains import case_specific_qa, global_qa, ping_backends chains_ok = True except Exception as e: # pragma: no cover - only hit when backends misconfigured chains_ok = False chains_import_error = e st.set_page_config( page_title=APP_TITLE, layout="wide", page_icon="⚖️", ) # ============================================================ # Data # ============================================================ @st.cache_data(show_spinner=False) def load_summaries() -> pd.DataFrame: """ Load the case summaries CSV and normalize columns/types. Expected base columns: - case_name - filing_date - docket_number - summary Optional metadata columns (if present) are parsed and kept for UI: - last_updated (legacy) - latest_update (canonical last activity date; YYYY-MM-DD recommended) - jurisdiction - court_id - courtlistener_url """ if not os.path.exists(CSV_PATH): raise FileNotFoundError(f"Missing summaries CSV at {CSV_PATH}") df = pd.read_csv(CSV_PATH) expected_cols = ["case_name", "filing_date", "docket_number", "summary"] missing = [c for c in expected_cols if c not in df.columns] if missing: raise ValueError(f"summaries.csv missing columns: {missing}") # Normalize core text columns to strings for c in expected_cols: df[c] = df[c].fillna("").astype(str) optional_cols = [ "last_updated", "latest_update", "jurisdiction", "court_id", "courtlistener_url", ] for c in optional_cols: if c in df.columns: df[c] = df[c].fillna("").astype(str) # Parse filing date for correct sorting/filtering df["filing_date_dt"] = pd.to_datetime(df["filing_date"], errors="coerce") # Parse latest_update into a datetime column if present # (this represents the best-guess "last activity" date for the case) if "latest_update" in df.columns: df["latest_update_dt"] = pd.to_datetime(df["latest_update"], errors="coerce") elif "last_updated" in df.columns: # Fallback for legacy naming: treat last_updated as latest_update df["latest_update_dt"] = pd.to_datetime(df["last_updated"], errors="coerce") return df def refresh_data() -> None: """Clear the cached summaries so the next load() call re-reads from disk.""" load_summaries.clear() def pretty_date(dt: pd.Timestamp) -> str: """ Format a Timestamp as 'Month D, YYYY' (e.g. 'August 8, 2025'). Returns 'N/A' for NaT values. """ if pd.isna(dt): return "N/A" # Month name + non-padded day, e.g. "August 8, 2025" return f"{dt.strftime('%B')} {dt.day}, {dt.year}" # ============================================================ # Chat helpers # ============================================================ def ensure_chat_state(key: str) -> None: """Initialize a session_state list for a given chat key if missing.""" if key not in st.session_state: st.session_state[key] = [] # list of {role, content} def replay_chat(key: str) -> None: """Replay all messages for a given chat key into the Streamlit chat UI.""" for msg in st.session_state.get(key, []): with st.chat_message(msg["role"]): st.write(msg["content"]) def add_message(key: str, role: str, content: str) -> None: """Append a new message to the stored chat transcript.""" st.session_state[key].append({"role": role, "content": content}) # ============================================================ # CSS (Lawfare-centered color scheme) # ============================================================ GLOBAL_CSS = """ """ st.markdown(GLOBAL_CSS, unsafe_allow_html=True) # ============================================================ # Table renderer (full summary, scrollable) # ============================================================ def render_cases_table(df: pd.DataFrame) -> None: """ Render an interactive Streamlit dataframe for the filtered cases. Includes: - Lawsuit name - Jurisdiction and court (if available) - Docket number - Filing date - Latest activity date (if available) - Short summary - CourtListener URL (if available) """ if df.empty: st.warning("No cases match the selected filters.") return display_df = df.copy() # Drop raw string date fields in favor of datetime columns used for display. for col in ["filing_date", "latest_update", "last_updated"]: if col in display_df.columns: display_df = display_df.drop(columns=[col]) # ---- Column mapping / ordering ---- column_mapping = { "case_name": "Lawsuit", "jurisdiction": "Jurisdiction", "court_id": "Court", "summary": "Summary", "docket_number": "Docket Number", "filing_date_dt": "Date Filed", "latest_update_dt": "Most Recent Activity", "courtlistener_url": "CourtListener URL", } # Only keep columns that actually exist in the dataframe base_cols = [ "case_name", "jurisdiction", "court_id", "docket_number", "filing_date_dt", "latest_update_dt", "summary", "courtlistener_url", ] existing_base_cols = [c for c in base_cols if c in display_df.columns] # Anything else in the df gets appended at the end extra_cols = [c for c in display_df.columns if c not in existing_base_cols] display_columns = existing_base_cols + extra_cols display_df = display_df[display_columns].copy() display_df = display_df.rename(columns={k: v for k, v in column_mapping.items() if k in display_df.columns}) # Streamlit's dataframe will turn HTTP URLs into clickable links automatically. st.dataframe( display_df, hide_index=True, width="stretch", column_config={ "Date Filed": st.column_config.DateColumn( "Date Filed", format="MMMM D, YYYY", ), "Most Recent Activity": st.column_config.DateColumn( "Most Recent Activity", format="MMMM D, YYYY", ), "Summary": st.column_config.TextColumn( "Summary", width="large", ), # Optional: make the link column a bit wider "CourtListener URL": st.column_config.TextColumn( "CourtListener URL", width="medium", ), }, ) # ---- CSV download (export the exact view the user sees) ---- csv = display_df.to_csv(index=False) st.download_button( label="Download Table as CSV", data=csv, file_name="ai_litigation_cases.csv", mime="text/csv", ) # ============================================================ # Load data # ============================================================ df: Optional[pd.DataFrame] = None try: df = load_summaries() except Exception as e: st.error(f"Unable to load summaries: {e}") # ============================================================ # Sidebar (dynamic filters like tracker) # ============================================================ with st.sidebar: st.markdown('
', unsafe_allow_html=True) st.markdown("### Filter Controls") # Defaults in case df is empty sidebar_q = "" date_mask = None if df is not None and not df.empty: # Basic full-text search over case_name and docket_number sidebar_q = st.text_input( "Search", placeholder="case name or docket…", ) # ---------------- Date Range ---------------- st.markdown("#### Date Range") min_dt = df["filing_date_dt"].min() max_dt = df["filing_date_dt"].max() if pd.isna(min_dt) or pd.isna(max_dt): st.caption("No valid filing dates available for filtering.") else: years = sorted( df["filing_date_dt"].dropna().dt.year.unique().tolist() ) month_names = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ] date_filter_mode = st.radio( "Filter by:", options=["No Date Filter", "Year Only", "Year & Month"], index=0, ) date_filter_summary = "" date_filter_count = None if date_filter_mode == "Year Only": selected_years = st.multiselect( "Select Years:", options=years, default=years, ) if selected_years: year_series = df["filing_date_dt"].dt.year date_mask = year_series.isin(selected_years) date_filter_summary = ( "Filtering: " + ", ".join(str(y) for y in selected_years) ) date_filter_count = int(date_mask.sum()) else: # no years picked → no rows date_mask = df["filing_date_dt"].notna() & False date_filter_summary = "No years selected." date_filter_count = 0 elif date_filter_mode == "Year & Month": # ---- Row 1: From / To year ---- col_y1, col_y2 = st.columns(2) with col_y1: from_year = st.selectbox( "From Year:", options=years, index=0, ) with col_y2: to_year = st.selectbox( "To Year:", options=years, index=len(years) - 1, ) # ---- Row 2: From / To month ---- col_m1, col_m2 = st.columns(2) with col_m1: from_month_name = st.selectbox( "From Month:", options=month_names, index=0, ) with col_m2: to_month_name = st.selectbox( "To Month:", options=month_names, index=11, ) from_month = month_names.index(from_month_name) + 1 to_month = month_names.index(to_month_name) + 1 # Ensure end is not before start if (to_year, to_month) < (from_year, from_month): from_year, to_year = to_year, from_year from_month, to_month = to_month, from_month from_month_name, to_month_name = to_month_name, from_month_name # Build mask: year*100 + month allows a clean between() filter date_vals = ( df["filing_date_dt"].dt.year * 100 + df["filing_date_dt"].dt.month ) start_val = from_year * 100 + from_month end_val = to_year * 100 + to_month date_mask = date_vals.between(start_val, end_val) # Use abbreviated month names in the summary, like "Jan 2024 - Nov 2025" from_abbr = calendar.month_abbr[from_month] to_abbr = calendar.month_abbr[to_month] date_filter_summary = ( f"Filtering: {from_abbr} {from_year} - {to_abbr} {to_year}" ) date_filter_count = int(date_mask.sum()) if date_filter_mode != "No Date Filter": if date_filter_summary: st.success(date_filter_summary) if date_filter_count is not None: st.info(f"{date_filter_count} cases with dates in range") else: sidebar_q = "" date_mask = None # ---- Developer tools ----------------------------------- st.markdown("---") with st.expander("Developer tools (advanced)", expanded=False): st.markdown("#### Backend Status") if chains_ok: try: status = ping_backends() st.write(f"OpenAI: {'✅' if status.get('openai') else '⚠️'}") st.write(f"Pinecone: {'✅' if status.get('pinecone') else '⚠️'}") if status.get("index_name"): st.write(f"Index: `{status['index_name']}`") except Exception as e: st.warning(f"Health check error: {e}") else: st.error("RAG chains import failed.") st.exception(chains_import_error) # ============================================================ # Hero + description # ============================================================ st.markdown( """Explore lawsuits involving artificial intelligence across U.S. courts. Use search and filters to browse cases, read summaries, and run AI-powered Q&A on individual matters or the full corpus.
This tracker is a centralized, user-friendly platform for monitoring AI-related lawsuits across the United States. It helps users quickly see where and how AI issues are being litigated, understand the posture of each case, and compare patterns across jurisdictions.
Navigate and filter AI litigation using search and sidebar filters. View case details, summaries, filing dates, latest activity dates, and export your view as a CSV for further analysis.
Current statistics for the filtered case set
Total Cases in View
Jurisdictions in View
{recent_label}
Comprehensive listing of AI-related lawsuits in the filtered view
Select a specific case to view its details and run AI-powered Q&A grounded in that case's documents and metadata.
Choose a docket and ask focused questions about that case
Ask questions across the full litigation corpus using RAG-based search over all tracked cases.
Explore broader patterns, themes, and trends in AI-related litigation
© 2025 AI Litigation Tracker · Vanderbilt AI Law Lab × Lawfare
Court data provided by CourtListener, a project of Free Law Project.