#!/usr/bin/env python3 """ DCI-Agent Pi Search – Prestige Purple edition Gradio 6.x · dark-first · follows prestige-purple-DESIGN.md """ from __future__ import annotations import html as _html import json import os import re import shutil import tempfile import time import zipfile from datetime import datetime from pathlib import Path from typing import Any, Dict, Generator, List, Optional import gradio as gr try: from charset_normalizer import from_bytes except ImportError: from_bytes = None try: from pypdf import PdfReader except ImportError: PdfReader = None from pi_wrapper import run_pi_stream # ── Paths ───────────────────────────────────────────────────────────────── APP_DIR = Path(__file__).resolve().parent DEFAULT_CORPUS_DIR = APP_DIR / "default_corpus" DEFAULT_CORPUS_LABEL = "Default (bright_corpus)" UPLOAD_FILE_LABEL = "Upload your own corpus(single file)" UPLOAD_FOLDER_LABEL = "Upload your own corpus(folder)" HERO_LOGO_PATH = APP_DIR / "img" / "logo.png" TEXT_FILE_SUFFIXES = { ".txt", ".md", ".csv", ".tsv", ".json", ".jsonl", ".xml", ".html", ".htm", ".yaml", ".yml", ".log", ".srt", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".c", ".cc", ".cpp", ".h", ".hpp", ".sql", ".rst", } # ────────────────────────────────────────────────────────────────────────── # Minimal Slate Gradio theme # Sets Gradio's own CSS variables so every component inherits dark colors. # ────────────────────────────────────────────────────────────────────────── PP_THEME = gr.themes.Base( primary_hue=gr.themes.colors.slate, secondary_hue=gr.themes.colors.slate, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("Plus Jakarta Sans"), "ui-sans-serif", "sans-serif"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], ).set( # ── Light mode backgrounds ───────────────────────────────────────── body_background_fill="#F5F7FA", background_fill_primary="#FFFFFF", background_fill_secondary="#F0F4F8", block_background_fill="#FFFFFF", block_border_color="#E5EBF0", block_border_width="1px", block_label_background_fill="#FFFFFF", block_label_text_color="#64748B", block_label_text_size="*text_xs", block_title_text_color="#64748B", block_title_background_fill="transparent", block_shadow="0 2px 8px rgba(15,23,42,0.06)", input_background_fill="#FFFFFF", input_border_color="#CBD5E1", input_border_color_focus="#475569", input_placeholder_color="#94A3B8", input_shadow="none", input_shadow_focus="0 0 0 3px rgba(71,84,105,0.1)", body_text_color="#0F172A", body_text_color_subdued="#64748B", button_primary_background_fill="linear-gradient(135deg,#334155 0%,#475569 100%)", button_primary_background_fill_hover="linear-gradient(135deg,#1E293B 0%,#334155 100%)", button_primary_text_color="#FFFFFF", button_primary_border_color="transparent", button_secondary_background_fill="transparent", button_secondary_background_fill_hover="rgba(71,84,105,0.06)", button_secondary_text_color="#334155", button_secondary_border_color="#475569", checkbox_background_color="#FFFFFF", checkbox_background_color_selected="#334155", checkbox_border_color="#CBD5E1", checkbox_border_color_focus="#475569", checkbox_label_background_fill="#F0F4F8", checkbox_label_background_fill_selected="#334155", checkbox_label_text_color="#1E293B", checkbox_label_text_color_selected="#FFFFFF", slider_color="#475569", border_color_primary="#E5EBF0", border_color_accent="#475569", color_accent="#334155", color_accent_soft="rgba(51,65,85,0.08)", panel_background_fill="#F5F3FF", panel_border_color="#DDD6FE", error_background_fill="#FFF1F2", error_border_color="#F87171", error_text_color="#DC2626", button_large_padding="10px 24px", button_large_radius="10px", form_gap_width="8px", layout_gap="12px", # ── Dark mode overrides ──────────────────────────────────────────── body_background_fill_dark="#0F172A", background_fill_primary_dark="#1E293B", background_fill_secondary_dark="#334155", block_background_fill_dark="#1E293B", block_border_color_dark="#475569", block_label_background_fill_dark="#1E293B", block_label_text_color_dark="#CBD5E1", block_title_text_color_dark="#CBD5E1", block_title_background_fill_dark="transparent", block_shadow_dark="0 2px 12px rgba(0,0,0,0.45)", input_background_fill_dark="#1E293B", input_border_color_dark="#475569", input_border_color_focus_dark="#E2E8F0", input_placeholder_color_dark="#94A3B8", input_shadow_focus_dark="0 0 0 3px rgba(226,232,240,0.1)", body_text_color_dark="#E2E8F0", body_text_color_subdued_dark="#CBD5E1", button_primary_background_fill_dark="linear-gradient(135deg,#334155 0%,#475569 100%)", button_primary_background_fill_hover_dark="linear-gradient(135deg,#1E293B 0%,#334155 100%)", button_primary_text_color_dark="#FFFFFF", button_primary_border_color_dark="transparent", button_secondary_background_fill_dark="transparent", button_secondary_background_fill_hover_dark="rgba(226,232,240,0.08)", button_secondary_text_color_dark="#E2E8F0", button_secondary_border_color_dark="#E2E8F0", checkbox_background_color_dark="#1E293B", checkbox_background_color_selected_dark="#334155", checkbox_border_color_dark="#475569", checkbox_border_color_focus_dark="#E2E8F0", checkbox_label_background_fill_dark="#334155", checkbox_label_background_fill_selected_dark="#334155", checkbox_label_text_color_dark="#CBD5E1", checkbox_label_text_color_selected_dark="#FFFFFF", slider_color_dark="#E2E8F0", border_color_primary_dark="#475569", border_color_accent_dark="#E2E8F0", color_accent_soft_dark="rgba(226,232,240,0.12)", panel_background_fill_dark="#0F172A", panel_border_color_dark="#475569", error_background_fill_dark="#160808", error_border_color_dark="#EF4444", error_text_color_dark="#FC8181", ) # ────────────────────────────────────────────────────────────────────────── # Supplementary CSS (only for things the theme system can't set) # ────────────────────────────────────────────────────────────────────────── CUSTOM_CSS = """ /* ── Hero ─────────────────────────────────────────────────────────── */ .hero-wrap { padding: 8px 0 12px; margin-bottom: 8px; } html.dark .hero-wrap { border-bottom: 1px solid #475569; } html:not(.dark) .hero-wrap { border-bottom: 1px solid #E5EBF0; } .hero-title-row { display: flex; align-items: center; gap: 14px; margin: 0 0 12px !important; } .hero-logo { width: 72.5px; height: 72.5px; object-fit: contain; flex: 0 0 72.5px; } .hero-title { font-size: 35px !important; font-weight: 800 !important; letter-spacing: -0.03em !important; line-height: 1.1 !important; background: linear-gradient(128deg, #334155 0%, #1E293B 100%) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; background-clip: text !important; margin: 0 !important; padding-bottom: 4px !important; display: block; } .hero-subtitle { font-size: 11px !important; font-weight: 600 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; color: #64748B !important; margin: 2px 0 12px !important; display: block; } .hero-links { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } .hero-links a { display: inline-flex; align-items: center; gap: 5px; padding: 3px 11px 4px; border-radius: 20px; font-size: 11px !important; font-weight: 600 !important; letter-spacing: 0.04em !important; text-decoration: none !important; transition: background 0.15s, border-color 0.15s, color 0.15s; } html.dark .hero-links a { background: rgba(180,175,242,0.07); border: 1px solid #2A2A3C; color: #8B8BA8 !important; } html:not(.dark) .hero-links a { background: #F0EEFF; border: 1px solid #DDD6FE; color: #7C6FA8 !important; } html.dark .hero-links a:hover { background: rgba(124,92,252,0.18); border-color: #7C5CFC; color: #C4BFFF !important; } html:not(.dark) .hero-links a:hover { background: #EDE9FE; border-color: #7C5CFC; color: #5822B4 !important; } .hero-links .sep { color: #2A2A3C; font-size: 11px; user-select: none; } /* ── Card groups (sidebar sections) ──────────────────────────────────── */ .pp-card { border-radius: 12px !important; padding: 10px 12px !important; } html.dark .pp-card { background: #1E293B !important; border: 1px solid #475569 !important; box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; } html:not(.dark) .pp-card { background: #FFFFFF !important; border: 1px solid #E5EBF0 !important; box-shadow: 0 2px 8px rgba(51,65,85,0.07) !important; } /* Tighten Gradio's own block padding inside cards */ .pp-card .block, .pp-card .wrap { padding-top: 2px !important; padding-bottom: 2px !important; } .pp-card .gap { gap: 6px !important; } .main-panel { margin-top: 60px; } .sidebar-panel .hero-subtitle { font-size: 13px !important; } .sidebar-panel .hero-links a, .sidebar-panel .hero-links .sep { font-size: 13px !important; } .sidebar-panel label, .sidebar-panel .block > label, .sidebar-panel .wrap > label, .sidebar-panel span.svelte-1gfkn6j, .sidebar-panel .gr-label { font-size: 12.5px !important; font-weight: 800 !important; } .sidebar-panel input, .sidebar-panel textarea, .sidebar-panel select, .sidebar-panel [data-testid="radio"] span, .sidebar-panel [data-testid="checkbox-group"] span, .sidebar-panel .status-box textarea, .sidebar-panel [data-testid="file"] { font-size: 13.5px !important; } .sidebar-panel [data-testid="radio"] label, .sidebar-panel [data-testid="radio"] span { font-weight: 700 !important; } html.dark .sidebar-panel [data-testid="radio"] span { color: #111111 !important; } html.dark .sidebar-panel [data-testid="radio"] input:checked + span { color: #FFFFFF !important; } html:not(.dark) .sidebar-panel [data-testid="radio"] span { color: #111111 !important; } html:not(.dark) .sidebar-panel [data-testid="radio"] input:checked + span { color: #FFFFFF !important; } .card-hdr { display: flex; align-items: center; gap: 7px; font-size: 10px !important; font-weight: 800 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; margin-bottom: 8px !important; padding-bottom: 7px !important; } html.dark .card-hdr { color: #94A3B8 !important; border-bottom: 1px solid #475569 !important; } html:not(.dark) .card-hdr { color: #64748B !important; border-bottom: 1px solid #E5EBF0 !important; } html.dark .card-hdr svg { width:13px; height:13px; stroke:#94A3B8; fill:none; } html:not(.dark) .card-hdr svg { width:13px; height:13px; stroke:#64748B; fill:none; } /* ── Labels (override Gradio uppercase labels to match design) ────────── */ label, .block > label, .wrap > label, span.svelte-1gfkn6j, .gr-label { font-size: 10.5px !important; font-weight: 700 !important; text-transform: uppercase !important; letter-spacing: 0.07em !important; color: #8B8BA8 !important; } /* ── Slider accent ───────────────────────────────────────────────────── */ input[type=range] { accent-color: #475569 !important; } input[type=range]::-webkit-slider-thumb { background: #475569 !important; } input[type=range]::-moz-range-thumb { background: #475569 !important; } /* ── File upload (Gradio 6 targets) ─────────────────────────────────── */ .upload-container, [data-testid="file"], [data-testid="upload"], div[class*="fileupload"], div[class*="file-upload"], .file-preview, .upload-btn-wrapper { background: #141420 !important; border: 2px dashed #2A2A3C !important; border-radius: 12px !important; color: #6B6B8A !important; transition: border-color 0.2s, background 0.2s !important; } [data-testid="file"]:hover, [data-testid="upload"]:hover, div[class*="fileupload"]:hover, div[class*="file-upload"]:hover { border-color: #7C5CFC !important; background: rgba(124,92,252,0.05) !important; } /* upload icon color */ [data-testid="file"] svg, [data-testid="upload"] svg { stroke: #6B6B8A !important; } /* ── Terminal composer ───────────────────────────────────────────────── */ #terminal-shell { display: flex !important; flex-direction: column !important; gap: 8px !important; } #terminal-input-wrap { order: 1; } #terminal-actions-wrap { order: 2; } #terminal-examples-wrap { order: 3; } #terminal-log-wrap { order: 4; } #terminal-examples-wrap { width: 100% !important; min-width: 0 !important; overflow: visible !important; } #terminal-shell.has-run #terminal-log-wrap { order: 1; } #terminal-shell.has-run #terminal-input-wrap { order: 2; } #terminal-shell.has-run #terminal-actions-wrap { order: 3; } #terminal-shell.has-run #terminal-examples-wrap { order: 4; } #terminal-shell.log-hidden #terminal-log-wrap { display: none !important; } #terminal-shell.log-collapsed .terminal-wrapper { height: 74px !important; overflow: hidden !important; } #terminal-shell.log-collapsed .terminal-body { padding-top: 8px; padding-bottom: 8px; } #terminal-shell.log-floating #terminal-log-wrap { position: fixed !important; top: 18px; right: 24px; width: min(980px, calc(100vw - 48px)); height: min(560px, calc(100vh - 36px)); min-width: 520px; min-height: 280px; max-width: calc(100vw - 32px); max-height: calc(100vh - 24px); overflow: auto !important; resize: both; z-index: 9999; } #terminal-shell.log-floating .terminal-wrapper { height: 100%; box-shadow: 0 18px 60px rgba(0,0,0,0.38); } #terminal-question-input textarea { min-height: 90px !important; max-height: 180px !important; font-size: 16px !important; line-height: 1.75 !important; padding: 14px 16px !important; border-radius: 10px !important; resize: vertical !important; font-family: 'Plus Jakarta Sans', sans-serif !important; } html.dark #terminal-question-input textarea { color: #E6E6F0 !important; background: #0D1117 !important; border: 1px solid #1E2030 !important; } html:not(.dark) #terminal-question-input textarea { color: #2D1A5E !important; background: #FFFFFF !important; border: 1px solid #DDD6FE !important; } html.dark #terminal-question-input textarea:focus { border-color: #B4AFF2 !important; box-shadow: 0 0 0 3px rgba(180,175,242,0.14) !important; } html:not(.dark) #terminal-question-input textarea:focus { border-color: #7C5CFC !important; box-shadow: 0 0 0 3px rgba(124,92,252,0.15) !important; } /* ── Run / Clear / Stop buttons ──────────────────────────────────────── */ .btn-run { height: 46px !important; font-size: 14px !important; font-weight: 700 !important; letter-spacing: 0.02em !important; } .btn-clear { height: 46px !important; font-weight: 600 !important; background: rgba(124,92,252,0.12) !important; color: #6C56D9 !important; border: 1px solid rgba(180,175,242,0.28) !important; border-radius: 10px !important; } .btn-clear:hover { background: rgba(124,92,252,0.22) !important; border-color: rgba(180,175,242,0.5) !important; } .btn-stop { height: 46px !important; font-weight: 700 !important; background: rgba(239,68,68,0.13) !important; color: #F87171 !important; border: 1px solid rgba(239,68,68,0.35) !important; border-radius: 10px !important; } .btn-stop:hover { background: rgba(239,68,68,0.22) !important; border-color: rgba(239,68,68,0.55) !important; } /* ── Section labels above answer / log ──────────────────────────────── */ .sec-label { font-size: 14px !important; font-weight: 700 !important; letter-spacing: 0.04em !important; text-transform: uppercase !important; margin: 18px 0 8px !important; display: block; } html.dark .sec-label { color: #B4AFF2 !important; } html:not(.dark) .sec-label { color: #5822B4 !important; } /* ── Execution log terminal ──────────────────────────────────────────── */ .terminal-wrapper { border-radius: 12px; padding: 0; height: 525px; overflow-y: auto; font-family: 'Plus Jakarta Sans', sans-serif; font-size: 13.5px; line-height: 1.75; scroll-behavior: smooth; } .terminal-wrapper { background:#050505; border:1px solid #1A1A1A; } .terminal-chrome { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 14px; border-bottom: 1px solid transparent; backdrop-filter: blur(10px); cursor: default; } #terminal-shell.log-floating .terminal-chrome { cursor: move; } .terminal-controls { display: inline-flex; align-items: center; gap: 7px; line-height: 0; } .terminal-chrome { background: rgba(12,12,12,0.96); border-bottom-color: #1F1F1F; } .term-dot { width: 11px; height: 11px; border-radius: 999px; display: block; flex: 0 0 11px; border: 0; padding: 0; margin: 0; cursor: pointer; appearance: none; vertical-align: middle; } .term-dot.red { background: #FF5F57; } .term-dot.yellow { background: #FEBC2E; } .term-dot.green { background: #28C840; } .term-dot:hover { transform: scale(1.08); } .terminal-body { padding: 18px 18px 22px; } .term-stack { display: flex; flex-direction: column; gap: 12px; } .term-block { padding: 12px 14px; border-radius: 12px; border: 1px solid transparent; } .term-block { background: rgba(16,16,16,0.92); border-color: #202020; } .term-block.term-question, .term-block.term-answer { background: transparent !important; border: 0 !important; border-top: 2px solid #303030 !important; border-radius: 0 !important; padding-left: 12px !important; padding-right: 0 !important; } .term-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } .term-icon { width: 22px; height: 22px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 800; flex: 0 0 22px; } .term-block.is-active .term-icon { animation: termPulse 0.9s ease-in-out infinite; } @keyframes termPulse { 0%, 100% { opacity: 0.5; transform: scale(0.96); } 50% { opacity: 1; transform: scale(1.08); box-shadow: 0 0 0 6px rgba(124,92,252,0.10); } } .term-label { font-size: 11px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; } .term-title { font-size: 14px; font-weight: 700; } .term-meta { margin-left: auto; font-size: 11px; } .term-body { word-break: break-word; overflow-wrap: anywhere; font-size: 14px; line-height: 1.75; } .term-footer { display: flex; justify-content: flex-end; margin-top: 8px; font-size: 10px; } .term-question .term-body, .term-answer .term-body { font-size: 15px; } .term-think .term-body, .term-result .term-body { font-size: 14px; line-height: 1.68; } .term-think .term-title, .term-tool .term-title, .term-result .term-title { font-size: 15px; } .term-tool .term-body { font-size: 15px; line-height: 1.7; } .term-body > *:first-child { margin-top: 0; } .term-body > *:last-child { margin-bottom: 0; } .term-body p, .term-body li, .term-body blockquote { margin: 0 0 10px; } .term-body ul, .term-body ol { margin: 0 0 10px 20px; padding: 0; } .term-body pre { margin: 8px 0 10px; padding: 12px 14px; border-radius: 10px; overflow-x: auto; } .term-body code { padding: 2px 6px; border-radius: 6px; font-size: 0.93em; } .term-body pre code { padding: 0; background: transparent !important; } .term-body blockquote { padding-left: 12px; border-left: 2px solid #7C5CFC; } .term-body a { color: #7C5CFC; text-decoration: none; } .term-body a:hover { text-decoration: underline; } .term-body pre { background: #0A0A0A; border: 1px solid #232323; } .term-body code { background: rgba(255,255,255,0.08); color: #F5F5F5; } .term-divider { height: 1px; background: linear-gradient(90deg, rgba(255,255,255,0.32), rgba(255,255,255,0.06)); margin: 2px 0 6px; } .term-muted { color: #9A9A9A; } .term-text { color: #F5F5F5; } .term-think .term-body, .term-think .term-body.term-text, .term-think .term-body *, .term-think .term-body code { color: #CFCFCF !important; } .term-tool .term-body, .term-tool .term-body.term-text, .term-tool .term-body *, .term-tool .term-body code { color: #8EC5FF !important; } .term-result .term-body, .term-result .term-body.term-text, .term-result .term-body *, .term-result .term-body code { color: #F5E6A8 !important; } .term-answer .term-body, .term-answer .term-body.term-text, .term-answer .term-body *, .term-answer .term-body code, .term-assistant .term-body, .term-assistant .term-body.term-text, .term-assistant .term-body *, .term-assistant .term-body code { color: #FFFFFF !important; } .term-info .term-icon, .term-status .term-icon { background: rgba(255,255,255,.08); color: #F5F5F5; } .term-think .term-icon { background: rgba(255,255,255,.08); color: #EAEAEA; } .term-tool .term-icon { background: rgba(255,255,255,.08); color: #EAEAEA; } .term-result .term-icon { background: rgba(255,255,255,.08); color: #EAEAEA; } .term-question .term-icon { background: rgba(124,92,252,.18); color: #F5F3FF; } .term-answer .term-icon { background: rgba(255,255,255,.08); color: #FFFFFF; } .term-error .term-icon { background: rgba(255,255,255,.08); color: #FFFFFF; } /* ── Status box ──────────────────────────────────────────────────────── */ .status-box textarea { font-family: 'JetBrains Mono', monospace !important; font-size: 11.5px !important; color: #6B6B8A !important; border-radius: 8px !important; min-height: unset !important; } /* ── Examples 2-column grid ──────────────────────────────────────────── */ .examples-section { margin-top: 28px; padding-top: 20px; border-top: 1px solid #2A2A3C; } .ex-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 10px; padding-top: 4px; width: 100%; box-sizing: border-box; overflow: visible; } @media (max-width: 680px) { .ex-grid { grid-template-columns: 1fr; } } .ex-card { border-radius: 12px; padding: 14px 16px; width: 100%; min-width: 0; box-sizing: border-box; overflow: hidden; cursor: pointer; transition: border-color .18s, background .18s, transform .12s, box-shadow .18s; } html.dark .ex-card { background:#141420; border:1px solid #2A2A3C; } html:not(.dark) .ex-card { background:#FFFFFF; border:1px solid #DDD6FE; } html.dark .ex-card:hover { border-color: #7C5CFC; background: rgba(124,92,252,.06); transform: translateY(-2px); box-shadow: 0 8px 32px rgba(0,0,0,.5); } html:not(.dark) .ex-card:hover { border-color: #7C5CFC; background: #F5F3FF; transform: translateY(-2px); box-shadow: 0 8px 24px rgba(88,34,180,.12); } .ex-card:active { transform: translateY(0); } .ex-ds { display: inline-block; font-size: 9px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; padding: 2px 9px; border-radius: 4px; margin-bottom: 10px; } html.dark .ex-ds { background:rgba(88,34,180,.28); color:#B4AFF2; } html:not(.dark) .ex-ds { background:#EDE9FE; color:#5822B4; } .ex-q { font-size: 15.5px; line-height: 1.55; margin: 0 0 10px; font-family: 'Plus Jakarta Sans', sans-serif; } html.dark .ex-q { color:#DEE1F0; } html:not(.dark) .ex-q { color:#1E1B4B; } .ex-a { font-size: 11.5px; line-height: 1.5; margin: 0; padding-top: 10px; font-family: 'Plus Jakarta Sans', sans-serif; } html.dark .ex-a { color:#6B6B8A; border-top:1px solid #2A2A3C; } html:not(.dark) .ex-a { color:#7C6FA8; border-top:1px solid #EDE9FE; } .ex-a::before { content: "A · "; font-weight: 700; } html.dark .ex-a::before { color:#4E4E72; } html:not(.dark) .ex-a::before { color:#A89CC8; } .ex-g { font-size: 10.5px; line-height: 1.45; margin: 8px 0 0; word-break: break-word; overflow-wrap: anywhere; white-space: normal; font-family: 'JetBrains Mono', monospace; } html.dark .ex-g { color:#8B8BA8; } html:not(.dark) .ex-g { color:#64748B; } /* ── Scrollbar ───────────────────────────────────────────────────────── */ ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-thumb { border-radius: 3px; } html.dark ::-webkit-scrollbar-track { background: #0A0A12; } html.dark ::-webkit-scrollbar-thumb { background: #1E2030; } html.dark ::-webkit-scrollbar-thumb:hover { background: #2A2A3C; } html:not(.dark) ::-webkit-scrollbar-track { background: #F5F3FF; } html:not(.dark) ::-webkit-scrollbar-thumb { background: #DDD6FE; } html:not(.dark) ::-webkit-scrollbar-thumb:hover { background: #C4B5FD; } /* ── Dropdown popup list ─────────────────────────────────────────────── */ html.dark ul[role="listbox"], html.dark [role="option"] { background: #141420 !important; border-color: #2A2A3C !important; color: #E6E6F0 !important; } html.dark [role="option"]:hover { background: #1A1A28 !important; } html:not(.dark) ul[role="listbox"], html:not(.dark) [role="option"] { background: #FFFFFF !important; border-color: #DDD6FE !important; color: #1E1B4B !important; } html:not(.dark) [role="option"]:hover { background: #F5F3FF !important; } /* ── Nuke residual backgrounds ───────────────────────────────────────── */ .tabs, .tabitem, .tab-nav, footer { background: transparent !important; } /* ════════════════════════════════════════════════════════════════════════ DARK MODE Key insight: Gradio wraps every component in a .block div with background #141420 that shows as a "lighter box" against the #0A0A12 page background. Fix = make ALL .block transparent by default; only pp-card siblings keep an explicit card background. ════════════════════════════════════════════════════════════════════════ */ /* CSS variable layer — components that read vars get the right colour */ html.dark { --block-background-fill: transparent; --input-background-fill: #141420; --background-fill-primary: #141420; --background-fill-secondary: #1A1A28; --body-background-fill: #0A0A12; --border-color-primary: #2A2A3C; --block-border-color: transparent; --input-border-color: #2A2A3C; --body-text-color: #E6E6F0; --body-text-color-subdued: #8B8BA8; --input-placeholder-color: #6B6B8A; --shadow-drop: none; --shadow-drop-lg: none; --block-shadow: none; } /* All generic wrappers → transparent (no extra box) */ html.dark .block, html.dark .wrap, html.dark .form, html.dark .gap, html.dark .contain, html.dark .padded, html.dark .compact, html.dark fieldset, html.dark .panel, html.dark .gr-box, html.dark .gr-form, html.dark .gr-group, html.dark .gradio-group, html.dark [data-testid="html"], html.dark [data-testid="html"] > *, html.dark [data-testid="markdown"], html.dark [data-testid="markdown"] > * { background: transparent !important; border-color: transparent !important; box-shadow: none !important; } /* pp-card gets its own visible card surface */ html.dark .pp-card { background: #141420 !important; border: 1px solid #2A2A3C !important; box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; } /* inner wrappers inside a pp-card inherit the card bg */ html.dark .pp-card .block, html.dark .pp-card .wrap, html.dark .pp-card fieldset, html.dark .pp-card .form { background: #141420 !important; border-color: #2A2A3C !important; box-shadow: none !important; } /* Actual input / textarea elements */ html.dark input, html.dark textarea, html.dark select { background: #141420 !important; color: #E6E6F0 !important; border-color: #2A2A3C !important; } html.dark input:focus, html.dark textarea:focus { border-color: #B4AFF2 !important; box-shadow: 0 0 0 3px rgba(180,175,242,0.14) !important; outline: none !important; } /* Radio / checkbox spans */ html.dark .wrap span, html.dark [data-testid="radio"] span, html.dark [data-testid="checkbox-group"] span { color: #E6E6F0 !important; } html.dark [data-testid="radio"] input + span, html.dark [data-testid="checkbox-group"] input + span { background: #141420 !important; border-color: #2A2A3C !important; } /* Labels */ html.dark .block > label > span, html.dark .wrap > label > span, html.dark label > span.text-sm { color: #8B8BA8 !important; } """ # ────────────────────────────────────────────────────────────────────────── # Global JavaScript # ────────────────────────────────────────────────────────────────────────── GLOBAL_JS = """ /* Gradio 6: js param is executed as a plain code string on page load. Do NOT wrap in an arrow function — it won't be called. */ /* fillQuestion: populate the terminal composer from an example card click. Tries multiple selectors for robustness across Gradio 6 DOM layouts. */ window.fillQuestion = function(text) { var el = ( document.querySelector('#terminal-question-input textarea') || document.querySelector('textarea[placeholder*="search question"]') || document.querySelector('textarea[placeholder*="Enter your"]') || document.querySelector('textarea[placeholder*="question here"]') ); if (!el) { console.warn('fillQuestion: textarea not found'); return; } var setter = Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, 'value' ).set; setter.call(el, text); el.dispatchEvent(new Event('input', { bubbles: true })); el.focus(); el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; window.syncTerminalShellState = function() { var shell = document.querySelector('#terminal-shell'); var terminal = document.querySelector('.terminal-wrapper'); var wrap = document.querySelector('#terminal-log-wrap'); if (!shell || !terminal) return; var text = (terminal.textContent || '').trim(); var isIdle = !text || text.indexOf('Waiting for run') !== -1; shell.classList.toggle('has-run', !isIdle); var lastState = shell.dataset.runState || 'idle'; if (!isIdle && lastState !== 'active') { shell.classList.remove('log-hidden', 'log-collapsed', 'log-floating'); if (wrap) { wrap.style.left = ''; wrap.style.top = ''; wrap.style.right = ''; wrap.style.width = ''; wrap.style.height = ''; delete wrap.dataset.floatingInit; } } shell.dataset.runState = isIdle ? 'idle' : 'active'; }; window.resetTerminalLogState = function() { var shell = document.querySelector('#terminal-shell'); var wrap = document.querySelector('#terminal-log-wrap'); if (!shell) return; shell.classList.remove('log-hidden', 'log-collapsed', 'log-floating'); if (wrap) { wrap.style.left = ''; wrap.style.top = ''; wrap.style.right = ''; wrap.style.width = ''; wrap.style.height = ''; delete wrap.dataset.floatingInit; } }; window.terminalLogControl = function(action) { var shell = document.querySelector('#terminal-shell'); var wrap = document.querySelector('#terminal-log-wrap'); if (!shell) return; if (action === 'hide') { var stopBtn = document.querySelector('.btn-stop button') || document.querySelector('.btn-stop'); if (stopBtn && typeof stopBtn.click === 'function') { stopBtn.click(); } shell.classList.remove('log-collapsed', 'log-floating'); shell.classList.add('log-hidden'); if (wrap) { wrap.style.left = ''; wrap.style.top = ''; wrap.style.right = ''; wrap.style.width = ''; wrap.style.height = ''; } return; } shell.classList.remove('log-hidden'); if (action === 'collapse') { if (shell.classList.contains('log-collapsed')) { shell.classList.remove('log-collapsed'); return; } shell.classList.remove('log-floating'); shell.classList.add('log-collapsed'); if (wrap) { wrap.style.left = ''; wrap.style.top = ''; wrap.style.right = ''; wrap.style.width = ''; wrap.style.height = ''; delete wrap.dataset.floatingInit; } return; } if (action === 'float') { if (shell.classList.contains('log-floating')) { shell.classList.remove('log-floating'); if (wrap) { wrap.style.left = ''; wrap.style.top = ''; wrap.style.right = ''; wrap.style.width = ''; wrap.style.height = ''; delete wrap.dataset.floatingInit; } return; } shell.classList.remove('log-collapsed'); shell.classList.add('log-floating'); if (wrap && !wrap.dataset.floatingInit) { wrap.style.right = '24px'; wrap.style.top = '18px'; wrap.dataset.floatingInit = '1'; } } }; window.__terminalDrag = { active: false, offsetX: 0, offsetY: 0, target: null }; document.addEventListener('mousedown', function(e) { var shell = document.querySelector('#terminal-shell'); if (!shell || !shell.classList.contains('log-floating')) return; var chrome = e.target && e.target.closest ? e.target.closest('.terminal-chrome') : null; var wrap = document.querySelector('#terminal-log-wrap'); if (!chrome || !wrap) return; if (e.target && e.target.closest && e.target.closest('.term-dot')) return; var rect = wrap.getBoundingClientRect(); window.__terminalDrag = { active: true, offsetX: e.clientX - rect.left, offsetY: e.clientY - rect.top, target: wrap }; wrap.style.right = 'auto'; wrap.style.left = rect.left + 'px'; wrap.style.top = rect.top + 'px'; wrap.style.width = rect.width + 'px'; wrap.style.height = rect.height + 'px'; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', function(e) { var drag = window.__terminalDrag; if (!drag || !drag.active || !drag.target) return; var maxLeft = Math.max(0, window.innerWidth - drag.target.offsetWidth); var maxTop = Math.max(0, window.innerHeight - drag.target.offsetHeight); var left = Math.min(Math.max(0, e.clientX - drag.offsetX), maxLeft); var top = Math.min(Math.max(0, e.clientY - drag.offsetY), maxTop); drag.target.style.left = left + 'px'; drag.target.style.top = top + 'px'; }); document.addEventListener('mouseup', function() { if (window.__terminalDrag) { window.__terminalDrag.active = false; window.__terminalDrag.target = null; } document.body.style.userSelect = ''; }); document.addEventListener('click', function(e) { var runBtn = e.target && e.target.closest ? e.target.closest('.btn-run') : null; if (runBtn) { window.resetTerminalLogState(); } }); document.addEventListener('keydown', function(e) { var target = e.target; if (!target || !target.matches || !target.matches('#terminal-question-input textarea')) return; if (e.key === 'Enter' && !e.shiftKey) { window.resetTerminalLogState(); } }); /* Auto-scroll terminal whenever new log lines arrive */ new MutationObserver(function() { var t = document.querySelector('.terminal-wrapper'); if (t) t.scrollTop = t.scrollHeight; window.syncTerminalShellState(); }).observe(document.body, { childList: true, subtree: true }); window.syncTerminalShellState(); """ # ────────────────────────────────────────────────────────────────────────── # Example data # ────────────────────────────────────────────────────────────────────────── EXAMPLES_DATA: List[Dict[str, str]] = [ { "dataset": "Animal Science", "corpus": "biology", "question": "According to the documents about animal handedness, what types of animals are mentioned as being kept as pets? Cite the file path where you found this.", "answer": "Parrots, dogs, cats, and rabbits are mentioned as commonly kept pets.", "gold_doc": "biology/animals_handedness_Animal_8_3.txt.txt", }, { "dataset": "Biology", "corpus": "biology", "question": "What bacterium is mentioned as being used as a pesticide in the biology documents? Provide the specific name.", "answer": "Bacillus thuringiensis (Bt) — a Gram-positive, soil-dwelling bacterium.", "gold_doc": "biology/bacterium_infect_another_Bacteria_13_2.txt.txt", }, { "dataset": "Earth Science", "corpus": "earth_science", "question": "According to the earth science documents, what annual rainfall range is given for a tropical savanna?", "answer": "Between 750 millimetres and 1,270 millimetres per year.", "gold_doc": "earth_science/arid_area_Earth_rainfall_climatology4_10.txt.txt", }, { "dataset": "Earth Science", "corpus": "earth_science", "question": "According to the drifting guide in the earth science corpus, which drivetrain is particularly good for drifting?", "answer": "Rear-wheel drive.", "gold_doc": "earth_science/continental_drift_what_is_drifting_guide1_7.txt.txt", }, { "dataset": "Economics", "corpus": "economics", "question": "According to the economics documents, what major event is described as a turning point after which fertility mostly continued to fall?", "answer": "The Great Recession.", "gold_doc": "economics/uspopulationgrowth_thelongtermdeclineinfertilityandwhatitmeansforstatebudgets_76.txt.txt", }, { "dataset": "Robotics", "corpus": "robotics", "question": "In the robotics documents, which message type is converted to Ackermann inputs?", "answer": "geometry_msgs/Twist.", "gold_doc": "robotics/ackermann_interfacecontrolchec_53.txt.txt", }, { "dataset": "Robotics", "corpus": "robotics", "question": "According to the robotics documents, which launch file combines tf-broadcaster and pcl2-spammer data into an octomap?", "answer": "octomap_mapping.launch.", "gold_doc": "robotics/octomap_publish_4NI0GL435o_171.txt.txt", }, ] def _build_examples_html() -> str: cards: List[str] = [] for ex in EXAMPLES_DATA: q_attr = _html.escape(ex["question"], quote=True) q_disp = _html.escape(ex["question"]) a_disp = _html.escape(ex.get("answer", "")) d_disp = _html.escape(ex["dataset"]) gold_doc = ex.get("gold_doc", "").replace(".txt.txt", ".txt") g_disp = _html.escape(gold_doc) cards.append( f'
' f'{d_disp}' f'

