RAG_ChatBot / ui /themes.py
Jialun He
fix search
f7c2b86
"""
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('<', '&lt;').replace('>', '&gt;').replace('&', '&amp;')
# 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>
"""