NotebookLM / theme.py
internomega-terrablue
fix: target Gradio 4 chatbot wrapper for compact user bubble
ba4c4fa
"""Dark theme, custom CSS, and logo SVGs for the NotebookLM Gradio app."""
import base64
import gradio as gr
# ── Logo SVGs ────────────────────────────────────────────────────────────────
LOGO_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 60">
<defs>
<linearGradient id="lg1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
<linearGradient id="lg2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#a78bfa"/>
<stop offset="100%" style="stop-color:#667eea"/>
</linearGradient>
<linearGradient id="sp" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#fbbf24"/>
<stop offset="100%" style="stop-color:#f59e0b"/>
</linearGradient>
</defs>
<g transform="translate(4,6)">
<rect x="2" y="4" width="36" height="44" rx="4" fill="url(#lg1)"/>
<rect x="2" y="4" width="8" height="44" rx="3" fill="url(#lg2)" opacity="0.7"/>
<line x1="16" y1="16" x2="32" y2="16" stroke="rgba(255,255,255,0.5)" stroke-width="1.8" stroke-linecap="round"/>
<line x1="16" y1="23" x2="30" y2="23" stroke="rgba(255,255,255,0.4)" stroke-width="1.8" stroke-linecap="round"/>
<line x1="16" y1="30" x2="32" y2="30" stroke="rgba(255,255,255,0.5)" stroke-width="1.8" stroke-linecap="round"/>
<line x1="16" y1="37" x2="28" y2="37" stroke="rgba(255,255,255,0.4)" stroke-width="1.8" stroke-linecap="round"/>
<g transform="translate(32,2)">
<path d="M6 0 L7.5 4.5 L12 6 L7.5 7.5 L6 12 L4.5 7.5 L0 6 L4.5 4.5 Z" fill="url(#sp)"/>
<path d="M14 8 L14.8 10.2 L17 11 L14.8 11.8 L14 14 L13.2 11.8 L11 11 L13.2 10.2 Z" fill="#fbbf24" opacity="0.7"/>
</g>
</g>
<text x="56" y="28" font-family="Inter,-apple-system,sans-serif" font-size="22" font-weight="700">
<tspan fill="url(#lg1)">Notebook</tspan><tspan fill="#a78bfa" font-weight="800">LM</tspan>
</text>
<text x="57" y="46" font-family="Inter,-apple-system,sans-serif" font-size="10.5" fill="#8888aa" font-weight="400" letter-spacing="0.8">
AI-Powered Study Companion
</text>
</svg>"""
LOGO_ICON_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 60">
<defs>
<linearGradient id="ig1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea"/>
<stop offset="100%" style="stop-color:#764ba2"/>
</linearGradient>
<linearGradient id="ig2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#a78bfa"/>
<stop offset="100%" style="stop-color:#667eea"/>
</linearGradient>
<linearGradient id="isp" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#fbbf24"/>
<stop offset="100%" style="stop-color:#f59e0b"/>
</linearGradient>
</defs>
<g transform="translate(2,4)">
<rect x="2" y="4" width="36" height="44" rx="5" fill="url(#ig1)"/>
<rect x="2" y="4" width="9" height="44" rx="4" fill="url(#ig2)" opacity="0.7"/>
<line x1="16" y1="16" x2="32" y2="16" stroke="rgba(255,255,255,0.55)" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="23" x2="30" y2="23" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="30" x2="32" y2="30" stroke="rgba(255,255,255,0.55)" stroke-width="2" stroke-linecap="round"/>
<line x1="16" y1="37" x2="28" y2="37" stroke="rgba(255,255,255,0.4)" stroke-width="2" stroke-linecap="round"/>
<g transform="translate(30,0)">
<path d="M7 0 L8.8 5.3 L14 7 L8.8 8.8 L7 14 L5.2 8.8 L0 7 L5.2 5.3 Z" fill="url(#isp)"/>
<path d="M16 9 L17 11.5 L19.5 12.5 L17 13.5 L16 16 L15 13.5 L12.5 12.5 L15 11.5 Z" fill="#fbbf24" opacity="0.7"/>
</g>
</g>
</svg>"""
def get_logo_b64(svg_str: str) -> str:
return base64.b64encode(svg_str.encode()).decode()
LOGO_B64 = get_logo_b64(LOGO_SVG)
ICON_B64 = get_logo_b64(LOGO_ICON_SVG)
# ── Gradio Dark Theme ────────────────────────────────────────────────────────
dark_theme = gr.themes.Base(
primary_hue=gr.themes.colors.indigo,
secondary_hue=gr.themes.colors.purple,
neutral_hue=gr.themes.colors.slate,
font=gr.themes.GoogleFont("Inter"),
).set(
body_background_fill="#0e1117",
body_background_fill_dark="#0e1117",
block_background_fill="rgba(255,255,255,0.02)",
block_background_fill_dark="rgba(255,255,255,0.02)",
block_border_color="rgba(255,255,255,0.08)",
block_border_color_dark="rgba(255,255,255,0.08)",
block_label_text_color="#a0a0b8",
block_label_text_color_dark="#a0a0b8",
block_title_text_color="#e0e0f0",
block_title_text_color_dark="#e0e0f0",
body_text_color="#c8c8d8",
body_text_color_dark="#c8c8d8",
button_primary_background_fill="linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
button_primary_background_fill_dark="linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
button_primary_text_color="white",
button_primary_text_color_dark="white",
button_secondary_background_fill="rgba(255,255,255,0.04)",
button_secondary_background_fill_dark="rgba(255,255,255,0.04)",
button_secondary_border_color="rgba(255,255,255,0.12)",
button_secondary_border_color_dark="rgba(255,255,255,0.12)",
button_secondary_text_color="#d0d0e0",
button_secondary_text_color_dark="#d0d0e0",
border_color_primary="rgba(255,255,255,0.08)",
border_color_primary_dark="rgba(255,255,255,0.08)",
input_background_fill="rgba(255,255,255,0.03)",
input_background_fill_dark="rgba(255,255,255,0.03)",
input_border_color="rgba(255,255,255,0.1)",
input_border_color_dark="rgba(255,255,255,0.1)",
shadow_drop="none",
shadow_drop_lg="none",
shadow_spread="none",
)
# ── Custom CSS ───────────────────────────────────────────────────────────────
CUSTOM_CSS = """
/* ── Import Google Font ── */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
/* ── Sidebar ── */
#sidebar {
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%) !important;
border-right: 1px solid rgba(255,255,255,0.06);
padding: 16px !important;
min-height: 100vh;
position: sticky;
top: 0;
align-self: flex-start;
max-height: 100vh;
overflow-y: auto;
border-radius: 0 !important;
}
#sidebar .gr-button-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
border-radius: 10px !important;
}
#sidebar .gr-button-secondary {
background: rgba(255,255,255,0.05) !important;
border: 1px solid rgba(255,255,255,0.1) !important;
border-radius: 10px !important;
color: #d0d0e0 !important;
}
/* ── Notebook selector radio ── */
#notebook-selector label {
border-radius: 10px !important;
padding: 8px 14px !important;
transition: all 0.2s ease !important;
font-size: 0.85rem !important;
}
#notebook-selector label.selected {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
}
/* ── Tab styling ── */
.tabs > .tab-nav > button {
border-radius: 10px !important;
padding: 10px 24px !important;
font-weight: 500 !important;
font-size: 0.9rem !important;
}
.tabs > .tab-nav > button.selected {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
}
/* ── Chat ── */
#chatbot {
border-radius: 14px !important;
border: 1px solid rgba(255,255,255,0.08) !important;
}
#chatbot .message {
border-radius: 14px !important;
padding: 14px 18px !important;
}
/* User bubble β€” shrink to fit content */
#chatbot .bot-row { justify-content: flex-start !important; }
#chatbot .user-row { justify-content: flex-end !important; }
#chatbot .user-row .message-bubble-border {
max-width: fit-content !important;
}
#chatbot .message.user {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border: none !important;
box-shadow: 0 4px 14px rgba(102,126,234,0.25);
max-width: fit-content !important;
}
/* Assistant bubble */
#chatbot .message.bot {
background: rgba(40, 44, 66, 0.85) !important;
border: 1px solid rgba(102, 126, 234, 0.3) !important;
color: #c8c8d8 !important;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.25);
border-radius: 14px !important;
padding: 14px 18px !important;
backdrop-filter: blur(6px);
}
/* ── Cards ── */
.source-card {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
margin-bottom: 10px;
transition: all 0.2s ease;
}
.source-card:hover {
border-color: rgba(102,126,234,0.3);
background: rgba(255,255,255,0.04);
}
.source-icon {
width: 48px; height: 48px; border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; flex-shrink: 0;
}
.source-icon.pdf { background: rgba(239,68,68,0.15); }
.source-icon.pptx { background: rgba(249,115,22,0.15); }
.source-icon.txt { background: rgba(59,130,246,0.15); }
.source-icon.url { background: rgba(34,197,94,0.15); }
.source-icon.youtube { background: rgba(239,68,68,0.15); }
.source-info { flex: 1; min-width: 0; }
.source-info .name {
font-weight: 600; font-size: 0.95rem; color: #e0e0f0;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.source-info .meta { font-size: 0.8rem; color: #707088; margin-top: 2px; }
.source-badge {
padding: 4px 12px; border-radius: 20px; font-size: 0.75rem;
font-weight: 600; letter-spacing: 0.3px;
}
.source-badge.ready { background: rgba(34,197,94,0.15); color: #22c55e; }
.source-badge.processing {
background: rgba(234,179,8,0.15); color: #eab308;
animation: pulse-badge 1.5s ease-in-out infinite;
}
.source-badge.failed { background: rgba(239,68,68,0.15); color: #ef4444; cursor: help; }
@keyframes pulse-badge {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* ── Welcome hero ── */
.welcome-hero {
text-align: center; padding: 80px 40px;
background: linear-gradient(135deg, rgba(102,126,234,0.08) 0%, rgba(118,75,162,0.08) 100%);
border-radius: 20px; border: 1px solid rgba(102,126,234,0.15); margin: 20px 0;
}
.welcome-hero h1 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
font-size: 2.5rem; font-weight: 700; margin-bottom: 12px;
}
.welcome-hero p { color: #9090a8; font-size: 1.1rem; line-height: 1.6; }
/* ── Empty state ── */
.empty-state {
text-align: center; padding: 60px 30px; color: #707088;
}
.empty-state h3 { color: #a0a0b8; margin-bottom: 8px; font-weight: 600; }
.empty-state p { font-size: 0.95rem; line-height: 1.5; }
/* ── Notebook header ── */
.notebook-header {
padding: 0 0 16px 0; margin-bottom: 16px;
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.notebook-header h2 {
font-weight: 700; font-size: 1.5rem; margin: 0; color: #e8e8f8;
}
.notebook-header .meta { font-size: 0.85rem; color: #707088; margin-top: 4px; }
/* ── Citation chip ── */
.citation-chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 14px; background: rgba(102,126,234,0.1);
border: 1px solid rgba(102,126,234,0.2); border-radius: 20px;
font-size: 0.8rem; color: #a0b0f0; margin: 3px 4px;
}
/* ── Artifact section header ── */
.artifact-section-header {
display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
}
.artifact-section-icon {
width: 36px; height: 36px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 1.1rem;
}
/* ── Locked state ── */
.locked-state {
text-align: center; padding: 50px 30px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 16px;
}
/* ── File uploader ── */
.gr-file-upload {
border-radius: 14px !important;
border: 2px dashed rgba(102,126,234,0.3) !important;
background: rgba(102,126,234,0.03) !important;
}
/* ── Primary button ── */
.gr-button-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important; border-radius: 10px !important;
font-weight: 600 !important;
}
.gr-button-primary:hover {
opacity: 0.9 !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 15px rgba(102,126,234,0.3) !important;
}
/* ── Source delete dropdown ── */
#source-delete-dropdown {
border: 1px solid rgba(102,126,234,0.3);
border-radius: 12px;
background: rgba(102,126,234,0.06);
}
#source-delete-dropdown label {
color: #a0a0f0 !important;
font-weight: 600;
}
#source-delete-dropdown input, #source-delete-dropdown .wrap {
background: rgba(14,17,23,0.8) !important;
border-color: rgba(102,126,234,0.25) !important;
color: #e0e0f0 !important;
border-radius: 10px !important;
}
#source-delete-dropdown ul {
background: #1a1d2e !important;
border: 1px solid rgba(102,126,234,0.3) !important;
border-radius: 10px !important;
}
#source-delete-dropdown li {
color: #c0c0e0 !important;
}
#source-delete-dropdown li:hover, #source-delete-dropdown li.selected {
background: rgba(102,126,234,0.15) !important;
color: #e0e0f0 !important;
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }
/* ── Hide Gradio footer ── */
footer { display: none !important; }
/* ── Auth gate ── */
#auth-gate { max-width: 500px; margin: 100px auto; }
"""
# ── Reusable HTML Templates ──────────────────────────────────────────────────
WELCOME_HTML = f"""
<div class="welcome-hero">
<img src="data:image/svg+xml;base64,{ICON_B64}" style="width:64px; margin-bottom:16px;" />
<h1>NotebookLM</h1>
<p>Your AI-powered study companion.<br>
Sign in with your Hugging Face account to get started.</p>
</div>
"""
NO_NOTEBOOKS_HTML = """
<div class="welcome-hero">
<h1>NotebookLM</h1>
<p>Create a notebook from the sidebar to get started.</p>
</div>
"""
SIDEBAR_LOGO_HTML = f"""
<div style="padding: 8px 0 4px 0;">
<img src="data:image/svg+xml;base64,{LOGO_B64}" style="width:100%; max-width:240px;" />
</div>
"""