Spaces:
Running
Running
""" | |
Custom themes and styling for the Gradio interface. | |
""" | |
import gradio as gr | |
from typing import Dict, Any | |
def get_custom_css() -> str: | |
"""Get custom CSS styling for the interface.""" | |
return """ | |
/* Global styles */ | |
.gradio-container { | |
font-family: 'Inter', sans-serif; | |
max-width: 1400px !important; | |
margin: 0 auto; | |
} | |
/* Header styling */ | |
.header-container { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
padding: 2rem; | |
border-radius: 12px; | |
margin-bottom: 2rem; | |
text-align: center; | |
} | |
.header-title { | |
font-size: 2.5rem; | |
font-weight: 700; | |
margin-bottom: 0.5rem; | |
} | |
.header-description { | |
font-size: 1.1rem; | |
opacity: 0.9; | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
/* Tab styling */ | |
.tab-nav button { | |
font-weight: 500; | |
font-size: 1rem; | |
padding: 12px 24px; | |
border-radius: 8px; | |
transition: all 0.2s ease; | |
} | |
.tab-nav button[aria-selected="true"] { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
} | |
/* Upload area styling */ | |
.upload-area { | |
border: 2px dashed #e0e7ff; | |
border-radius: 12px; | |
padding: 2rem; | |
text-align: center; | |
background: #f8faff; | |
transition: all 0.3s ease; | |
} | |
.upload-area:hover { | |
border-color: #667eea; | |
background: #f0f4ff; | |
} | |
/* Search interface styling */ | |
.search-container { | |
background: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
margin-bottom: 1.5rem; | |
} | |
.search-input { | |
font-size: 1.1rem; | |
padding: 1rem; | |
border-radius: 8px; | |
border: 2px solid #e5e7eb; | |
transition: border-color 0.2s ease; | |
} | |
.search-input:focus { | |
border-color: #667eea; | |
outline: none; | |
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
} | |
.search-button { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
border: none; | |
padding: 1rem 2rem; | |
border-radius: 8px; | |
font-weight: 600; | |
font-size: 1rem; | |
cursor: pointer; | |
transition: all 0.2s ease; | |
} | |
.search-button:hover { | |
transform: translateY(-1px); | |
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); | |
} | |
/* Process Documents button styling */ | |
.process-button { | |
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; | |
color: white !important; | |
border: none !important; | |
padding: 1rem 2rem !important; | |
border-radius: 12px !important; | |
font-weight: 700 !important; | |
font-size: 1.1rem !important; | |
cursor: pointer !important; | |
transition: all 0.3s ease !important; | |
box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3) !important; | |
min-height: 60px !important; | |
width: 100% !important; | |
} | |
.process-button:hover { | |
transform: translateY(-2px) !important; | |
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4) !important; | |
background: linear-gradient(135deg, #059669 0%, #047857 100%) !important; | |
} | |
.process-button:disabled { | |
background: #d1d5db !important; | |
color: #9ca3af !important; | |
cursor: not-allowed !important; | |
transform: none !important; | |
box-shadow: none !important; | |
} | |
/* Results styling */ | |
.result-card { | |
background: white; | |
border: 1px solid #e5e7eb; | |
border-radius: 12px; | |
padding: 1.5rem; | |
margin-bottom: 1rem; | |
transition: all 0.2s ease; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
} | |
.result-card:hover { | |
border-color: #667eea; | |
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1); | |
} | |
.result-title { | |
font-size: 1.1rem; | |
font-weight: 600; | |
color: #374151; | |
margin-bottom: 0.5rem; | |
} | |
.result-content { | |
color: #6b7280; | |
line-height: 1.6; | |
margin-bottom: 1rem; | |
} | |
.result-metadata { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 0.5rem; | |
margin-bottom: 0.5rem; | |
} | |
.metadata-tag { | |
background: #f3f4f6; | |
color: #374151; | |
padding: 0.25rem 0.5rem; | |
border-radius: 6px; | |
font-size: 0.875rem; | |
font-weight: 500; | |
} | |
.score-badge { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
color: white; | |
padding: 0.25rem 0.75rem; | |
border-radius: 20px; | |
font-size: 0.875rem; | |
font-weight: 600; | |
} | |
/* Progress bar styling */ | |
.progress-container { | |
background: #f3f4f6; | |
border-radius: 8px; | |
overflow: hidden; | |
height: 12px; | |
margin: 1rem 0; | |
} | |
.progress-bar { | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
height: 100%; | |
transition: width 0.3s ease; | |
} | |
/* Statistics cards */ | |
.stat-card { | |
background: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
text-align: center; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
border: 1px solid #e5e7eb; | |
} | |
.stat-number { | |
font-size: 2rem; | |
font-weight: 700; | |
color: #667eea; | |
margin-bottom: 0.5rem; | |
} | |
.stat-label { | |
color: #6b7280; | |
font-weight: 500; | |
} | |
/* Analytics charts */ | |
.chart-container { | |
background: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
border: 1px solid #e5e7eb; | |
margin-bottom: 1rem; | |
} | |
/* Settings panel */ | |
.settings-panel { | |
background: #f8faff; | |
border-radius: 12px; | |
padding: 1.5rem; | |
border: 1px solid #e0e7ff; | |
} | |
.settings-group { | |
margin-bottom: 1.5rem; | |
} | |
.settings-label { | |
font-weight: 600; | |
color: #374151; | |
margin-bottom: 0.5rem; | |
display: block; | |
} | |
/* Status indicators */ | |
.status-indicator { | |
display: inline-flex; | |
align-items: center; | |
gap: 0.5rem; | |
padding: 0.75rem 1.25rem; | |
border-radius: 25px; | |
font-size: 0.9rem; | |
font-weight: 600; | |
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
transition: all 0.2s ease; | |
} | |
.status-ready { | |
background: #10b981; | |
color: #ffffff; | |
font-weight: 600; | |
border: 1px solid #059669; | |
} | |
.status-processing { | |
background: #f59e0b; | |
color: #ffffff; | |
font-weight: 600; | |
border: 1px solid #d97706; | |
} | |
.status-error { | |
background: #ef4444; | |
color: #ffffff; | |
font-weight: 600; | |
border: 1px solid #dc2626; | |
} | |
/* Responsive design */ | |
@media (max-width: 768px) { | |
.gradio-container { | |
padding: 1rem; | |
} | |
.header-title { | |
font-size: 2rem; | |
} | |
.header-description { | |
font-size: 1rem; | |
} | |
.search-container, | |
.result-card, | |
.stat-card, | |
.chart-container { | |
padding: 1rem; | |
} | |
.result-metadata { | |
flex-direction: column; | |
} | |
} | |
/* Animation classes */ | |
.fade-in { | |
animation: fadeIn 0.3s ease-in; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
.pulse { | |
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
} | |
@keyframes pulse { | |
0%, 100% { opacity: 1; } | |
50% { opacity: .5; } | |
} | |
/* Loading spinner */ | |
.loading-spinner { | |
display: inline-block; | |
width: 20px; | |
height: 20px; | |
border: 3px solid #f3f4f6; | |
border-radius: 50%; | |
border-top-color: #667eea; | |
animation: spin 1s ease-in-out infinite; | |
} | |
@keyframes spin { | |
to { transform: rotate(360deg); } | |
} | |
/* Scrollbar styling */ | |
::-webkit-scrollbar { | |
width: 8px; | |
height: 8px; | |
} | |
::-webkit-scrollbar-track { | |
background: #f1f5f9; | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb { | |
background: #cbd5e1; | |
border-radius: 4px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background: #94a3b8; | |
} | |
""" | |
def get_theme() -> gr.Theme: | |
"""Get the custom Gradio theme.""" | |
theme = gr.themes.Soft( | |
primary_hue=gr.themes.Color( | |
c50="#f0f4ff", | |
c100="#e0e7ff", | |
c200="#c7d2fe", | |
c300="#a5b4fc", | |
c400="#8b5cf6", | |
c500="#667eea", | |
c600="#5b21b6", | |
c700="#4c1d95", | |
c800="#3730a3", | |
c900="#312e81", | |
c950="#1e1b4b" | |
), | |
secondary_hue=gr.themes.Color( | |
c50="#f8fafc", | |
c100="#f1f5f9", | |
c200="#e2e8f0", | |
c300="#cbd5e1", | |
c400="#94a3b8", | |
c500="#64748b", | |
c600="#475569", | |
c700="#334155", | |
c800="#1e293b", | |
c900="#0f172a", | |
c950="#020617" | |
), | |
neutral_hue=gr.themes.Color( | |
c50="#f9fafb", | |
c100="#f3f4f6", | |
c200="#e5e7eb", | |
c300="#d1d5db", | |
c400="#9ca3af", | |
c500="#6b7280", | |
c600="#4b5563", | |
c700="#374151", | |
c800="#1f2937", | |
c900="#111827", | |
c950="#030712" | |
), | |
font=[ | |
gr.themes.GoogleFont("Inter"), | |
"ui-sans-serif", | |
"system-ui", | |
"sans-serif" | |
], | |
font_mono=[ | |
gr.themes.GoogleFont("JetBrains Mono"), | |
"ui-monospace", | |
"Consolas", | |
"monospace" | |
] | |
) | |
# Customize component styles (using only supported parameters) | |
try: | |
theme.set( | |
button_primary_background_fill="#667eea", | |
button_primary_background_fill_hover="#5a67d8", | |
button_primary_text_color="white", | |
button_secondary_background_fill="#f8fafc", | |
button_secondary_text_color="#374151", | |
input_background_fill="#ffffff", | |
input_border_color="#e5e7eb", | |
block_background_fill="#ffffff", | |
block_border_color="#e5e7eb", | |
panel_background_fill="#ffffff" | |
) | |
except Exception as e: | |
# Fallback if theme customization fails | |
print(f"Theme customization failed: {e}") | |
pass | |
return theme | |
def create_info_card(title: str, value: str, description: str = "") -> str: | |
"""Create an info card HTML.""" | |
return f""" | |
<div class="stat-card"> | |
<div class="stat-number">{value}</div> | |
<div class="stat-label">{title}</div> | |
{f'<div style="font-size: 0.875rem; color: #6b7280; margin-top: 0.5rem;">{description}</div>' if description else ''} | |
</div> | |
""" | |
def create_status_indicator(status: str, message: str) -> str: | |
"""Create a status indicator HTML.""" | |
status_class = f"status-{status.lower()}" | |
icon = { | |
"ready": "🟢", | |
"processing": "🟡", | |
"error": "🔴", | |
"loading": "⏳" | |
}.get(status.lower(), "⚪") | |
return f""" | |
<div class="status-indicator {status_class}"> | |
<span>{icon}</span> | |
<span>{message}</span> | |
</div> | |
""" | |
def format_search_result(result: dict, rank: int) -> str: | |
"""Format a search result as HTML.""" | |
content = result.get("content", "No content available") | |
metadata = result.get("metadata", {}) | |
scores = result.get("scores", {}) | |
# Truncate content for display but show more than before | |
display_content = content | |
if len(content) > 800: | |
display_content = content[:800] + "..." | |
# Escape HTML characters in content | |
display_content = display_content.replace('<', '<').replace('>', '>').replace('&', '&') | |
# Format metadata tags | |
metadata_tags = [] | |
if metadata.get("source"): | |
filename = metadata['source'].split('/')[-1] | |
metadata_tags.append(f"📄 {filename}") | |
if metadata.get("page"): | |
metadata_tags.append(f"📖 Page {metadata['page']}") | |
if metadata.get("chunk_index") is not None: | |
metadata_tags.append(f"🔢 Chunk {metadata['chunk_index']}") | |
metadata_html = "".join([f'<span class="metadata-tag">{tag}</span>' for tag in metadata_tags]) | |
# Format scores with more detail | |
final_score = scores.get("final_score", 0) | |
vector_score = scores.get("vector_score", 0) | |
bm25_score = scores.get("bm25_score", 0) | |
score_html = f'<span class="score-badge">Score: {final_score:.3f}</span>' | |
# Add detailed scores in a collapsible section | |
detailed_scores = f""" | |
<div style="margin-top: 0.5rem; font-size: 0.8rem; color: #6b7280;"> | |
Vector: {vector_score:.3f} | BM25: {bm25_score:.3f} | |
</div> | |
""" | |
return f""" | |
<div class="result-card fade-in" style="margin-bottom: 1.5rem;"> | |
<div class="result-title" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;"> | |
<span>Result #{rank}</span> | |
{score_html} | |
</div> | |
<div class="result-content" style="background: #ffffff; border: 1px solid #e5e7eb; border-left: 4px solid #667eea; padding: 1rem; margin-bottom: 0.75rem; border-radius: 4px; line-height: 1.6; white-space: pre-wrap; color: #1f2937; font-weight: 500; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);"> | |
{display_content} | |
</div> | |
<div class="result-metadata" style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;"> | |
{metadata_html} | |
{detailed_scores} | |
</div> | |
</div> | |
""" | |
def create_progress_bar(progress: float, message: str = "") -> str: | |
"""Create a progress bar HTML.""" | |
progress_percent = max(0, min(100, progress * 100)) | |
return f""" | |
<div style="margin: 1rem 0;"> | |
{f'<div style="margin-bottom: 0.5rem; color: #374151; font-weight: 500;">{message}</div>' if message else ''} | |
<div class="progress-container"> | |
<div class="progress-bar" style="width: {progress_percent}%"></div> | |
</div> | |
<div style="text-align: center; font-size: 0.875rem; color: #6b7280; margin-top: 0.25rem;"> | |
{progress_percent:.1f}% | |
</div> | |
</div> | |
""" |