|
""" |
|
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" |
|
] |
|
) |
|
|
|
|
|
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: |
|
|
|
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", {}) |
|
|
|
|
|
display_content = content |
|
if len(content) > 800: |
|
display_content = content[:800] + "..." |
|
|
|
|
|
display_content = display_content.replace('<', '<').replace('>', '>').replace('&', '&') |
|
|
|
|
|
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]) |
|
|
|
|
|
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>' |
|
|
|
|
|
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> |
|
""" |