demo / app.py
IPF's picture
Upload app.py
b6e09dc verified
#!/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'<div class="ex-card" data-q="{q_attr}" onclick="fillQuestion(this.dataset.q)">'
f'<span class="ex-ds">{d_disp}</span>'
f'<p class="ex-q">{q_disp}</p>'
f'<p class="ex-a">{a_disp}</p>'
f'<p class="ex-g">Gold doc: {g_disp}</p>'
f"</div>"
)
return f'<div class="ex-grid">{"".join(cards)}</div>'
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 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"<code>\1</code>", escaped)
escaped = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", escaped)
escaped = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", 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"<p>{'<br>'.join(_inline_markdown(line) for line in paragraph)}</p>")
paragraph = []
def flush_list() -> None:
nonlocal in_list
if in_list:
html_parts.append("</ul>")
in_list = False
def flush_code() -> None:
nonlocal in_code, code_lines
if in_code:
html_parts.append(f"<pre><code>{_esc(chr(10).join(code_lines))}</code></pre>")
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"<h{level}>{_inline_markdown(heading_match.group(2))}</h{level}>")
continue
list_match = re.match(r"^[-*]\s+(.*)$", stripped)
if list_match:
flush_paragraph()
if not in_list:
html_parts.append("<ul>")
in_list = True
html_parts.append(f"<li>{_inline_markdown(list_match.group(1))}</li>")
continue
flush_list()
paragraph.append(stripped)
flush_paragraph()
flush_list()
flush_code()
return "".join(html_parts)
def _make_block(
kind: str,
*,
label: str,
title: str = "",
body: str = "",
meta: str = "",
footer: str = "",
icon: str = "",
active: bool = False,
) -> Dict[str, Any]:
return {
"kind": kind,
"label": label,
"title": title,
"body": body,
"meta": meta,
"footer": footer,
"icon": icon,
"active": active,
}
def _append_block(state: Dict[str, Any], block: Dict[str, Any]) -> int:
state["blocks"].append(block)
return len(state["blocks"]) - 1
def _insert_block(state: Dict[str, Any], idx: int, block: Dict[str, Any]) -> int:
state["blocks"].insert(idx, block)
for key, value in list(state.items()):
if key in {"blocks"}:
continue
if isinstance(value, int) and value >= idx:
state[key] = value + 1
return idx
def _close_current(state: Dict[str, Any], key: str) -> None:
idx = state.get(key)
if idx is None:
return
if 0 <= idx < len(state["blocks"]):
state["blocks"][idx]["active"] = False
state[key] = None
def _ensure_block(
state: Dict[str, Any],
key: str,
*,
kind: str,
label: str,
title: str,
icon: str,
) -> Dict[str, Any]:
idx = state.get(key)
if idx is None:
idx = _append_block(state, _make_block(kind, label=label, title=title, icon=icon, active=True))
state[key] = idx
block = state["blocks"][idx]
block["kind"] = kind
block["label"] = label
block["title"] = title
block["icon"] = icon
block["active"] = True
return block
def _tool_message(tool_name: str, args: Any = None, raw_arguments: str = "") -> str:
parsed = args
if not parsed and raw_arguments:
try:
parsed = json.loads(raw_arguments)
except Exception:
parsed = None
if tool_name == "bash":
if isinstance(parsed, dict) and parsed.get("command"):
return str(parsed["command"])
return raw_arguments.strip() or "bash"
if tool_name == "read":
if isinstance(parsed, dict):
file_path = parsed.get("file_path") or parsed.get("path")
offset = parsed.get("offset")
limit = parsed.get("limit")
parts: List[str] = ["read"]
if file_path:
parts.append(str(file_path))
if offset not in (None, ""):
parts.append(f"--offset {offset}")
if limit not in (None, ""):
parts.append(f"--limit {limit}")
if len(parts) > 1:
return " ".join(parts)
return raw_arguments.strip() or "read"
if isinstance(parsed, dict) and parsed:
return "\n".join(f"{k}: {v}" for k, v in parsed.items())
return raw_arguments.strip() or tool_name
def _tool_result_text(value: Any) -> str:
if value is None:
return ""
if isinstance(value, str):
stripped = value.strip()
if stripped.startswith("{") or stripped.startswith("["):
try:
parsed = json.loads(stripped)
except Exception:
return value
extracted = _tool_result_text(parsed)
return extracted if extracted else value
return value
if isinstance(value, list):
parts = [_tool_result_text(item) for item in value]
return "\n".join(part for part in parts if part).strip()
if isinstance(value, dict):
content = value.get("content")
if isinstance(content, list):
parts: List[str] = []
for item in content:
if isinstance(item, dict):
if isinstance(item.get("text"), str):
parts.append(item["text"])
elif item.get("type") == "text" and isinstance(item.get("content"), str):
parts.append(item["content"])
else:
nested = _tool_result_text(item)
if nested:
parts.append(nested)
else:
nested = _tool_result_text(item)
if nested:
parts.append(nested)
joined = "\n".join(part for part in parts if part).strip()
if joined:
return joined
for key in ("text", "stdout", "stderr", "output", "result", "message", "content"):
extracted = _tool_result_text(value.get(key))
if extracted:
return extracted
fragments: List[str] = []
for nested in value.values():
extracted = _tool_result_text(nested)
if extracted:
fragments.append(extracted)
return "\n".join(fragments).strip()
return str(value)
def _extract_thinking_text(event: Dict[str, Any]) -> str:
direct = str(event.get("thinking") or "").strip()
if direct:
return direct
message = event.get("message") or {}
content = message.get("content") if isinstance(message, dict) else None
if isinstance(content, list):
parts: List[str] = []
for item in content:
if not isinstance(item, dict):
continue
if item.get("type") == "thinking" and item.get("thinking"):
parts.append(str(item.get("thinking")))
return "\n\n".join(part for part in parts if part).strip()
return ""
def _render_block(block: Dict[str, Any]) -> str:
classes = f'term-block term-{block["kind"]}'
if block.get("active"):
classes += " is-active"
head = (
f'<div class="term-head">'
f'<span class="term-icon">{_esc(block.get("icon") or block["label"][:1])}</span>'
f'<span class="term-label term-muted">{_esc(block["label"])}</span>'
f'<span class="term-title term-text">{_esc(block.get("title", ""))}</span>'
f'<span class="term-meta term-muted">{_esc(block.get("meta", ""))}</span>'
f'</div>'
)
body_value = block.get("body", "") or ""
if not body_value:
body = ""
else:
body_html = _markdown_to_html(body_value) if block["kind"] in {"think", "result", "answer", "assistant"} else _esc(body_value).replace("\n", "<br>\n")
body = f'<div class="term-body term-text">{body_html}</div>'
footer_value = block.get("footer", "") or ""
footer = f'<div class="term-footer term-muted">{_esc(footer_value)}</div>' if footer_value else ""
return f'<div class="{classes}">{head}{body}{footer}</div>'
def _render_terminal(blocks: List[Dict[str, Any]]) -> str:
rendered: List[str] = []
for block in blocks:
if block["kind"] == "divider":
rendered.append('<div class="term-divider"></div>')
else:
rendered.append(_render_block(block))
chrome = (
'<div class="terminal-chrome">'
'<div class="terminal-controls">'
'<button type="button" class="term-dot red" onclick="window.terminalLogControl(\'hide\')" aria-label="Hide log"></button>'
'<button type="button" class="term-dot yellow" onclick="window.terminalLogControl(\'collapse\')" aria-label="Collapse log"></button>'
'<button type="button" class="term-dot green" onclick="window.terminalLogControl(\'float\')" aria-label="Float log"></button>'
'</div>'
'</div>'
)
return f'<div class="terminal-wrapper">{chrome}<div class="terminal-body"><div class="term-stack">{"".join(rendered)}</div></div></div>'
def _initial_terminal_state(question: str, cwd: Path, mode: str, model: str, max_turns: int, selected_domain: Optional[str]) -> Dict[str, Any]:
state: Dict[str, Any] = {
"blocks": [],
"current_think": None,
"current_tool": None,
"current_result": None,
"current_assistant": None,
"working_dir_block": None,
"config_block": None,
"pending_tool_name": "",
"pending_tool_args": "",
"thinking_started_at": None,
"thinking_streamed_this_turn": False,
"tool_started_at": {},
"run_started_at": time.perf_counter(),
"finalized": False,
}
state["working_dir_block"] = _append_block(state, _make_block("info", label="Info", title="Working Dir", body=str(cwd), meta=_ts(), icon="i"))
state["config_block"] = _append_block(
state,
_make_block(
"info",
label="Info",
title="Run config",
body=(
"Runner: Native PI\n"
"Thinking level: unknown\n"
"Provider: openai\n"
f"Model: {model}\n"
f"Max turns: {max_turns}\n"
f"Domain: {selected_domain or 'all'}"
),
meta=_ts(),
icon="i",
),
)
_append_block(state, _make_block("question", label="Question", title="User question", body=question.strip(), meta=_ts(), icon="?"))
_append_block(state, _make_block("divider", label="", icon=""))
return state
def _render_terminal_state(state: Dict[str, Any]) -> str:
return _render_terminal(state["blocks"])
def _update_run_config(state: Dict[str, Any], *, runner: str, thinking_level: str, provider: str, model: str, max_turns: int, domain: Optional[str]) -> None:
idx = state.get("config_block")
if idx is None or idx >= len(state["blocks"]):
return
state["blocks"][idx]["body"] = (
f"Runner: {runner}\n"
f"Thinking level: {thinking_level}\n"
f"Provider: {provider}\n"
f"Model: {model}\n"
f"Max turns: {max_turns}\n"
f"Domain: {domain or 'all'}"
)
def _prepare_terminal_state(
terminal_state: Optional[Dict[str, Any]],
*,
question: str,
cwd: Path,
mode: str,
model: str,
max_turns: int,
selected_domain: Optional[str],
) -> Dict[str, Any]:
if not terminal_state:
return _initial_terminal_state(question, cwd, mode, model, max_turns, selected_domain)
state = dict(terminal_state)
state["blocks"] = [dict(block) for block in terminal_state.get("blocks", [])]
for key in ("current_think", "current_tool", "current_result", "current_assistant"):
_close_current(state, key)
state["pending_tool_name"] = ""
state["pending_tool_args"] = ""
state["thinking_started_at"] = None
state["thinking_streamed_this_turn"] = False
state["tool_started_at"] = {}
state["run_started_at"] = time.perf_counter()
state["finalized"] = False
working_idx = state.get("working_dir_block")
if isinstance(working_idx, int) and 0 <= working_idx < len(state["blocks"]):
state["blocks"][working_idx]["body"] = str(cwd)
state["blocks"][working_idx]["meta"] = _ts()
_update_run_config(
state,
runner="Native PI",
thinking_level="unknown",
provider="openai",
model=model,
max_turns=max_turns,
domain=selected_domain,
)
config_idx = state.get("config_block")
if isinstance(config_idx, int) and 0 <= config_idx < len(state["blocks"]):
state["blocks"][config_idx]["meta"] = _ts()
_append_block(state, _make_block("divider", label="", icon=""))
_append_block(state, _make_block("question", label="Question", title="User question", body=question.strip(), meta=_ts(), icon="?"))
_append_block(state, _make_block("divider", label="", icon=""))
return state
def _run_button_label(runtime_state: Optional[Dict[str, Any]]) -> str:
runtime = dict(runtime_state or {})
if runtime.get("session_file") or runtime.get("session_id"):
return "โ–ถ Continue"
return "โ–ถ Start deep research now"
def _finalize_answer_block(state: Dict[str, Any], final_text: str) -> None:
final_text = final_text.strip() or "(empty answer)"
idx = state.get("current_assistant")
if idx is None:
for pos in range(len(state["blocks"]) - 1, -1, -1):
if state["blocks"][pos]["kind"] in {"assistant", "answer"}:
idx = pos
break
if idx is None:
idx = _append_block(
state,
_make_block("answer", label="Answer", title="Final answer", body=final_text, meta=_ts(), icon="A"),
)
block = state["blocks"][idx]
block["kind"] = "answer"
block["label"] = "Answer"
block["title"] = "Final answer"
block["icon"] = "A"
if final_text:
block["body"] = final_text
block["meta"] = _ts()
block["active"] = False
state["current_assistant"] = None
def _apply_event(state: Dict[str, Any], event: Dict[str, Any]) -> bool:
et = event.get("type", "")
if et == "turn_start":
state["thinking_streamed_this_turn"] = False
return False
if et in {"message_start", "message_end", "agent_start", "provider_request_context", "response"}:
return False
if et == "turn_end":
thinking = _extract_thinking_text(event)
if not thinking:
return False
if state.get("thinking_streamed_this_turn"):
return False
response_idx = None
for pos in range(len(state["blocks"]) - 1, -1, -1):
if state["blocks"][pos]["kind"] in {"assistant", "answer"}:
response_idx = pos
break
if response_idx is not None and state.get("current_think") is None:
idx = _insert_block(
state,
response_idx,
_make_block("think", label="Thinking", title="Reasoning", icon="T", active=True),
)
state["current_think"] = idx
block = _ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T")
block["body"] += thinking
if state.get("thinking_started_at") is not None:
block["meta"] = f"{time.perf_counter() - state['thinking_started_at']:.1f}s"
state["thinking_started_at"] = None
_close_current(state, "current_think")
return True
if et == "message_update":
ame = event.get("assistantMessageEvent", {}) or {}
mt = ame.get("type")
if mt == "thinking_start":
_ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T")
state["thinking_started_at"] = time.perf_counter()
state["thinking_streamed_this_turn"] = True
return True
if mt == "thinking_delta":
block = _ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T")
block["body"] += ame.get("delta", "")
state["thinking_streamed_this_turn"] = True
return True
if mt == "thinking_end":
block = _ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T")
if not block["body"] and ame.get("thinking"):
block["body"] = ame.get("thinking", "")
state["thinking_streamed_this_turn"] = True
if state.get("thinking_started_at") is not None:
block["meta"] = f"{time.perf_counter() - state['thinking_started_at']:.1f}s"
state["thinking_started_at"] = None
_close_current(state, "current_think")
return True
if mt == "toolcall_start":
state["pending_tool_name"] = ""
state["pending_tool_args"] = ""
return False
if mt == "toolcall_delta":
state["pending_tool_args"] += ame.get("delta", "")
return False
if mt == "toolcall_end":
tool_call = ame.get("toolCall", {}) or {}
state["pending_tool_name"] = str(tool_call.get("name") or "tool")
state["pending_tool_args"] = str(tool_call.get("arguments") or state.get("pending_tool_args", ""))
return False
if mt == "text_start":
_ensure_block(state, "current_assistant", kind="assistant", label="Answer", title="Drafting response", icon="A")
return True
if mt == "text_delta":
block = _ensure_block(state, "current_assistant", kind="assistant", label="Answer", title="Drafting response", icon="A")
block["body"] += ame.get("delta", "")
return True
if mt == "text_end":
block = _ensure_block(state, "current_assistant", kind="assistant", label="Answer", title="Drafting response", icon="A")
if ame.get("content") and len(str(ame["content"])) >= len(block["body"]):
block["body"] = str(ame["content"])
_close_current(state, "current_assistant")
return True
if mt in {"done", "error"}:
_close_current(state, "current_assistant")
_close_current(state, "current_think")
_close_current(state, "current_tool")
state["pending_tool_name"] = ""
state["pending_tool_args"] = ""
state["thinking_started_at"] = None
return True
return False
if et == "tool_execution_start":
tool_name = str(event.get("toolName") or "tool")
args = event.get("args") or {}
tool_call_id = str(event.get("toolCallId") or "")
pending_args = str(state.get("pending_tool_args") or "")
_append_block(
state,
_make_block(
"tool",
label="Tool",
title=tool_name,
body=_tool_message(tool_name, args=args, raw_arguments=pending_args),
meta=_ts(),
icon=">",
),
)
if tool_call_id:
state["tool_started_at"][tool_call_id] = time.perf_counter()
state["pending_tool_name"] = ""
state["pending_tool_args"] = ""
_ensure_block(state, "current_result", kind="result", label="Result", title=tool_name, icon="#")
return True
if et == "tool_execution_update":
output = event.get("output") or event.get("partialOutput") or event.get("delta") or ""
if not output:
return False
tool_name = str(event.get("toolName") or "tool")
block = _ensure_block(state, "current_result", kind="result", label="Result", title=tool_name, icon="#")
block["body"] += _tool_result_text(output)
return True
if et == "tool_execution_end":
tool_name = str(event.get("toolName") or "tool")
result = _tool_result_text(event.get("result"))
tool_call_id = str(event.get("toolCallId") or "")
block = _ensure_block(state, "current_result", kind="result", label="Result", title=tool_name, icon="#")
if result and len(result) >= len(block["body"]):
block["body"] = result
if tool_call_id:
started = state["tool_started_at"].pop(tool_call_id, None)
if started is not None:
block["meta"] = _ts()
block["footer"] = f"{time.perf_counter() - started:.1f}s"
elif not block.get("meta"):
block["meta"] = _ts()
_close_current(state, "current_result")
if event.get("isError"):
block["kind"] = "error"
block["label"] = "Error"
return True
if et == "agent_end":
_close_current(state, "current_think")
_close_current(state, "current_tool")
_close_current(state, "current_result")
state["thinking_started_at"] = None
state["pending_tool_name"] = ""
state["pending_tool_args"] = ""
state["tool_started_at"].clear()
note = str(event.get("note") or "").strip()
if note:
_append_block(state, _make_block("status", label="Status", title="Agent finished", body=note, meta=_ts(), icon="="))
return True
return False
if et == "error":
_close_current(state, "current_think")
_close_current(state, "current_tool")
_close_current(state, "current_result")
_close_current(state, "current_assistant")
state["thinking_started_at"] = None
state["pending_tool_name"] = ""
state["pending_tool_args"] = ""
state["tool_started_at"].clear()
_append_block(
state,
_make_block("error", label="Error", title="Run error", body=str(event.get("error", "?")), meta=_ts(), icon="!"),
)
return True
if et == "__final__":
if state.get("finalized"):
return False
_finalize_answer_block(state, str(event.get("text", "")))
elapsed = time.perf_counter() - state["run_started_at"]
_append_block(
state,
_make_block(
"status",
label="Status",
title="Run time",
body=f"{elapsed:.1f} second",
meta=_ts(),
icon="=",
),
)
state["finalized"] = True
return True
return False
_TERM_IDLE = _render_terminal(
[
_make_block("info", label="Info", title="Execution log", body="Waiting for run...", meta=_ts(), icon="i"),
]
)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Prompt builders
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def build_benchmark_prompt(query: str, corpus_dir: Path) -> str:
return (
"Answer the following question. "
f"The answer is contained in the corpus directory at @{corpus_dir}. "
"**Do Not use web search!** Use ripgrep (rg) instead of grep.\n\n"
f"QUESTION:\n{query}\n"
)
def build_ir_prompt(query: str, corpus_dir: Path) -> str:
return (
f"You are a careful research assistant. Answer using ONLY documents in @{corpus_dir}.\n"
"Do not use online search or any external tools beyond Grep and Bash.\n\n"
f"Question:\n{query}\n\n"
"SEARCH STRATEGY:\n"
"1. Use Grep/Bash ONLY โ€” no Agent tool, no subagents, no web.\n"
"2. Run multiple searches IN PARALLEL per response.\n"
"3. Diverse, targeted keywords; exhaust all angles.\n\n"
"RETRIEVAL:\n"
"- Maximize both recall and precision (NDCG scoring).\n"
"- Include every relevant document; exclude every irrelevant one.\n\n"
"OUTPUT FORMAT:\n"
"Relevant Documents (ranked; max 20):\n"
"1. {corpus}/path/doc1.txt\n\n"
"Explanation: {reasoning}\n"
"Exact Answer: {answer}\n"
"Confidence: {0โ€“100%}\n"
)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Main search handler
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def run_search(
api_key: str,
question: str,
model: str,
max_turns: int,
mode: str,
corpus_source: str,
uploaded_corpus: Any,
runtime_state: Optional[Dict[str, Any]],
) -> Generator[tuple[str, str, Dict[str, Any]], None, None]:
"""Yields (terminal_html, status_str, runtime_state, run_btn_update)."""
if not api_key or not api_key.strip():
runtime = dict(runtime_state or {})
yield _TERM_IDLE, "โš  OpenAI API Key is required.", runtime, gr.update(value=_run_button_label(runtime))
return
if not question or not question.strip():
runtime = dict(runtime_state or {})
yield _TERM_IDLE, "โš  Please enter a question.", runtime, gr.update(value=_run_button_label(runtime))
return
runtime = dict(runtime_state or {})
use_default_corpus = corpus_source == DEFAULT_CORPUS_LABEL
selected_domain = _selected_example_corpus(question) if use_default_corpus else None
stored_domain = str(runtime.get("selected_domain") or "").strip() or None
if use_default_corpus and selected_domain and selected_domain != stored_domain:
runtime = _cleanup_runtime_state(runtime)
corpus_key = json.dumps(
{
"source": corpus_source,
"uploaded_corpus": _uploaded_corpus_signature(uploaded_corpus),
},
ensure_ascii=False,
sort_keys=True,
)
corpus_root_value = runtime.get("corpus_root")
corpus_root = Path(corpus_root_value) if corpus_root_value else None
if runtime.get("corpus_key") != corpus_key or corpus_root is None or not corpus_root.exists():
runtime = _cleanup_runtime_state(runtime)
try:
corpus_root = _ensure_corpus_ready(use_default_corpus, uploaded_corpus, selected_domain)
except ValueError as exc:
yield _TERM_IDLE, f"โš  {exc}", runtime, gr.update(value=_run_button_label(runtime))
return
except Exception as exc:
yield _TERM_IDLE, f"โš  Corpus error: {exc}", runtime, gr.update(value=_run_button_label(runtime))
return
session_dir = Path(tempfile.mkdtemp(prefix="pi_session_"))
runtime.update(
{
"corpus_key": corpus_key,
"corpus_root": str(corpus_root),
"session_dir": str(session_dir),
"session_file": "",
"session_id": "",
"selected_domain": selected_domain or "",
"session_cwd": str(_resolve_example_corpus(corpus_root, question) if use_default_corpus else corpus_root),
}
)
assert corpus_root is not None
active_domain = str(runtime.get("selected_domain") or "").strip() or None
session_cwd_value = runtime.get("session_cwd")
cwd = Path(session_cwd_value) if session_cwd_value else (_resolve_example_corpus(corpus_root, question) if use_default_corpus else corpus_root)
prompt = build_ir_prompt(question, cwd) if mode == "IR" else build_benchmark_prompt(question, cwd)
state = _prepare_terminal_state(
runtime.get("terminal_state"),
question=question,
cwd=cwd,
mode=mode,
model=model,
max_turns=max_turns,
selected_domain=active_domain,
)
runtime["terminal_state"] = state
yield _render_terminal_state(state), "โš™ Runningโ€ฆ", runtime, gr.update(value=_run_button_label(runtime))
try:
for event in run_pi_stream(
prompt,
cwd=cwd,
api_key=api_key.strip(),
provider="openai",
model=model,
max_turns=max_turns,
session_dir=Path(runtime["session_dir"]) if runtime.get("session_dir") else None,
session_path=Path(runtime["session_file"]) if runtime.get("session_file") else None,
continue_session=bool(runtime.get("session_file") or runtime.get("session_id")),
):
if event.get("type") == "__session__":
if event.get("sessionFile"):
runtime["session_file"] = str(event["sessionFile"])
if event.get("sessionId"):
runtime["session_id"] = str(event["sessionId"])
_update_run_config(
state,
runner=str(event.get("runner") or "Native PI"),
thinking_level=str(event.get("thinkingLevel") or "unknown"),
provider=str(event.get("provider") or "openai"),
model=str(event.get("model") or model),
max_turns=max_turns,
domain=active_domain,
)
runtime["terminal_state"] = state
yield _render_terminal_state(state), "โš™ Runningโ€ฆ", runtime, gr.update(value=_run_button_label(runtime))
continue
if _apply_event(state, event):
status = "โœ“ Completed" if event.get("type") == "__final__" else "โš™ Runningโ€ฆ"
runtime["terminal_state"] = state
yield _render_terminal_state(state), status, runtime, gr.update(value=_run_button_label(runtime))
except Exception as exc:
_apply_event(state, {"type": "error", "error": str(exc)})
runtime["terminal_state"] = state
yield _render_terminal_state(state), f"โš  {exc}", runtime, gr.update(value=_run_button_label(runtime))
return
if not state.get("finalized"):
_apply_event(state, {"type": "__final__", "text": ""})
runtime["terminal_state"] = state
yield _render_terminal_state(state), "โœ“ Completed", runtime, gr.update(value=_run_button_label(runtime))
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Gradio UI
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def build_ui() -> gr.Blocks:
hero_logo_src = f"/gradio_api/file={HERO_LOGO_PATH}"
with gr.Blocks(title="DCI-Agent Search") as demo:
runtime_state = gr.State({})
with gr.Row(equal_height=False):
# โ”€โ”€ Left sidebar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Column(scale=1, min_width=270, elem_classes=["sidebar-panel"]):
gr.HTML(f"""
<div class="hero-wrap">
<div class="hero-title-row">
<img class="hero-logo" src="{hero_logo_src}" alt="DCI-Agent logo" />
<span class="hero-title">DCI-Agent Search</span>
</div>
<span class="hero-subtitle">
Deep Research on your own knowledge base
</span>
<div class="hero-links">
<a href="https://huggingface.co/papers/2605.05242" title="Hugging Face Paper" target="_blank" rel="noopener noreferrer">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
HF Paper
</a>
<a href="https://github.com/DCI-Agent/DCI-Agent-Lite" title="GitHub" target="_blank" rel="noopener noreferrer">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
GitHub
</a>
<a href="https://x.com/zhuofengli96475/status/2052784645398303198" title="X (Twitter)" target="_blank" rel="noopener noreferrer">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.747l7.73-8.835L1.254 2.25H8.08l4.253 5.622 5.911-5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
X (Twitter)
</a>
<a href="#" title="Slack" target="_blank" rel="noopener noreferrer">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"/><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"/><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"/><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"/><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"/><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"/><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"/></svg>
Slack
</a>
</div>
</div>
""")
# Card 1: Configuration
with gr.Group(elem_classes=["pp-card"]):
_env_key = os.environ.get("OPENAI_API_KEY", "")
if not _env_key:
gr.HTML('<div style="color:#FC8181;font-size:11px">โš  OPENAI_API_KEY not configured</div>')
api_key = gr.Textbox(
label="OpenAI API Key",
value=_env_key,
visible=False,
)
model = gr.Dropdown(
choices=[
("GPT-5.4 nano", "gpt-5.4-nano"),
],
value="gpt-5.4-nano",
label="Model",
allow_custom_value=False,
filterable=False,
)
max_turns = gr.Slider(
minimum=1, maximum=50, step=1, value=20,
label="Max Turns",
)
# Mode (QA by default, hidden)
mode = gr.Dropdown(
choices=["QA"],
value="QA",
visible=False,
)
# Card 3: Corpus
with gr.Group(elem_classes=["pp-card"]):
corpus_source = gr.Radio(
choices=[DEFAULT_CORPUS_LABEL, UPLOAD_FILE_LABEL, UPLOAD_FOLDER_LABEL],
value=DEFAULT_CORPUS_LABEL,
label="Corpus source",
)
with gr.Column(visible=False) as upload_file_wrap:
uploaded_single_file = gr.File(
label="file",
file_count="single",
)
with gr.Column(visible=False) as upload_folder_wrap:
uploaded_folder = gr.File(
label="folder",
file_count="directory",
)
corpus_hint = gr.HTML(
value='<div style="font-size:10.5px;color:#4E4E72;line-height:1.9;margin-top:8px">'
'Use single file for one document &nbsp;ยท&nbsp; Max 5 MB per file'
'</div>',
visible=False,
)
# Status
status_text = gr.Textbox(
label="Status",
value="Ready โœ”",
interactive=False,
elem_classes=["status-box"],
)
# โ”€โ”€ Right main panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
with gr.Column(scale=3, elem_classes=["main-panel"]):
with gr.Column(elem_id="terminal-shell"):
with gr.Column(elem_id="terminal-input-wrap"):
terminal_question = gr.Textbox(
label="",
placeholder="In our demo, we use the BRIGHT corpus, where all questions are tied to the provided documents",
lines=3,
elem_id="terminal-question-input",
)
with gr.Row(elem_id="terminal-actions-wrap"):
run_btn = gr.Button(
"โ–ถ Start deep research now",
variant="primary",
elem_classes=["btn-run"],
scale=3,
)
stop_btn = gr.Button(
"โ–  Stop",
variant="secondary",
elem_classes=["btn-stop"],
scale=1,
)
clear_btn = gr.Button(
"New",
variant="secondary",
elem_classes=["btn-clear"],
scale=1,
)
with gr.Column(elem_id="terminal-examples-wrap"):
gr.HTML(f"""
<div style="margin-top:22px">
<span class="sec-label">
Example Questions
<span style="font-weight:400;text-transform:none;letter-spacing:0;color:#4E4E72;font-size:10.5px">
โ€” click to fill
</span>
</span>
{_build_examples_html()}
</div>
""")
with gr.Column(elem_id="terminal-log-wrap"):
terminal = gr.HTML(value=_TERM_IDLE)
# โ”€โ”€ Wiring โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def _toggle_upload(source: str):
show_file = source == UPLOAD_FILE_LABEL
show_folder = source == UPLOAD_FOLDER_LABEL
show_hint = show_file or show_folder
if show_file:
hint_html = (
'<div style="font-size:10.5px;color:#4E4E72;line-height:1.9;margin-top:8px">'
'Use single file for one document &nbsp;ยท&nbsp; Max 5 MB per file'
'</div>'
)
elif show_folder:
hint_html = (
'<div style="font-size:10.5px;color:#4E4E72;line-height:1.9;margin-top:8px">'
'Use folder for a corpus &nbsp;ยท&nbsp; Max 25 MB total &nbsp;ยท&nbsp; 5 MB per file'
'</div>'
)
else:
hint_html = ""
return (
gr.update(visible=show_file),
gr.update(visible=show_folder),
gr.update(visible=show_hint, value=hint_html),
)
corpus_source.change(
fn=_toggle_upload,
inputs=[corpus_source],
outputs=[upload_file_wrap, upload_folder_wrap, corpus_hint],
)
corpus_source.select(
fn=_toggle_upload,
inputs=[corpus_source],
outputs=[upload_file_wrap, upload_folder_wrap, corpus_hint],
)
def clear_all(runtime_state):
cleared_runtime = _cleanup_runtime_state(runtime_state)
return (
_TERM_IDLE, "", "Ready โœ”", cleared_runtime, gr.update(value=_run_button_label(cleared_runtime)),
)
clear_btn.click(
fn=clear_all,
inputs=[runtime_state],
outputs=[terminal, terminal_question, status_text, runtime_state, run_btn],
)
def _run_with_qa_mode(api_key, question, model, max_turns, corpus_source, uploaded_single_file, uploaded_folder, runtime_state):
uploaded_corpus = _select_uploaded_corpus(corpus_source, uploaded_single_file, uploaded_folder)
yield from run_search(api_key, question, model, max_turns, "QA", corpus_source, uploaded_corpus, runtime_state)
run_event = run_btn.click(
fn=_run_with_qa_mode,
inputs=[api_key, terminal_question, model, max_turns, corpus_source, uploaded_single_file, uploaded_folder, runtime_state],
outputs=[terminal, status_text, runtime_state, run_btn],
)
terminal_question.submit(
fn=_run_with_qa_mode,
inputs=[api_key, terminal_question, model, max_turns, corpus_source, uploaded_single_file, uploaded_folder, runtime_state],
outputs=[terminal, status_text, runtime_state, run_btn],
)
stop_btn.click(fn=None, cancels=[run_event])
return demo
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Entry point
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if __name__ == "__main__":
demo = build_ui()
demo.queue(default_concurrency_limit=3)
demo.launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", 7860)),
share=False,
theme=PP_THEME,
css=CUSTOM_CSS,
js=GLOBAL_JS,
allowed_paths=[str(APP_DIR / "img")],
)