CoolFace
Apppublic

idxwze/SentinelOps

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
app.py1668 linesDownload Raw Back to root
1from __future__ import annotations2 3import json4import time5from pathlib import Path6from typing import Dict, List, Tuple7 8import gradio as gr9import plotly.graph_objects as go10from plotly.subplots import make_subplots11 12 13BASE_DIR = Path(__file__).parent14DATA_DIR = BASE_DIR / "data"15 16 17TEXT = {18    "en": {19        "tagline": "AI incident copilot for reliability and security triage",20        "subtitle": "Review live signals, isolate likely causes, and decide the next safe action faster.",21        "workspace_status": "Workspace status",22        "language": "Language",23        "text_size": "Text size",24        "contrast": "High contrast",25        "compact": "Compact",26        "comfortable": "Comfortable",27        "large": "Large",28        "nav_dashboard": "Dashboard",29        "nav_incidents": "Incidents",30        "nav_copilot": "Copilot",31        "nav_profile": "Profile",32        "queue_title": "Incident queue",33        "queue_subtitle": "Active incidents and current response state",34        "incident_selector": "Selected incident",35        "summary_title": "Incident summary",36        "summary_subtitle": "Current condition and recent operational context",37        "signals": "Core signals",38        "signals_subtitle": "Latency and error behavior for the selected service",39        "load": "Infrastructure and traffic",40        "load_subtitle": "Capacity pressure and request shape",41        "dependencies": "Service dependencies",42        "dependencies_subtitle": "Connected systems affected by this incident",43        "hypotheses": "Ranked hypotheses",44        "hypotheses_subtitle": "Most likely explanations based on current evidence",45        "actions": "Recommended actions",46        "actions_subtitle": "Practical next checks for the on-call engineer",47        "profile_title": "Analyst profile",48        "profile_subtitle": "Local prototype profile and working preferences",49        "profile_name": "Name",50        "profile_role": "Role",51        "profile_team": "Team",52        "profile_language": "Preferred language",53        "profile_focus": "Alert focus",54        "profile_status": "On-call status",55        "profile_name_value": "Maya Chen",56        "profile_role_value": "Junior SRE",57        "profile_team_value": "Platform Reliability",58        "profile_focus_value": "Payments and authentication",59        "profile_status_value": "Primary on-call",60        "copilot": "AI copilot",61        "copilot_subtitle": "Incident-aware assistant for triage and explanation",62        "copilot_placeholder": "Ask about likely cause, blast radius, recent changes, or the best next check...",63        "copilot_empty": "Choose an incident to start a focused triage conversation.",64        "copilot_closed": "Copilot minimized",65        "copilot_open": "Copilot ready",66        "launcher": "Open Copilot",67        "close_chat": "Close",68        "suggested_prompts": "Suggested prompts",69        "ask_button": "Send",70        "why_checkout": "Why is checkout failing?",71        "recent_changes": "What changed recently?",72        "root_cause": "What is the most likely root cause?",73        "check_first": "What should I check first?",74        "security_concern": "Is there a security concern?",75        "current_state": "Current state",76        "recent_change": "Recent change",77        "affected_service": "Affected service",78        "latency": "Latency",79        "error_rate": "Error rate",80        "cpu": "CPU",81        "requests": "Request volume",82        "confidence": "Confidence",83        "next_check": "Next check",84        "footer_note": "SentinelOps prototype for incident triage and reliability UX",85        "status_loading": "Loading incident context",86        "status_loaded": "Incident context loaded",87        "status_no_incident": "Select an incident to unlock the workspace",88        "status_chat_blocked": "Select an incident before using the copilot",89        "status_no_question": "Enter a question or choose a prompt",90        "assistant_intro": "Copilot ready. I can summarize likely causes, recent changes, dependency impact, and recommended next checks.",91        "empty_hypotheses": "No hypotheses are available for this incident yet.",92        "empty_dependencies": "No dependency impact data available.",93        "empty_overview": "Select an incident from the queue to inspect service health, signals, and likely causes.",94        "unavailable": "Unavailable",95    },96    "fr": {97        "tagline": "Copilote IA d'incident pour le triage fiabilite et securite",98        "subtitle": "Analysez les signaux, isolez les causes probables et choisissez plus vite la prochaine action sure.",99        "workspace_status": "Etat de l'espace",100        "language": "Langue",101        "text_size": "Taille du texte",102        "contrast": "Contraste eleve",103        "compact": "Compact",104        "comfortable": "Confortable",105        "large": "Grand",106        "nav_dashboard": "Tableau",107        "nav_incidents": "Incidents",108        "nav_copilot": "Copilote",109        "nav_profile": "Profil",110        "queue_title": "File d'incidents",111        "queue_subtitle": "Incidents actifs et etat actuel de reponse",112        "incident_selector": "Incident selectionne",113        "summary_title": "Resume de l'incident",114        "summary_subtitle": "Etat actuel et contexte operationnel recent",115        "signals": "Signaux principaux",116        "signals_subtitle": "Comportement de latence et d'erreur pour le service selectionne",117        "load": "Infrastructure et trafic",118        "load_subtitle": "Pression de capacite et forme du trafic",119        "dependencies": "Dependances du service",120        "dependencies_subtitle": "Systemes connectes touches par cet incident",121        "hypotheses": "Hypotheses classees",122        "hypotheses_subtitle": "Explications les plus probables selon les preuves actuelles",123        "actions": "Actions recommandees",124        "actions_subtitle": "Prochains controles concrets pour l'ingenieur d'astreinte",125        "profile_title": "Profil analyste",126        "profile_subtitle": "Profil local du prototype et preferences de travail",127        "profile_name": "Nom",128        "profile_role": "Role",129        "profile_team": "Equipe",130        "profile_language": "Langue preferee",131        "profile_focus": "Focus alertes",132        "profile_status": "Statut d'astreinte",133        "profile_name_value": "Maya Chen",134        "profile_role_value": "SRE junior",135        "profile_team_value": "Fiabilite plateforme",136        "profile_focus_value": "Paiements et authentification",137        "profile_status_value": "Astreinte principale",138        "copilot": "Copilote IA",139        "copilot_subtitle": "Assistant contextuel pour le triage et l'explication",140        "copilot_placeholder": "Posez une question sur la cause probable, la portee, les changements recents ou le meilleur prochain controle...",141        "copilot_empty": "Choisissez un incident pour commencer une conversation de triage ciblee.",142        "copilot_closed": "Copilote reduit",143        "copilot_open": "Copilote pret",144        "launcher": "Ouvrir le copilote",145        "close_chat": "Fermer",146        "suggested_prompts": "Invites suggerees",147        "ask_button": "Envoyer",148        "why_checkout": "Pourquoi le checkout echoue-t-il ?",149        "recent_changes": "Qu'est-ce qui a change recemment ?",150        "root_cause": "Quelle est la cause la plus probable ?",151        "check_first": "Que dois-je verifier en premier ?",152        "security_concern": "Y a-t-il un risque de securite ?",153        "current_state": "Etat actuel",154        "recent_change": "Changement recent",155        "affected_service": "Service affecte",156        "latency": "Latence",157        "error_rate": "Taux d'erreur",158        "cpu": "CPU",159        "requests": "Volume de requetes",160        "confidence": "Confiance",161        "next_check": "Prochain controle",162        "footer_note": "Prototype SentinelOps pour le triage d'incident et l'UX fiabilite",163        "status_loading": "Chargement du contexte d'incident",164        "status_loaded": "Contexte d'incident charge",165        "status_no_incident": "Selectionnez un incident pour activer l'espace de travail",166        "status_chat_blocked": "Selectionnez un incident avant d'utiliser le copilote",167        "status_no_question": "Saisissez une question ou choisissez une invite",168        "assistant_intro": "Copilote pret. Je peux resumer les causes probables, les changements recents, l'impact sur les dependances et les prochains controles recommandes.",169        "empty_hypotheses": "Aucune hypothese n'est disponible pour cet incident.",170        "empty_dependencies": "Aucune information sur l'impact des dependances.",171        "empty_overview": "Selectionnez un incident dans la file pour inspecter la sante du service, les signaux et les causes probables.",172        "unavailable": "Indisponible",173    },174}175 176 177SEVERITY_COLORS = {178    "Critical": "#dc2626",179    "High": "#ea580c",180    "Medium": "#ca8a04",181    "Low": "#16a34a",182}183 184STATUS_COLORS = {185    "Investigating": "#b45309",186    "Mitigating": "#2563eb",187    "Escalated": "#be123c",188    "Monitoring": "#0f766e",189    "Contained": "#7c3aed",190}191 192 193def load_json(path: Path):194    with path.open("r", encoding="utf-8") as file:195        return json.load(file)196 197 198def load_datasets():199    incidents = load_json(DATA_DIR / "incidents.json")200    metrics = load_json(DATA_DIR / "metrics.json")201    hypotheses = load_json(DATA_DIR / "hypotheses.json")202    return incidents, metrics, hypotheses203 204 205INCIDENTS, METRICS, HYPOTHESES = load_datasets()206INCIDENT_MAP = {incident["id"]: incident for incident in INCIDENTS}207 208 209def t(lang: str, key: str) -> str:210    return TEXT.get(lang, TEXT["en"]).get(key, key)211 212 213def format_number(value: float, unit: str = "") -> str:214    if unit == "%":215        return f"{value:.1f}%"216    if unit == "ms":217        return f"{int(value)} ms"218    if unit == "rpm":219        return f"{int(value):,} rpm"220    return f"{value}"221 222 223def get_incident_label(incident: Dict, lang: str) -> str:224    return f"{incident[f'title_{lang}']} | {incident['severity']} | {incident['service']}"225 226 227def get_incident_choices(lang: str) -> List[Tuple[str, str]]:228    return [(get_incident_label(incident, lang), incident["id"]) for incident in INCIDENTS]229 230 231def get_accessibility_classes(text_size: str, contrast: bool) -> str:232    contrast_class = "contrast-on" if contrast else "contrast-off"233    size_class = f"size-{text_size}"234    return f"{contrast_class} {size_class}"235 236 237def build_header_html(lang: str, text_size: str, contrast: bool) -> str:238    classes = get_accessibility_classes(text_size, contrast)239    return f"""240    <div class="topbar-shell {classes}">241        <div class="brand-cluster">242            <div class="brand-mark">SO</div>243            <div class="brand-copy">244                <div class="brand-title">SentinelOps</div>245                <div class="brand-subtitle">{t(lang, 'subtitle')}</div>246            </div>247        </div>248        <div class="nav-cluster">249            <div class="nav-item active">{t(lang, 'nav_dashboard')}</div>250            <div class="nav-item">{t(lang, 'nav_incidents')}</div>251            <div class="nav-item">{t(lang, 'nav_copilot')}</div>252            <div class="nav-item">{t(lang, 'nav_profile')}</div>253        </div>254        <div class="account-cluster">255            <div class="account-meta">256                <div class="account-label">{t(lang, 'workspace_status')}</div>257                <div class="account-value">SentinelOps / MVP</div>258            </div>259            <div class="account-avatar">MC</div>260        </div>261    </div>262    """263 264 265def build_status_html(message: str, lang: str, text_size: str, contrast: bool, mode: str = "neutral") -> str:266    classes = get_accessibility_classes(text_size, contrast)267    return f"""268    <div class="status-shell {classes} mode-{mode}">269        <div class="status-label">{t(lang, 'workspace_status')}</div>270        <div class="status-value">{message}</div>271    </div>272    """273 274 275def build_section_heading(title: str, subtitle: str) -> str:276    return f"""277    <div class="section-heading">278        <div class="section-title">{title}</div>279        <div class="section-subtitle">{subtitle}</div>280    </div>281    """282 283 284def build_footer_html(lang: str) -> str:285    return f"""286    <div class="footer-shell">287        <div class="footer-left">SentinelOps</div>288        <div class="footer-right">{t(lang, 'footer_note')}</div>289    </div>290    """291 292 293def build_incident_cards(selected_id: str | None, lang: str, text_size: str, contrast: bool) -> str:294    classes = get_accessibility_classes(text_size, contrast)295    cards = []296    for incident in INCIDENTS:297        selected_class = "selected" if incident["id"] == selected_id else ""298        severity_color = SEVERITY_COLORS.get(incident["severity"], "#64748b")299        status_color = STATUS_COLORS.get(incident["status"], "#64748b")300        cards.append(301            f"""302            <div class="incident-card {classes} {selected_class}">303                <div class="incident-card-top">304                    <span class="severity-pill" style="background:{severity_color};">{incident['severity']}</span>305                    <span class="status-pill" style="color:{status_color}; border-color:{status_color};">{incident['status']}</span>306                </div>307                <div class="incident-title">{incident[f'title_{lang}']}</div>308                <div class="incident-meta">{incident['service']} • {incident['timestamp']}</div>309                <div class="incident-description">{incident[f'short_status_{lang}']}</div>310            </div>311            """312        )313    return "\n".join(cards)314 315 316def build_profile_html(lang: str, text_size: str, contrast: bool) -> str:317    classes = get_accessibility_classes(text_size, contrast)318    items = [319        (t(lang, "profile_name"), t(lang, "profile_name_value")),320        (t(lang, "profile_role"), t(lang, "profile_role_value")),321        (t(lang, "profile_team"), t(lang, "profile_team_value")),322        (t(lang, "profile_language"), "EN / FR"),323        (t(lang, "profile_focus"), t(lang, "profile_focus_value")),324        (t(lang, "profile_status"), t(lang, "profile_status_value")),325    ]326    rows = "".join(327        [328            f"""329            <div class="profile-row">330                <div class="profile-key">{label}</div>331                <div class="profile-value">{value}</div>332            </div>333            """334            for label, value in items335        ]336    )337    return f"""338    <div class="panel-card profile-card {classes}">339        <div class="profile-top">340            <div>341                <div class="profile-avatar">MC</div>342            </div>343            <div class="profile-summary">344                <h3>{t(lang, 'profile_title')}</h3>345                <p>{t(lang, 'profile_subtitle')}</p>346            </div>347        </div>348        <div class="profile-grid">349            {rows}350        </div>351    </div>352    """353 354 355def build_overview_html(incident_id: str | None, lang: str, text_size: str, contrast: bool) -> str:356    classes = get_accessibility_classes(text_size, contrast)357    if not incident_id or incident_id not in INCIDENT_MAP:358        return f"""359        <div class="panel-card workspace-card {classes}">360            <h3>{t(lang, 'summary_title')}</h3>361            <p class="empty-state">{t(lang, 'empty_overview')}</p>362        </div>363        """364 365    incident = INCIDENT_MAP[incident_id]366    metric_payload = METRICS.get(incident_id, {})367    stats = metric_payload.get("current_stats", {})368    stat_items = [369        (t(lang, "latency"), format_number(stats.get("latency_ms", 0), "ms")),370        (t(lang, "error_rate"), format_number(stats.get("error_rate_pct", 0), "%")),371        (t(lang, "cpu"), format_number(stats.get("cpu_pct", 0), "%")),372        (t(lang, "requests"), format_number(stats.get("request_volume_rpm", 0), "rpm")),373    ]374    stat_html = "".join(375        [376            f"""377            <div class="stat-card">378                <div class="stat-label">{label}</div>379                <div class="stat-value">{value}</div>380            </div>381            """382            for label, value in stat_items383        ]384    )385    severity_color = SEVERITY_COLORS.get(incident["severity"], "#64748b")386    return f"""387    <div class="panel-card workspace-card {classes}">388        <div class="workspace-top">389            <div>390                <div class="eyebrow">{t(lang, 'affected_service')}</div>391                <h3>{incident['service']}</h3>392            </div>393            <span class="severity-pill" style="background:{severity_color};">{incident['severity']}</span>394        </div>395        <div class="info-grid">396            <div class="info-card">397                <div class="info-label">{t(lang, 'current_state')}</div>398                <div class="info-body">{incident[f'health_summary_{lang}']}</div>399            </div>400            <div class="info-card">401                <div class="info-label">{t(lang, 'recent_change')}</div>402                <div class="info-body">{incident[f'recent_change_{lang}']}</div>403            </div>404        </div>405        <div class="summary-block">406            <div class="info-label">{t(lang, 'summary_title')}</div>407            <p>{incident[f'summary_{lang}']}</p>408        </div>409        <div class="stat-grid">410            {stat_html}411        </div>412    </div>413    """414 415 416def build_dependencies_html(incident_id: str | None, lang: str, text_size: str, contrast: bool) -> str:417    classes = get_accessibility_classes(text_size, contrast)418    if not incident_id or incident_id not in METRICS:419        return f"""420        <div class="panel-card workspace-card {classes}">421            <h3>{t(lang, 'dependencies')}</h3>422            <p class="empty-state">{t(lang, 'empty_dependencies')}</p>423        </div>424        """425 426    dependencies = METRICS[incident_id].get("dependency_health", [])427    dependency_html = "".join(428        [429            f"""430            <div class="dependency-node">431                <div class="dependency-name">{item['name']}</div>432                <div class="dependency-status status-{item['status'].lower()}">{item['status']}</div>433                <div class="dependency-impact">{item[f'impact_{lang}']}</div>434            </div>435            """436            for item in dependencies437        ]438    )439    return f"""440    <div class="panel-card workspace-card {classes}">441        <div class="section-inline-title">{t(lang, 'dependencies')}</div>442        <div class="dependency-grid">{dependency_html}</div>443    </div>444    """445 446 447def build_hypotheses_html(incident_id: str | None, lang: str, text_size: str, contrast: bool) -> str:448    classes = get_accessibility_classes(text_size, contrast)449    items = HYPOTHESES.get(incident_id, [])450    if not items:451        return f"""452        <div class="panel-card workspace-card {classes}">453            <h3>{t(lang, 'hypotheses')}</h3>454            <p class="empty-state">{t(lang, 'empty_hypotheses')}</p>455        </div>456        """457 458    cards = []459    for item in items:460        risk_color = item.get("risk_color", "#64748b")461        cards.append(462            f"""463            <div class="hypothesis-card">464                <div class="hypothesis-top">465                    <div class="hypothesis-title">{item[f'title_{lang}']}</div>466                    <span class="confidence-pill" style="color:{risk_color}; border-color:{risk_color};">467                        {t(lang, 'confidence')}: {item['confidence_score']}%468                    </span>469                </div>470                <p>{item[f'explanation_{lang}']}</p>471                <div class="next-check"><strong>{t(lang, 'next_check')}:</strong> {item[f'recommended_action_{lang}']}</div>472            </div>473            """474        )475    return f"""476    <div class="panel-card workspace-card {classes}">477        <div class="hypothesis-grid">{''.join(cards)}</div>478    </div>479    """480 481 482def build_actions_html(incident_id: str | None, lang: str, text_size: str, contrast: bool) -> str:483    classes = get_accessibility_classes(text_size, contrast)484    items = HYPOTHESES.get(incident_id, [])485    if not items:486        return f"""487        <div class="panel-card workspace-card {classes}">488            <p class="empty-state">{t(lang, 'empty_hypotheses')}</p>489        </div>490        """491 492    actions = "".join(493        [494            f"""495            <div class="action-item">496                <div class="action-rank">{index + 1}</div>497                <div class="action-copy">498                    <div class="action-title">{item[f'title_{lang}']}</div>499                    <div class="action-text">{item[f'recommended_action_{lang}']}</div>500                </div>501            </div>502            """503            for index, item in enumerate(items[:3])504        ]505    )506    return f"""507    <div class="panel-card workspace-card {classes}">508        <div class="action-list">{actions}</div>509    </div>510    """511 512 513def build_empty_figure(title: str, lang: str, contrast: bool) -> go.Figure:514    font_color = "#0f172a"515    paper_bg = "#ffffff"516    plot_bg = "#ffffff"517    if contrast:518        font_color = "#020617"519    fig = go.Figure()520    fig.update_layout(521        template="plotly_white",522        title=title,523        paper_bgcolor=paper_bg,524        plot_bgcolor=plot_bg,525        font=dict(color=font_color, size=13),526        margin=dict(l=24, r=18, t=54, b=24),527        xaxis=dict(visible=False),528        yaxis=dict(visible=False),529        annotations=[530            dict(531                text=t(lang, "status_no_incident"),532                x=0.5,533                y=0.5,534                xref="paper",535                yref="paper",536                showarrow=False,537                font=dict(size=14, color="#64748b"),538            )539        ],540    )541    return fig542 543 544def build_signal_figure(incident_id: str | None, lang: str, contrast: bool) -> go.Figure:545    if not incident_id or incident_id not in METRICS:546        return build_empty_figure(t(lang, "signals"), lang, contrast)547    payload = METRICS[incident_id]548    timestamps = payload["timestamps"]549    series = payload["series"]550    fig = make_subplots(rows=1, cols=2, subplot_titles=(t(lang, "latency"), t(lang, "error_rate")))551    fig.add_trace(552        go.Scatter(553            x=timestamps,554            y=series["latency_ms"],555            mode="lines+markers",556            name=t(lang, "latency"),557            line=dict(color="#2563eb", width=2.5),558            fill="tozeroy",559            fillcolor="rgba(37,99,235,0.10)",560        ),561        row=1,562        col=1,563    )564    fig.add_trace(565        go.Scatter(566            x=timestamps,567            y=series["error_rate_pct"],568            mode="lines+markers",569            name=t(lang, "error_rate"),570            line=dict(color="#ea580c", width=2.5),571            fill="tozeroy",572            fillcolor="rgba(234,88,12,0.10)",573        ),574        row=1,575        col=2,576    )577    fig.update_layout(578        template="plotly_white",579        title=t(lang, "signals"),580        paper_bgcolor="#ffffff",581        plot_bgcolor="#ffffff",582        font=dict(color="#0f172a", size=13),583        margin=dict(l=24, r=18, t=56, b=24),584        legend=dict(orientation="h", yanchor="bottom", y=1.08, xanchor="right", x=1.0),585        height=320,586    )587    fig.update_xaxes(showgrid=False, tickangle=-35, linecolor="#e2e8f0")588    fig.update_yaxes(showgrid=True, gridcolor="#eef2f7", zerolinecolor="#eef2f7")589    return fig590 591 592def build_load_figure(incident_id: str | None, lang: str, contrast: bool) -> go.Figure:593    if not incident_id or incident_id not in METRICS:594        return build_empty_figure(t(lang, "load"), lang, contrast)595    payload = METRICS[incident_id]596    timestamps = payload["timestamps"]597    series = payload["series"]598    fig = make_subplots(rows=1, cols=2, subplot_titles=(t(lang, "cpu"), t(lang, "requests")))599    fig.add_trace(600        go.Scatter(601            x=timestamps,602            y=series["cpu_pct"],603            mode="lines+markers",604            name=t(lang, "cpu"),605            line=dict(color="#0f766e", width=2.5),606            fill="tozeroy",607            fillcolor="rgba(15,118,110,0.10)",608        ),609        row=1,610        col=1,611    )612    fig.add_trace(613        go.Bar(614            x=timestamps,615            y=series["request_volume_rpm"],616            name=t(lang, "requests"),617            marker=dict(color="#7c3aed", opacity=0.82),618        ),619        row=1,620        col=2,621    )622    fig.update_layout(623        template="plotly_white",624        title=t(lang, "load"),625        paper_bgcolor="#ffffff",626        plot_bgcolor="#ffffff",627        font=dict(color="#0f172a", size=13),628        margin=dict(l=24, r=18, t=56, b=24),629        legend=dict(orientation="h", yanchor="bottom", y=1.08, xanchor="right", x=1.0),630        height=320,631    )632    fig.update_xaxes(showgrid=False, tickangle=-35, linecolor="#e2e8f0")633    fig.update_yaxes(showgrid=True, gridcolor="#eef2f7", zerolinecolor="#eef2f7")634    return fig635 636 637def get_suggested_prompts(lang: str) -> List[str]:638    return [639        t(lang, "why_checkout"),640        t(lang, "recent_changes"),641        t(lang, "root_cause"),642        t(lang, "check_first"),643        t(lang, "security_concern"),644    ]645 646 647def get_copilot_intro(lang: str) -> List[Dict[str, str]]:648    return [{"role": "assistant", "content": t(lang, "assistant_intro")}]649 650 651def build_copilot_response(incident_id: str, lang: str, question: str) -> str:652    incident = INCIDENT_MAP[incident_id]653    top_hypothesis = HYPOTHESES.get(incident_id, [{}])[0]654    responses = incident["copilot_responses"]655    normalized = question.lower()656    if any(keyword in normalized for keyword in ["change", "changed", "recent", "modification", "change rec", "changement"]):657        return responses[f"changes_{lang}"]658    if any(keyword in normalized for keyword in ["root", "cause", "likely", "probable", "racine"]):659        return responses[f"root_cause_{lang}"]660    if any(keyword in normalized for keyword in ["check", "first", "verify", "verifier", "premier"]):661        return responses[f"check_first_{lang}"]662    if any(keyword in normalized for keyword in ["security", "abuse", "credential", "securite", "attaque"]):663        return responses[f"security_{lang}"]664    if any(keyword in normalized for keyword in ["why", "failing", "down", "latency", "timeout", "pourquoi", "echec"]):665        return responses[f"summary_{lang}"]666    fallback_title = top_hypothesis.get(f"title_{lang}", t(lang, "unavailable"))667    fallback_action = top_hypothesis.get(f"recommended_action_{lang}", t(lang, "unavailable"))668    return (669        f"{responses[f'summary_{lang}']} "670        f"{t(lang, 'next_check')}: {fallback_action}. "671        f"{t(lang, 'confidence')}: {top_hypothesis.get('confidence_score', 0)}% on {fallback_title}."672    )673 674 675def sync_ui(lang: str, selected_incident: str | None, text_size: str, contrast: bool, chat_open: bool):676    enabled = selected_incident is not None677    status_message = t(lang, "status_loaded" if enabled else "status_no_incident")678    status_mode = "healthy" if enabled else "neutral"679    prompts = get_suggested_prompts(lang)680    chat_value = get_copilot_intro(lang)681    launcher = f"● {t(lang, 'copilot')}" if chat_open or enabled else f"○ {t(lang, 'launcher')}"682    return (683        build_header_html(lang, text_size, contrast),684        build_status_html(status_message, lang, text_size, contrast, status_mode),685        build_section_heading(t(lang, "queue_title"), t(lang, "queue_subtitle")),686        build_incident_cards(selected_incident, lang, text_size, contrast),687        gr.update(choices=get_incident_choices(lang), label=t(lang, "incident_selector"), value=selected_incident),688        gr.update(label=t(lang, "language")),689        gr.update(label=t(lang, "text_size")),690        gr.update(label=t(lang, "contrast")),691        build_profile_html(lang, text_size, contrast),692        build_section_heading(t(lang, "summary_title"), t(lang, "summary_subtitle")),693        build_overview_html(selected_incident, lang, text_size, contrast),694        build_section_heading(t(lang, "signals"), t(lang, "signals_subtitle")),695        build_signal_figure(selected_incident, lang, contrast),696        build_section_heading(t(lang, "load"), t(lang, "load_subtitle")),697        build_load_figure(selected_incident, lang, contrast),698        build_section_heading(t(lang, "dependencies"), t(lang, "dependencies_subtitle")),699        build_dependencies_html(selected_incident, lang, text_size, contrast),700        build_section_heading(t(lang, "hypotheses"), t(lang, "hypotheses_subtitle")),701        build_hypotheses_html(selected_incident, lang, text_size, contrast),702        build_section_heading(t(lang, "actions"), t(lang, "actions_subtitle")),703        build_actions_html(selected_incident, lang, text_size, contrast),704        build_section_heading(t(lang, "copilot"), t(lang, "copilot_subtitle")),705        gr.update(value=chat_value, label=t(lang, "copilot")),706        gr.update(placeholder=t(lang, "copilot_placeholder"), interactive=enabled),707        gr.update(value=t(lang, "ask_button"), interactive=enabled),708        gr.update(value=prompts[0], interactive=enabled),709        gr.update(value=prompts[1], interactive=enabled),710        gr.update(value=prompts[2], interactive=enabled),711        gr.update(value=prompts[3], interactive=enabled),712        gr.update(value=prompts[4], interactive=enabled),713        gr.update(value=launcher),714        gr.update(visible=chat_open),715        build_footer_html(lang),716        chat_value,717    )718 719 720def update_incident_view(selected_incident: str | None, lang: str, text_size: str, contrast: bool):721    cards = build_incident_cards(selected_incident, lang, text_size, contrast)722    intro_history = get_copilot_intro(lang)723    if not selected_incident:724        yield (725            cards,726            build_overview_html(None, lang, text_size, contrast),727            build_signal_figure(None, lang, contrast),728            build_load_figure(None, lang, contrast),729            build_dependencies_html(None, lang, text_size, contrast),730            build_hypotheses_html(None, lang, text_size, contrast),731            build_actions_html(None, lang, text_size, contrast),732            intro_history,733            intro_history,734            build_status_html(t(lang, "status_no_incident"), lang, text_size, contrast, "neutral"),735            gr.update(interactive=False, placeholder=t(lang, "copilot_placeholder")),736            gr.update(interactive=False),737            gr.update(interactive=False),738            gr.update(interactive=False),739            gr.update(interactive=False),740            gr.update(interactive=False),741            gr.update(interactive=False),742            gr.update(interactive=False),743            gr.update(value=f"○ {t(lang, 'launcher')}"),744        )745        return746    yield (747        cards,748        build_overview_html(None, lang, text_size, contrast),749        build_empty_figure(t(lang, "signals"), lang, contrast),750        build_empty_figure(t(lang, "load"), lang, contrast),751        build_dependencies_html(None, lang, text_size, contrast),752        build_hypotheses_html(None, lang, text_size, contrast),753        build_actions_html(None, lang, text_size, contrast),754        intro_history,755        intro_history,756        build_status_html(t(lang, "status_loading"), lang, text_size, contrast, "loading"),757        gr.update(interactive=False, placeholder=t(lang, "copilot_placeholder")),758        gr.update(interactive=False),759        gr.update(interactive=False),760        gr.update(interactive=False),761        gr.update(interactive=False),762        gr.update(interactive=False),763        gr.update(interactive=False),764        gr.update(interactive=False),765        gr.update(value=f"● {t(lang, 'copilot')}"),766    )767    time.sleep(0.25)768    yield (769        cards,770        build_overview_html(selected_incident, lang, text_size, contrast),771        build_signal_figure(selected_incident, lang, contrast),772        build_load_figure(selected_incident, lang, contrast),773        build_dependencies_html(selected_incident, lang, text_size, contrast),774        build_hypotheses_html(selected_incident, lang, text_size, contrast),775        build_actions_html(selected_incident, lang, text_size, contrast),776        intro_history,777        intro_history,778        build_status_html(t(lang, "status_loaded"), lang, text_size, contrast, "healthy"),779        gr.update(interactive=True, placeholder=t(lang, "copilot_placeholder")),780        gr.update(interactive=True),781        gr.update(interactive=True),782        gr.update(interactive=True),783        gr.update(interactive=True),784        gr.update(interactive=True),785        gr.update(interactive=True),786        gr.update(interactive=True),787        gr.update(value=f"● {t(lang, 'copilot')}"),788    )789 790 791def toggle_chat(chat_open: bool, lang: str):792    next_state = not chat_open793    label = t(lang, "copilot") if next_state else t(lang, "launcher")794    prefix = "●" if next_state else "○"795    return next_state, gr.update(visible=next_state), gr.update(value=f"{prefix} {label}")796 797 798def submit_chat(799    message: str,800    history: List[Dict[str, str]] | None,801    selected_incident: str | None,802    lang: str,803    text_size: str,804    contrast: bool,805):806    history = history or []807    if not selected_incident:808        history.append({"role": "assistant", "content": t(lang, "status_chat_blocked")})809        return history, history, "", build_status_html(t(lang, "status_chat_blocked"), lang, text_size, contrast, "warning")810    if not message or not message.strip():811        return history, history, "", build_status_html(t(lang, "status_no_question"), lang, text_size, contrast, "warning")812    answer = build_copilot_response(selected_incident, lang, message.strip())813    history.append({"role": "user", "content": message.strip()})814    history.append({"role": "assistant", "content": answer})815    return history, history, "", build_status_html(t(lang, "status_loaded"), lang, text_size, contrast, "healthy")816 817 818def use_prompt(819    prompt: str,820    history: List[Dict[str, str]] | None,821    selected_incident: str | None,822    lang: str,823    text_size: str,824    contrast: bool,825):826    return submit_chat(prompt, history, selected_incident, lang, text_size, contrast)827 828 829CUSTOM_CSS = """830:root {831  --bg: #f5f7fb;832  --bg-soft: #eef2f7;833  --panel: #ffffff;834  --panel-soft: #fbfcfe;835  --panel-accent: #f2f7ff;836  --text: #0f172a;837  --text-soft: #475569;838  --text-faint: #64748b;839  --border: #e6ebf2;840  --border-strong: #d4dce8;841  --accent: #2563eb;842  --accent-soft: #dbeafe;843  --success: #15803d;844  --warning: #b45309;845  --danger: #dc2626;846  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);847  --shadow-md: 0 12px 32px rgba(15, 23, 42, 0.06);848  --radius-xl: 18px;849  --radius-lg: 16px;850  --radius-md: 12px;851}852 853body, .gradio-container {854  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;855  background:856    radial-gradient(circle at top left, rgba(37, 99, 235, 0.035), transparent 26%),857    linear-gradient(180deg, #f8fafc 0%, #f5f7fb 100%);858  color: var(--text);859}860 861.gradio-container {862  max-width: 1560px !important;863  padding: 20px 18px 28px !important;864}865 866.gradio-container .block,867.gradio-container .gr-box,868.gradio-container .gr-group {869  border: none !important;870  box-shadow: none !important;871  background: transparent !important;872}873 874.gradio-container button,875.gradio-container input,876.gradio-container textarea,877.gradio-container select,878.gradio-container label,879.gradio-container div,880.gradio-container span,881.gradio-container p {882  font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;883}884 885.app-shell {886  gap: 16px !important;887}888 889.topbar-shell {890  background: rgba(255, 255, 255, 0.88);891  backdrop-filter: blur(10px);892  border: 1px solid var(--border);893  border-radius: 18px;894  box-shadow: var(--shadow-sm);895  padding: 14px 18px;896  display: grid;897  grid-template-columns: 1.3fr 1fr 0.9fr;898  align-items: center;899  gap: 18px;900}901 902.brand-cluster,903.account-cluster {904  display: flex;905  align-items: center;906  gap: 12px;907}908 909.brand-mark {910  width: 40px;911  height: 40px;912  border-radius: 12px;913  background: linear-gradient(180deg, #eff6ff, #dbeafe);914  color: var(--accent);915  display: flex;916  align-items: center;917  justify-content: center;918  font-weight: 800;919  font-size: 14px;920}921 922.brand-title {923  font-size: 16px;924  font-weight: 700;925  color: var(--text);926}927 928.brand-subtitle {929  margin-top: 2px;930  font-size: 12px;931  color: var(--text-faint);932}933 934.nav-cluster {935  display: flex;936  align-items: center;937  justify-content: center;938  gap: 8px;939}940 941.nav-item {942  padding: 8px 12px;943  border-radius: 10px;944  color: var(--text-faint);945  font-size: 13px;946  font-weight: 600;947  border: 1px solid transparent;948}949 950.nav-item.active {951  background: var(--panel-accent);952  color: var(--accent);953  border-color: #bfdbfe;954}955 956.account-meta {957  margin-left: auto;958  text-align: right;959}960 961.account-label {962  font-size: 11px;963  color: var(--text-faint);964  text-transform: uppercase;965  letter-spacing: 0.06em;966  font-weight: 700;967}968 969.account-value {970  margin-top: 3px;971  font-size: 13px;972  color: var(--text-soft);973}974 975.account-avatar,976.profile-avatar {977  width: 38px;978  height: 38px;979  border-radius: 999px;980  background: #eff6ff;981  color: var(--accent);982  display: flex;983  align-items: center;984  justify-content: center;985  font-weight: 700;986}987 988.settings-shell,989.status-shell,990.pane-shell,991.panel-card,992.chat-popup {993  background: var(--panel);994  border: 1px solid var(--border);995  box-shadow: var(--shadow-sm);996}997 998.settings-shell {999  border-radius: 16px;1000  padding: 14px 16px 8px;1001}1002 1003.status-shell {1004  border-radius: 16px;1005  padding: 16px 18px;1006}1007 1008.status-label,1009.eyebrow,1010.info-label,1011.stat-label,1012.profile-key,1013.dependency-impact {1014  font-size: 11px;1015  color: var(--text-faint);1016  text-transform: uppercase;1017  letter-spacing: 0.06em;1018  font-weight: 700;1019}1020 1021.status-value {1022  margin-top: 8px;1023  font-size: 15px;1024  color: var(--text);1025  font-weight: 700;1026}1027 1028.mode-healthy .status-value { color: var(--success); }1029.mode-warning .status-value { color: var(--warning); }1030.mode-loading .status-value { color: var(--accent); }1031 1032.section-heading {1033  padding: 2px 2px 10px;1034}1035 1036.section-title {1037  font-size: 15px;1038  font-weight: 700;1039  color: var(--text);1040}1041 1042.section-subtitle {1043  margin-top: 3px;1044  font-size: 12px;1045  color: var(--text-faint);1046}1047 1048.master-detail {1049  gap: 16px !important;1050  align-items: start !important;1051}1052 1053.pane-shell {1054  border-radius: 18px;1055  padding: 14px;1056}1057 1058.incident-card {1059  background: var(--panel);1060  border: 1px solid var(--border);1061  border-radius: 14px;1062  padding: 14px;1063  margin-bottom: 10px;1064  transition: border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;1065}1066 1067.incident-card:hover {1068  border-color: var(--border-strong);1069  box-shadow: var(--shadow-sm);1070}1071 1072.incident-card.selected {1073  background: var(--panel-accent);1074  border-color: #93c5fd;1075  box-shadow: inset 3px 0 0 var(--accent);1076}1077 1078.incident-card-top,1079.workspace-top,1080.hypothesis-top,1081.profile-top {1082  display: flex;1083  align-items: flex-start;1084  justify-content: space-between;1085  gap: 10px;1086}1087 1088.incident-title,1089.hypothesis-title,1090.action-title {1091  margin-top: 10px;1092  color: var(--text);1093  font-weight: 700;1094  font-size: 15px;1095  line-height: 1.35;1096}1097 1098.incident-meta,1099.incident-description,1100.info-body,1101.summary-block p,1102.hypothesis-card p,1103.next-check,1104.action-text,1105.profile-summary p,1106.empty-state {1107  color: var(--text-soft);1108  font-size: 13px;1109  line-height: 1.55;1110}1111 1112.incident-meta,1113.incident-description {1114  margin-top: 7px;1115  color: var(--text-faint);1116  font-size: 12px;1117}1118 1119.severity-pill,1120.status-pill,1121.confidence-pill {1122  display: inline-flex;1123  align-items: center;1124  justify-content: center;1125  border-radius: 999px;1126  padding: 4px 9px;1127  font-size: 11px;1128  font-weight: 700;1129}1130 1131.severity-pill {1132  color: white;1133}1134 1135.status-pill,1136.confidence-pill {1137  background: #ffffff;1138  border: 1px solid var(--border-strong);1139}1140 1141.workspace-card,1142.profile-card {1143  border-radius: 16px;1144  padding: 18px;1145}1146 1147.workspace-card h3,1148.profile-summary h3 {1149  margin: 0;1150  font-size: 16px;1151  color: var(--text);1152}1153 1154.info-grid,1155.stat-grid,1156.dependency-grid,1157.hypothesis-grid,1158.workspace-two-up {1159  display: grid;1160  gap: 14px;1161}1162 1163.info-grid {1164  grid-template-columns: repeat(2, minmax(0, 1fr));1165  margin: 18px 0;1166}1167 1168.stat-grid {1169  grid-template-columns: repeat(4, minmax(0, 1fr));1170  margin-top: 18px;1171}1172 1173.dependency-grid,1174.hypothesis-grid {1175  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));1176}1177 1178.info-card,1179.stat-card,1180.dependency-node,1181.hypothesis-card,1182.action-item,1183.profile-row {1184  background: var(--panel-soft);1185  border: 1px solid var(--border);1186  border-radius: 14px;1187  padding: 14px;1188}1189 1190.stat-value {1191  margin-top: 8px;1192  font-size: 21px;1193  font-weight: 700;1194  color: var(--text);1195}1196 1197.summary-block {1198  margin-top: 2px;1199}1200 

Showing the first 1,200 of 1668 lines. Download the file for the rest.