{q_disp}

' f'

{a_disp}

' f'

Gold doc: {g_disp}

' f"
" ) return f'
{"".join(cards)}
' # ────────────────────────────────────────────────────────────────────────── # Corpus helpers # ────────────────────────────────────────────────────────────────────────── def _ts() -> str: return datetime.now().strftime("%H:%M:%S") def _example_corpus_map() -> Dict[str, str]: return { str(item["question"]): str(item["corpus"]) for item in EXAMPLES_DATA if item.get("corpus") } def _selected_example_corpus(question: str) -> Optional[str]: return _example_corpus_map().get(question.strip()) def _has_text_default_corpus() -> bool: return DEFAULT_CORPUS_DIR.exists() and any(DEFAULT_CORPUS_DIR.glob("*/*.txt")) def _decode_text_bytes(data: bytes) -> str: if not data: return "" if from_bytes is not None: best = from_bytes(data).best() if best is not None: return str(best) for encoding in ("utf-8", "utf-8-sig", "utf-16", "utf-16-le", "utf-16-be", "gb18030", "big5", "shift_jis"): try: return data.decode(encoding) except UnicodeDecodeError: continue return data.decode("utf-8", errors="replace") def _extract_pdf_text(pdf_path: Path) -> str: if PdfReader is None: raise RuntimeError("PDF parsing support is unavailable because pypdf is not installed.") reader = PdfReader(str(pdf_path)) pages: List[str] = [] for idx, page in enumerate(reader.pages, start=1): text = page.extract_text() or "" cleaned = text.strip() if cleaned: pages.append(f"[Page {idx}]\n{cleaned}") return "\n\n".join(pages).strip() def _normalize_corpus_files(corpus_root: Path) -> None: for file_path in corpus_root.rglob("*"): if not file_path.is_file(): continue suffix = file_path.suffix.lower() if suffix == ".pdf": try: extracted = _extract_pdf_text(file_path) except Exception: continue if extracted: output_path = file_path.with_name(file_path.name + ".txt") output_path.write_text(extracted, encoding="utf-8") continue if suffix not in TEXT_FILE_SUFFIXES: continue try: raw = file_path.read_bytes() decoded = _decode_text_bytes(raw) file_path.write_text(decoded, encoding="utf-8") except Exception: continue def _resolve_example_corpus(corpus_dir: Path, question: str) -> Path: domain = _selected_example_corpus(question) if not domain: return corpus_dir candidate = corpus_dir / domain return candidate if candidate.exists() else corpus_dir def _cleanup_runtime_state(runtime_state: Optional[Dict[str, Any]]) -> Dict[str, Any]: state = dict(runtime_state or {}) corpus_root = state.get("corpus_root") session_dir = state.get("session_dir") for path_value in (corpus_root, session_dir): if not path_value: continue try: path = Path(path_value) if path.exists(): shutil.rmtree(path, ignore_errors=True) except Exception: pass return {} def _uploaded_corpus_signature(uploaded_value: Any) -> str: if uploaded_value is None: return "" if isinstance(uploaded_value, (list, tuple)): items = [str(item) for item in uploaded_value if item] return json.dumps(sorted(items), ensure_ascii=False) return str(uploaded_value) def _select_uploaded_corpus(corpus_source: str, single_file: Any, folder_files: Any) -> Any: if corpus_source == UPLOAD_FOLDER_LABEL and folder_files not in (None, "", []): return folder_files if corpus_source == UPLOAD_FILE_LABEL and single_file not in (None, "", []): return single_file return None def _copy_uploaded_corpus_files(work_dir: Path, uploaded_files: List[str]) -> None: files = [Path(path) for path in uploaded_files if path and os.path.exists(path)] if not files: raise ValueError("No corpus files were uploaded.") total = sum(path.stat().st_size for path in files) if total > 25 * 1024 * 1024: raise ValueError(f"Uploaded files total {total/1024/1024:.1f} MB — max 25 MB.") common_root: Optional[Path] = None try: common_root = Path(os.path.commonpath([str(path.parent) for path in files])) except Exception: common_root = None target_root = work_dir / "corpus" target_root.mkdir(parents=True, exist_ok=True) for file_path in files: size = file_path.stat().st_size if size > 5 * 1024 * 1024: raise ValueError(f"'{file_path.name}' {size/1024/1024:.1f} MB — max 5 MB/file.") rel_path: Path if common_root is not None: try: rel_path = file_path.relative_to(common_root) except ValueError: rel_path = Path(file_path.name) else: rel_path = Path(file_path.name) destination = target_root / rel_path destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(file_path, destination) def _ensure_corpus_ready( use_default: bool, uploaded_corpus: Any, selected_domain: Optional[str] = None, ) -> Path: work_dir = Path(tempfile.mkdtemp(prefix="pi_corpus_")) if use_default: if _has_text_default_corpus(): text_root = DEFAULT_CORPUS_DIR source_root = text_root / selected_domain if selected_domain and (text_root / selected_domain).exists() else text_root shutil.copytree(source_root, work_dir / "corpus", dirs_exist_ok=True) return work_dir / "corpus" raise ValueError("Default bright corpus is unavailable.") if isinstance(uploaded_corpus, (list, tuple)): _copy_uploaded_corpus_files(work_dir, [str(item) for item in uploaded_corpus if item]) elif uploaded_corpus and os.path.exists(str(uploaded_corpus)): uploaded_path = str(uploaded_corpus) if uploaded_path.lower().endswith(".zip"): size = os.path.getsize(uploaded_path) if size > 25 * 1024 * 1024: raise ValueError(f"ZIP is {size/1024/1024:.1f} MB — max 25 MB.") with zipfile.ZipFile(uploaded_path) as zf: total = sum(i.file_size for i in zf.infolist()) if total > 25 * 1024 * 1024: raise ValueError(f"Uncompressed {total/1024/1024:.1f} MB — max 25 MB.") for info in zf.infolist(): if info.file_size > 5 * 1024 * 1024 and not info.is_dir(): raise ValueError(f"'{info.filename}' {info.file_size/1024/1024:.1f} MB — max 5 MB/file.") zf.extractall(work_dir / "corpus") else: _copy_uploaded_corpus_files(work_dir, [uploaded_path]) else: raise ValueError("No corpus available. Upload a folder or enable 'Use Default Corpus'.") _normalize_corpus_files(work_dir / "corpus") return work_dir / "corpus" # ────────────────────────────────────────────────────────────────────────── # Terminal HTML rendering # ────────────────────────────────────────────────────────────────────────── def _esc(v: Any) -> str: return _html.escape(str(v)) def _markdown_to_html(text: str) -> str: safe_text = str(text or "") try: import markdown # type: ignore return markdown.markdown( safe_text, extensions=["fenced_code", "tables", "nl2br", "sane_lists"], ) except Exception: try: import markdown2 # type: ignore return markdown2.markdown( safe_text, extras=["fenced-code-blocks", "tables", "break-on-newline"], ) except Exception: return _simple_markdown_to_html(safe_text) def _inline_markdown(text: str) -> str: escaped = _esc(text) escaped = re.sub(r"`([^`]+)`", r"\1", escaped) escaped = re.sub(r"\*\*([^*]+)\*\*", r"\1", escaped) escaped = re.sub(r"\*([^*]+)\*", r"\1", escaped) return escaped def _simple_markdown_to_html(text: str) -> str: lines = text.splitlines() html_parts: List[str] = [] in_list = False in_code = False code_lines: List[str] = [] paragraph: List[str] = [] def flush_paragraph() -> None: nonlocal paragraph if paragraph: html_parts.append(f"

{'
'.join(_inline_markdown(line) for line in paragraph)}

") paragraph = [] def flush_list() -> None: nonlocal in_list if in_list: html_parts.append("") in_list = False def flush_code() -> None: nonlocal in_code, code_lines if in_code: html_parts.append(f"
{_esc(chr(10).join(code_lines))}
") code_lines = [] in_code = False for line in lines: stripped = line.rstrip() if stripped.startswith("```"): flush_paragraph() flush_list() if in_code: flush_code() else: in_code = True continue if in_code: code_lines.append(line) continue if not stripped: flush_paragraph() flush_list() continue heading_match = re.match(r"^(#{1,6})\s+(.*)$", stripped) if heading_match: flush_paragraph() flush_list() level = len(heading_match.group(1)) html_parts.append(f"{_inline_markdown(heading_match.group(2))}") continue list_match = re.match(r"^[-*]\s+(.*)$", stripped) if list_match: flush_paragraph() if not in_list: html_parts.append("