"""
NotebookLM Clone - Gradio UI (professional dark theme).
Per-user isolation; sidebar + main tabs (Sources, Chat, Artifacts).
"""
import traceback
from pathlib import Path
from typing import Any, Dict, List, Tuple
import gradio as gr
from backend import artifacts as artifacts_module
from backend import ingestion
from backend import notebooks as notebooks_module
from backend import rag
from backend.auth import get_username_from_request
from backend.config import (
MAX_CHAT_HISTORY_LOAD,
RETRIEVAL_STRATEGY_MMR,
RETRIEVAL_STRATEGY_SIMILARITY,
TOP_K,
)
from backend.storage import chat_messages_path
from backend.utils import append_jsonl, read_jsonl, logger
# ---------- State ----------
def initial_state() -> Dict[str, Any]:
return {"notebook_id": None, "notebooks": [], "username": None}
# ---------- Notebook handlers ----------
def list_notebooks_for_user(request: gr.Request) -> Tuple[Dict, str, str, Any]:
username = get_username_from_request(request)
notebooks = notebooks_module.list_notebooks(username)
if not notebooks:
s = initial_state()
s["username"] = username
return s, "", "No notebooks yet. Create one.", gr.Dropdown(choices=[], value=None)
current = notebooks[0].get("id", "")
names = [n.get("name", "Untitled") for n in notebooks]
state = {"notebook_id": current, "notebooks": notebooks, "username": username}
return state, current, "Ready.", gr.Dropdown(choices=names, value=names[0] if names else None)
def create_notebook_handler(name: str, request: gr.Request) -> Tuple[Dict, str, str, gr.Dropdown]:
username = get_username_from_request(request)
try:
nb = notebooks_module.create_notebook(username, name or "Untitled Notebook")
notebooks = notebooks_module.list_notebooks(username)
names = [n.get("name", "Untitled") for n in notebooks]
return (
{"notebook_id": nb["id"], "notebooks": notebooks, "username": username},
nb["id"],
f"Created '{nb['name']}'.",
gr.Dropdown(choices=names, value=nb["name"]),
)
except Exception as e:
logger.exception("create_notebook")
return initial_state(), "", f"Error: {e}", gr.Dropdown(choices=[], value=None)
def delete_notebook_handler(notebook_id: str, request: gr.Request) -> Tuple[Dict, str, str, gr.Dropdown]:
username = get_username_from_request(request)
if not notebook_id:
return initial_state(), "", "Select a notebook first.", gr.Dropdown(choices=[], value=None)
try:
notebooks_module.delete_notebook(username, notebook_id)
notebooks = notebooks_module.list_notebooks(username)
names = [n.get("name", "Untitled") for n in notebooks]
next_id = notebooks[0].get("id", "") if notebooks else ""
next_state = {"notebook_id": next_id, "notebooks": notebooks, "username": username}
return (
next_state,
next_id,
"Notebook deleted.",
gr.Dropdown(choices=names, value=names[0] if names else None),
)
except Exception as e:
logger.exception("delete_notebook")
return initial_state(), "", f"Error: {e}", gr.Dropdown(choices=[], value=None)
def rename_notebook_handler(notebook_id: str, new_name: str, request: gr.Request) -> Tuple[str, gr.Dropdown]:
username = get_username_from_request(request)
if not notebook_id:
return "Select a notebook first.", gr.update()
try:
nb = notebooks_module.rename_notebook(username, notebook_id, new_name)
notebooks = notebooks_module.list_notebooks(username)
names = [n.get("name", "Untitled") for n in notebooks]
return f"Renamed to '{nb['name']}'.", gr.Dropdown(choices=names, value=nb["name"])
except Exception as e:
return f"Error: {e}", gr.update()
def switch_notebook_handler(notebook_id: str, request: gr.Request) -> Tuple[Dict, str, List, str]:
if not notebook_id:
return {"notebook_id": None, "notebooks": [], "username": None}, "", [], ""
username = get_username_from_request(request)
notebooks = notebooks_module.list_notebooks(username)
state = {"notebook_id": notebook_id, "notebooks": notebooks, "username": username}
path = chat_messages_path(username, notebook_id)
messages = read_jsonl(path)
messages = messages[-MAX_CHAT_HISTORY_LOAD:]
chat_ui = []
for m in messages:
role = m.get("role", "user")
content = m.get("content", "")
chat_ui.append((content if role == "user" else None, content if role == "assistant" else None))
cite_text = _last_citations_markdown(messages)
return state, notebook_id, chat_ui, cite_text
def _last_citations_markdown(messages: List[Dict]) -> str:
for m in reversed(messages):
if m.get("role") != "assistant":
continue
cites = m.get("citations", [])
if not cites:
continue
parts = []
for i, c in enumerate(cites, 1):
meta = c.get("metadata", {})
name = meta.get("source_name", "Source")
page = meta.get("page_or_slide", "")
snip = (c.get("document", ""))[:250] + "..." if len(c.get("document", "")) > 250 else c.get("document", "")
parts.append(f"**[{i}] {name}** (p.{page})\n\n{snip}")
return "\n\n---\n\n".join(parts)
return ""
# ---------- Ingestion ----------
def ingest_file_handler(file, notebook_id: str, request: gr.Request) -> str:
if not file or not notebook_id:
return "Select a notebook and upload a file."
username = get_username_from_request(request)
try:
path = Path(file.name) if hasattr(file, "name") else Path(file)
ingestion.add_source_file(username, notebook_id, path, path.name)
return f"Ingested: {path.name}"
except Exception as e:
logger.exception("ingest_file")
return f"Error: {traceback.format_exc()}"
def ingest_url_handler(url: str, notebook_id: str, request: gr.Request) -> str:
if not url or not notebook_id:
return "Enter URL and select a notebook."
username = get_username_from_request(request)
try:
ingestion.add_source_url(username, notebook_id, url.strip())
return f"Ingested URL: {url[:60]}..."
except Exception as e:
logger.exception("ingest_url")
return f"Error: {e}"
def refresh_sources_handler(notebook_id: str, request: gr.Request) -> Tuple[str, str, Any]:
if not notebook_id:
return "No notebook selected.", "", gr.Dropdown(choices=[], value=None)
username = get_username_from_request(request)
sources = ingestion.list_sources(username, notebook_id)
lines = []
choices = []
for s in sources:
name = (s.get("filename", "?") or "?")[:60]
en = "✓" if s.get("enabled", True) else "✗"
lines.append(f"- {en} {name}")
choices.append((f"{en} {name}", s.get("id", "")))
md = "\n".join(lines) if lines else "No sources. Upload files or add a URL."
return md, f"{len(sources)} source(s)", gr.Dropdown(choices=choices, value=choices[0][1] if choices else None)
def toggle_source_handler(notebook_id: str, source_id: str, enabled: bool, request: gr.Request) -> str:
if not notebook_id or not source_id:
return "Missing notebook or source."
username = get_username_from_request(request)
try:
ingestion.set_source_enabled(username, notebook_id, source_id, enabled)
return "Updated."
except Exception as e:
return f"Error: {e}"
# ---------- Chat ----------
def chat_send(
message: str,
history: List,
notebook_id: str,
strategy: str,
request: gr.Request,
) -> Tuple[List, str, str, str, str]:
if not message or not notebook_id:
return history, "", "", "", "Select a notebook and type a message."
username = get_username_from_request(request)
path = chat_messages_path(username, notebook_id)
append_jsonl(path, {"role": "user", "content": message, "ts": _now_iso()})
try:
answer_text, citations, ret_time, gen_time = rag.answer(
username, notebook_id, message, strategy=strategy, top_k=TOP_K
)
append_jsonl(
path,
{
"role": "assistant",
"content": answer_text,
"ts": _now_iso(),
"citations": [
{"document": c.get("document"), "metadata": c.get("metadata"), "id": c.get("id")}
for c in citations
],
},
)
history = history + [(message, answer_text)]
cite_block = _format_citations(citations)
timing = f"Retrieval: {ret_time:.2f}s | Generation: {gen_time:.2f}s"
return history, cite_block, "", timing, ""
except Exception as e:
logger.exception("chat_send")
return history, "", "", "", f"Error: {traceback.format_exc()}"
def _format_citations(citations: List[Dict]) -> str:
if not citations:
return ""
parts = []
for i, c in enumerate(citations, 1):
meta = c.get("metadata", {})
name = meta.get("source_name", "Source")
page = meta.get("page_or_slide", "")
snip = (c.get("document", ""))[:300] + "..." if len(c.get("document", "")) > 300 else c.get("document", "")
parts.append(f"[{i}] **{name}** (p.{page})\n\n{snip}")
return "\n\n---\n\n".join(parts)
def clear_chat_handler(notebook_id: str, request: gr.Request) -> Tuple[List, str]:
if not notebook_id:
return [], ""
username = get_username_from_request(request)
path = chat_messages_path(username, notebook_id)
if path.exists():
path.write_text("")
return [], ""
# ---------- Artifacts ----------
def artifact_quiz_click(notebook_id: str, extra: str, strategy: str, request: gr.Request) -> Tuple[str, Any, str]:
if not notebook_id:
return "Select a notebook first.", gr.update(), ""
username = get_username_from_request(request)
try:
out = artifacts_module.generate_quiz(username, notebook_id, extra_instruction=extra or "", strategy=strategy)
if out.get("error"):
return out["error"], gr.update(), ""
arts = artifacts_module.list_artifacts(username, notebook_id)
quizzes = [a for a in arts if a.get("type") == "quiz"]
choices = [(a.get("filename", "?"), a.get("filename", "")) for a in quizzes]
return f"Quiz generated: {out['filename']}", gr.Dropdown(choices=choices, value=out["filename"]), out["content"]
except Exception as e:
logger.exception("quiz")
return f"Error: {traceback.format_exc()}", gr.update(), ""
def artifact_podcast_click(notebook_id: str, extra: str, strategy: str, request: gr.Request) -> Tuple[str, Any, str, Any]:
if not notebook_id:
return "Select a notebook first.", gr.update(), "", None
username = get_username_from_request(request)
try:
out = artifacts_module.generate_podcast(username, notebook_id, extra_instruction=extra or "", strategy=strategy)
if out.get("error"):
return out["error"], gr.update(), "", None
arts = artifacts_module.list_artifacts(username, notebook_id)
podcasts = [a for a in arts if a.get("type") == "podcast"]
choices = [(a.get("filename", "?"), a.get("filename", "")) for a in podcasts]
transcript = out.get("transcript_content", "")
audio_path = artifacts_module.get_podcast_audio_path(username, notebook_id, out["filename"]) if out.get("audio_ok") else None
return (
f"Podcast generated: {out['filename']}" + ("" if out.get("audio_ok") else " (audio failed; transcript saved)"),
gr.Dropdown(choices=choices, value=out["filename"]),
transcript,
audio_path,
)
except Exception as e:
logger.exception("podcast")
return f"Error: {traceback.format_exc()}", gr.update(), "", None
def list_artifacts_handler(notebook_id: str, request: gr.Request) -> Tuple[str, List[Dict]]:
if not notebook_id:
return "Select a notebook.", []
username = get_username_from_request(request)
arts = artifacts_module.list_artifacts(username, notebook_id)
lines = []
for a in arts:
t = a.get("type", "?")
f = a.get("filename", "?")
lines.append(f"- **{t}**: {f}")
return "\n".join(lines) if lines else "No artifacts yet.", arts
def view_report_content(notebook_id: str, filename: str, request: gr.Request) -> str:
if not notebook_id or not filename:
return ""
username = get_username_from_request(request)
return artifacts_module.get_report_content(username, notebook_id, filename)
def view_quiz_content(notebook_id: str, filename: str, request: gr.Request) -> str:
if not notebook_id or not filename:
return ""
username = get_username_from_request(request)
return artifacts_module.get_quiz_content(username, notebook_id, filename)
def view_podcast_content(notebook_id: str, filename: str, request: gr.Request) -> Tuple[str, Any]:
if not notebook_id or not filename:
return "", None
username = get_username_from_request(request)
arts = artifacts_module.list_artifacts(username, notebook_id)
for a in arts:
if a.get("type") == "podcast" and a.get("filename") == filename:
trans_fn = a.get("transcript_filename")
if trans_fn:
transcript = artifacts_module.get_podcast_transcript(username, notebook_id, trans_fn)
else:
transcript = ""
audio_path = artifacts_module.get_podcast_audio_path(username, notebook_id, filename)
return transcript, audio_path
return "", None
def _now_iso() -> str:
from datetime import datetime
return datetime.utcnow().isoformat() + "Z"
# ---------- Professional styling: typography, spacing, refined theme ----------
CUSTOM_CSS = """
/* Base: professional font and smooth rendering */
.gradio-container {
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
}
.gradio-container .block, .gradio-container label, .gradio-container input,
.gradio-container textarea, .gradio-container button, .gradio-container .markdown {
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
}
.gradio-container {
background: #0e0e14 !important;
font-size: 15px !important;
line-height: 1.5 !important;
}
.gradio-container .main {
background: #12121a !important;
max-width: 1400px !important;
margin: 0 auto !important;
}
.gradio-container .block {
background: transparent !important;
border: none !important;
padding: 0 !important;
}
/* Headings */
.gradio-container h1, .gradio-container .markdown h1 {
font-size: 1.5rem !important;
font-weight: 700 !important;
letter-spacing: -0.02em !important;
color: #f0f0f5 !important;
margin-bottom: 0.5rem !important;
}
.gradio-container .markdown p, .gradio-container .markdown {
color: #a0a0b0 !important;
font-size: 0.9375rem !important;
font-weight: 400 !important;
}
/* Labels: subtle, not loud */
.gradio-container label, .gradio-container .label-wrap {
color: #9090a0 !important;
font-size: 0.8125rem !important;
font-weight: 500 !important;
text-transform: none !important;
}
/* Inputs: clean, rounded */
.gradio-container .input, .gradio-container .output,
.gradio-container .textbox textarea, .gradio-container input[type="text"] {
background: #1c1c26 !important;
color: #e8e8f0 !important;
border: 1px solid #2d2d3a !important;
border-radius: 10px !important;
padding: 10px 14px !important;
font-size: 0.9375rem !important;
transition: border-color 0.15s ease, box-shadow 0.15s ease !important;
}
.gradio-container .input:focus-within,
.gradio-container .textbox textarea:focus,
.gradio-container input[type="text"]:focus {
border-color: #6366f1 !important;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15) !important;
outline: none !important;
}
/* Buttons: primary = indigo/violet, refined */
.gradio-container .primary {
background: linear-gradient(180deg, #6366f1 0%, #4f46e5 100%) !important;
color: #fff !important;
border: none !important;
border-radius: 10px !important;
font-weight: 600 !important;
font-size: 0.9375rem !important;
padding: 10px 18px !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.2) !important;
transition: transform 0.05s ease, box-shadow 0.15s ease !important;
}
.gradio-container .primary:hover {
background: linear-gradient(180deg, #5558e3 0%, #4338ca 100%) !important;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.35) !important;
}
.gradio-container button:not(.primary) {
background: #252532 !important;
color: #c0c0d0 !important;
border: 1px solid #2d2d3a !important;
border-radius: 10px !important;
font-weight: 500 !important;
}
.gradio-container button:not(.primary):hover {
background: #2d2d3a !important;
color: #e0e0e8 !important;
}
.gradio-container .stop { background: #2d2020 !important; color: #e8a0a0 !important; border-color: #4a3030 !important; }
.gradio-container .stop:hover { background: #3d2828 !important; }
/* Sidebar: card-like sections */
#sidebar {
background: #16161f !important;
border-radius: 12px !important;
padding: 20px !important;
border: 1px solid #22222e !important;
}
#sidebar .block { background: transparent !important; margin-bottom: 16px !important; }
#sidebar .markdown { color: #a0a0b0 !important; }
#user-display {
font-size: 0.8125rem !important;
color: #707080 !important;
padding: 8px 0 !important;
border-bottom: 1px solid #22222e !important;
margin-bottom: 12px !important;
}
/* Dropdown */
.gradio-container .dropdown, .gradio-container select {
background: #1c1c26 !important;
color: #e8e8f0 !important;
border: 1px solid #2d2d3a !important;
border-radius: 10px !important;
font-size: 0.9375rem !important;
}
/* Tabs: clean pill-style */
.gradio-container .tabs {
margin-bottom: 20px !important;
}
.gradio-container .tabs .tabitem {
background: transparent !important;
padding: 16px 0 !important;
}
.gradio-container .tabs button {
color: #808090 !important;
font-weight: 500 !important;
font-size: 0.9375rem !important;
border-radius: 8px !important;
padding: 8px 16px !important;
margin-right: 4px !important;
border: none !important;
background: transparent !important;
}
.gradio-container .tabs button.selected {
background: #252532 !important;
color: #e0e0e8 !important;
}
/* Chatbot */
.gradio-container .chatbot {
background: #1c1c26 !important;
border: 1px solid #2d2d3a !important;
border-radius: 12px !important;
}
/* File upload area */
.gradio-container .file-preview, .gradio-container .upload {
background: #1c1c26 !important;
border: 2px dashed #2d2d3a !important;
border-radius: 12px !important;
color: #808090 !important;
}
.gradio-container .file-preview:hover, .gradio-container .upload:hover {
border-color: #6366f1 !important;
background: #1e1e2a !important;
}
/* Status / read-only text: subtle, not error-like */
.gradio-container .input[data-type="textbox"] textarea:disabled,
.gradio-container input:disabled {
background: #181822 !important;
color: #9090a0 !important;
border-color: #252532 !important;
}
/* Error message area: only show when needed, subtle */
#chat-error-box {
font-size: 0.8125rem !important;
color: #c08080 !important;
background: #1e1818 !important;
border: 1px solid #3d2828 !important;
border-radius: 8px !important;
padding: 10px 12px !important;
}
#chat-error-box:empty, .hide-when-empty:empty { display: none !important; }
/* Accordion */
.gradio-container .accordion {
background: #1c1c26 !important;
border: 1px solid #2d2d3a !important;
border-radius: 10px !important;
}
"""
# ---------- Build UI ----------
def build_ui():
with gr.Blocks(
title="NotebookLM Clone",
theme=gr.themes.Soft(primary_hue="violet"),
css=CUSTOM_CSS,
head="""
""",
) as demo:
state = gr.State(value=initial_state)
notebook_id_hidden = gr.Textbox(visible=False)
with gr.Row():
# ---- Left sidebar ----
with gr.Column(scale=1, min_width=280, elem_id="sidebar"):
gr.Markdown("# 📓 NotebookLM Clone")
user_display = gr.Markdown(value="*Logged in as: …*", elem_id="user-display")
gr.Markdown("**Select notebook**")
notebook_dropdown = gr.Dropdown(
label="",
choices=[],
value=None,
interactive=True,
)
create_name = gr.Textbox(label="New notebook name", placeholder="e.g. Research notes", scale=2)
create_btn = gr.Button("+ New", variant="primary", scale=1)
gr.Markdown("**Manage notebook**")
rename_name = gr.Textbox(label="Rename to", placeholder="New name", show_label=True)
with gr.Row():
rename_btn = gr.Button("Rename")
delete_btn = gr.Button("Delete", variant="stop")
notebook_status = gr.Textbox(label="Status", interactive=False, visible=True, value="Ready.")
gr.Markdown("---")
gr.Markdown("**Ingested sources**")
sources_md = gr.Markdown(value="No sources yet. Use the **Sources** tab to upload or add a URL, then refresh.")
source_toggle_dropdown = gr.Dropdown(label="Source to toggle", choices=[], value=None)
with gr.Row():
enable_src_btn = gr.Button("Enable", size="sm")
disable_src_btn = gr.Button("Disable", size="sm")
refresh_sources_btn = gr.Button("Refresh sources", size="sm")
# ---- Main content: Tabs ----
with gr.Column(scale=3):
with gr.Tabs() as main_tabs:
# --- Sources tab ---
with gr.TabItem("Sources"):
gr.Markdown("Upload PDF, PPTX, or TXT files, or paste a URL to add content to this notebook.")
file_upload = gr.File(
label="Upload file",
file_types=[".pdf", ".pptx", ".txt"],
)
with gr.Row():
url_input = gr.Textbox(
label="Or paste URL",
placeholder="https://example.com/article",
scale=3,
)
ingest_url_btn = gr.Button("Ingest URL", variant="primary", scale=1)
ingest_btn = gr.Button("Ingest file", variant="primary")
ingest_status = gr.Textbox(label="", interactive=False, value="", show_label=False)
# --- Chat tab ---
with gr.TabItem("Chat"):
strategy_dropdown = gr.Dropdown(
label="Retrieval strategy",
choices=[RETRIEVAL_STRATEGY_SIMILARITY, RETRIEVAL_STRATEGY_MMR],
value=RETRIEVAL_STRATEGY_SIMILARITY,
)
chat = gr.Chatbot(label="", height=420, show_label=False)
chat_msg = gr.Textbox(
label="",
placeholder="Ask about your sources…",
lines=2,
show_label=False,
)
with gr.Row():
send_btn = gr.Button("Send", variant="primary")
clear_chat_btn = gr.Button("Clear chat")
timing_tb = gr.Textbox(label="Response time", interactive=False, value="")
with gr.Accordion("Citations", open=True):
citations_display = gr.Markdown(value="")
chat_error = gr.Textbox(
label="",
interactive=False,
visible=True,
value="",
show_label=False,
elem_id="chat-error-box",
)
# --- Artifacts tab ---
with gr.TabItem("Artifacts"):
artifact_extra = gr.Textbox(
label="Extra instruction (optional)",
placeholder="e.g. Focus on topic X and how it relates to Y",
)
artifact_strategy = gr.Dropdown(
label="Retrieval strategy",
choices=[RETRIEVAL_STRATEGY_SIMILARITY, RETRIEVAL_STRATEGY_MMR],
value=RETRIEVAL_STRATEGY_SIMILARITY,
)
with gr.Tabs() as artifact_tabs:
# Reports
with gr.TabItem("Reports"):
report_btn = gr.Button("Generate report", variant="primary")
report_status = gr.Textbox(label="", interactive=False, value="", show_label=False)
report_files_dropdown = gr.Dropdown(
label="Report files",
choices=[],
value=None,
)
report_content_md = gr.Markdown(
value="*Generate a report or select one above to view.*",
elem_classes=["report-display"],
)
# Quizzes
with gr.TabItem("Quizzes"):
quiz_btn = gr.Button("Generate quiz", variant="primary")
quiz_status = gr.Textbox(label="", interactive=False, value="", show_label=False)
quiz_files_dropdown = gr.Dropdown(
label="Quiz files",
choices=[],
value=None,
)
quiz_content_md = gr.Markdown(
value="*Generate a quiz or select one above to view.*",
)
# Podcasts
with gr.TabItem("Podcasts"):
podcast_btn = gr.Button("Generate podcast (transcript + MP3)", variant="primary")
podcast_status = gr.Textbox(label="", interactive=False, value="", show_label=False)
podcast_files_dropdown = gr.Dropdown(
label="Podcast files",
choices=[],
value=None,
)
podcast_audio = gr.Audio(label="Play podcast", type="filepath")
podcast_transcript_md = gr.Markdown(
value="*Generate a podcast or select one above to view transcript.*",
)
gr.Markdown("### Your artifacts")
artifacts_list_md = gr.Markdown(value="No artifacts yet. Generate a report, quiz, or podcast above.")
artifacts_list_btn = gr.Button("Refresh list")
# ---- Load on mount ----
def on_load(request: gr.Request):
username = get_username_from_request(request)
state_val, nb_id, status, dd = list_notebooks_for_user(request)
user_md = f"**Logged in as:** {username}"
if nb_id:
_, _, chat_hist, cites = switch_notebook_handler(nb_id, request)
return state_val, nb_id, status, dd, chat_hist, cites, user_md
return state_val, nb_id, status, dd, [], "", user_md
demo.load(
fn=on_load,
inputs=[],
outputs=[state, notebook_id_hidden, notebook_status, notebook_dropdown, chat, citations_display, user_display],
)
def on_notebook_select(choice, request: gr.Request):
username = get_username_from_request(request)
notebooks = notebooks_module.list_notebooks(username)
nb_id = ""
for n in notebooks:
if n.get("name") == choice:
nb_id = n.get("id", "")
break
return switch_notebook_handler(nb_id, request)
notebook_dropdown.change(
fn=on_notebook_select,
inputs=[notebook_dropdown],
outputs=[state, notebook_id_hidden, chat, citations_display],
)
create_btn.click(
fn=create_notebook_handler,
inputs=[create_name],
outputs=[state, notebook_id_hidden, notebook_status, notebook_dropdown],
)
delete_btn.click(
fn=delete_notebook_handler,
inputs=[notebook_id_hidden],
outputs=[state, notebook_id_hidden, notebook_status, notebook_dropdown],
)
rename_btn.click(
fn=rename_notebook_handler,
inputs=[notebook_id_hidden, rename_name],
outputs=[notebook_status, notebook_dropdown],
)
ingest_btn.click(
fn=ingest_file_handler,
inputs=[file_upload, notebook_id_hidden],
outputs=[ingest_status],
)
ingest_url_btn.click(
fn=ingest_url_handler,
inputs=[url_input, notebook_id_hidden],
outputs=[ingest_status],
)
url_input.submit(
fn=ingest_url_handler,
inputs=[url_input, notebook_id_hidden],
outputs=[ingest_status],
)
refresh_sources_btn.click(
fn=refresh_sources_handler,
inputs=[notebook_id_hidden],
outputs=[sources_md, notebook_status, source_toggle_dropdown],
)
def do_enable(nid, sid, request: gr.Request):
if not sid:
return "Select a source."
toggle_source_handler(nid, sid, True, request)
return "Enabled."
def do_disable(nid, sid, request: gr.Request):
if not sid:
return "Select a source."
toggle_source_handler(nid, sid, False, request)
return "Disabled."
enable_src_btn.click(
fn=do_enable,
inputs=[notebook_id_hidden, source_toggle_dropdown],
outputs=[notebook_status],
)
disable_src_btn.click(
fn=do_disable,
inputs=[notebook_id_hidden, source_toggle_dropdown],
outputs=[notebook_status],
)
def do_send(msg, hist, nid, strat, request: gr.Request):
return chat_send(msg, hist, nid, strat, request)
send_btn.click(
fn=do_send,
inputs=[chat_msg, chat, notebook_id_hidden, strategy_dropdown],
outputs=[chat, citations_display, chat_msg, timing_tb, chat_error],
)
chat_msg.submit(
fn=do_send,
inputs=[chat_msg, chat, notebook_id_hidden, strategy_dropdown],
outputs=[chat, citations_display, chat_msg, timing_tb, chat_error],
)
clear_chat_btn.click(
fn=clear_chat_handler,
inputs=[notebook_id_hidden],
outputs=[chat, citations_display],
)
# Artifacts: report
def do_report(nid, ex, strat, state: Dict):
username = (state or {}).get("username") or "anonymous"
try:
out = artifacts_module.generate_report(username, nid, extra_instruction=ex or "", strategy=strat)
if out.get("error"):
return out["error"], gr.update(), ""
arts = artifacts_module.list_artifacts(username, nid)
reports = [a for a in arts if a.get("type") == "report"]
choices = [(a.get("filename", "?"), a.get("filename", "")) for a in reports]
timing = out.get("generation_time")
status = f"Report generated: {out['filename']}" + (f" ({timing:.1f}s)" if timing is not None else "")
return status, gr.Dropdown(choices=choices, value=out["filename"]), out["content"]
except Exception as e:
return f"Error: {traceback.format_exc()}", gr.update(), ""
report_btn.click(
fn=do_report,
inputs=[notebook_id_hidden, artifact_extra, artifact_strategy, state],
outputs=[report_status, report_files_dropdown, report_content_md],
)
def view_report_with_req(nid, fname, request: gr.Request):
return view_report_content(nid, fname, request)
report_files_dropdown.change(
fn=view_report_with_req,
inputs=[notebook_id_hidden, report_files_dropdown],
outputs=[report_content_md],
)
def do_quiz(nid, ex, strat, state: Dict):
username = (state or {}).get("username") or "anonymous"
try:
out = artifacts_module.generate_quiz(username, nid, extra_instruction=ex or "", strategy=strat)
if out.get("error"):
return out["error"], gr.update(), ""
arts = artifacts_module.list_artifacts(username, nid)
quizzes = [a for a in arts if a.get("type") == "quiz"]
choices = [(a.get("filename", "?"), a.get("filename", "")) for a in quizzes]
timing = out.get("generation_time")
status = f"Quiz generated: {out['filename']}" + (f" ({timing:.1f}s)" if timing is not None else "")
return status, gr.Dropdown(choices=choices, value=out["filename"]), out["content"]
except Exception as e:
return f"Error: {traceback.format_exc()}", gr.update(), ""
quiz_btn.click(
fn=do_quiz,
inputs=[notebook_id_hidden, artifact_extra, artifact_strategy, state],
outputs=[quiz_status, quiz_files_dropdown, quiz_content_md],
)
def view_quiz_with_req(nid, fname, request: gr.Request):
return view_quiz_content(nid, fname, request)
quiz_files_dropdown.change(
fn=view_quiz_with_req,
inputs=[notebook_id_hidden, quiz_files_dropdown],
outputs=[quiz_content_md],
)
def do_podcast(nid, ex, strat, state: Dict):
username = (state or {}).get("username") or "anonymous"
try:
out = artifacts_module.generate_podcast(username, nid, extra_instruction=ex or "", strategy=strat)
if out.get("error"):
return out["error"], gr.update(), "", None
arts = artifacts_module.list_artifacts(username, nid)
podcasts = [a for a in arts if a.get("type") == "podcast"]
choices = [(a.get("filename", "?"), a.get("filename", "")) for a in podcasts]
transcript = out.get("transcript_content", "")
audio_path = artifacts_module.get_podcast_audio_path(username, nid, out["filename"]) if out.get("audio_ok") else None
timing = out.get("generation_time")
msg = f"Podcast generated: {out['filename']}" + (f" ({timing:.1f}s)" if timing is not None else "")
if not out.get("audio_ok"):
msg += " (audio failed; transcript saved)"
return msg, gr.Dropdown(choices=choices, value=out["filename"]), transcript, audio_path
except Exception as e:
return f"Error: {traceback.format_exc()}", gr.update(), "", None
podcast_btn.click(
fn=do_podcast,
inputs=[notebook_id_hidden, artifact_extra, artifact_strategy, state],
outputs=[podcast_status, podcast_files_dropdown, podcast_transcript_md, podcast_audio],
)
def view_podcast_with_req(nid, fname, request: gr.Request):
return view_podcast_content(nid, fname, request)
podcast_files_dropdown.change(
fn=view_podcast_with_req,
inputs=[notebook_id_hidden, podcast_files_dropdown],
outputs=[podcast_transcript_md, podcast_audio],
)
def refresh_artifacts(nid, request: gr.Request):
md, _ = list_artifacts_handler(nid, request)
return md
artifacts_list_btn.click(
fn=refresh_artifacts,
inputs=[notebook_id_hidden],
outputs=[artifacts_list_md],
)
return demo
# For "gradio run app.py" or HF Space
demo = build_ui()
if __name__ == "__main__":
import os
if not os.environ.get("MOCK_USER"):
os.environ.setdefault("MOCK_USER", "localuser")
demo.launch(server_name="0.0.0.0", server_port=7860)