from pathlib import Path import shutil import uuid from dotenv import load_dotenv # Load env vars (e.g., GROQ_API_KEY) from project .env reliably. load_dotenv(Path(__file__).with_name(".env")) import gradio as gr from datetime import datetime from components.Header import Header from components.ManageNotebook import ManageNotebook from components.ChatInterface import ChatInterface from config.settings import settings from notebook_store import ( append_source_event, create_notebook, ensure_default_notebooks, ensure_notebook_layout, list_source_events, notebook_dir, ) from rag import ( extract_plain_text_from_file, extract_plain_text_from_url, generate_quiz, generate_report as rag_generate_report, ingest_file_for_rag, ingest_url_for_rag, ) def _notebook_choices(notebooks: list[dict]) -> list[tuple[str, str]]: return [(n["name"], n["id"]) for n in notebooks] def _sources_table(notebook_id: str) -> list[list[str]]: events = list_source_events(notebook_id) rows: list[list[str]] = [] for e in events: kind = e.get("kind") if kind == "file": rows.append([str(e.get("original_filename") or e.get("source_name") or "")]) elif kind == "url": rows.append([str(e.get("url") or e.get("source_name") or "")]) return [r for r in rows if r and r[0]] def _chroma_dir(notebook_id: str) -> str: ensure_notebook_layout(notebook_id) return str(notebook_dir(notebook_id) / "chroma_db") def _save_notebook_markdown(*, notebook_id: str, subdir: str, prefix: str, content: str) -> Path: nb = ensure_notebook_layout(notebook_id) target_dir = nb / subdir target_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now().strftime("%Y%m%d-%H%M%S") path = target_dir / f"{prefix}-{ts}.md" path.write_text(content, encoding="utf-8") return path def generate_report(notebook_id: str, existing_files): """ Generate a report from the ingested sources using RAG, save it, and return preview + file list. """ notebook_id = (notebook_id or "").strip() if not notebook_id: md = "## No notebook selected\nPlease select a notebook first." return existing_files, md, "No report generated (no notebook selected)." report_md = rag_generate_report( persist_directory=_chroma_dir(notebook_id), title="Report", focus_prompt="", ) # Don't save error reports if report_md.startswith("No sources") or report_md.startswith("Unable to retrieve"): return existing_files, report_md, "Report could not be generated. Upload/ingest sources first." files = list(existing_files) if existing_files else [] report_path = _save_notebook_markdown( notebook_id=notebook_id, subdir="reports", prefix="report", content=report_md ) report_name = report_path.name if report_name not in files: files = [report_name] + files status = f"Report generated: {report_name}" return files, report_md, status def add_source(file_obj, notebook_id: str): """ Persist an uploaded file into the selected notebook, extract text, ingest into the notebook's vector store, and return rows for the sources table. """ if file_obj is not None: # Gradio File component provides a temporary local path via .name. tmp_path = getattr(file_obj, "name", None) if tmp_path: notebook_id = (notebook_id or "").strip() ensure_notebook_layout(notebook_id) source_id = uuid.uuid4().hex original = str(tmp_path).split("/")[-1].split("\\")[-1] or "uploaded_file" raw_dir = notebook_dir(notebook_id) / "sources" / "raw" / source_id raw_dir.mkdir(parents=True, exist_ok=True) raw_path = raw_dir / original shutil.copy2(tmp_path, raw_path) extracted = extract_plain_text_from_file(str(raw_path)) text_path = notebook_dir(notebook_id) / "sources" / "text" / f"{source_id}.txt" text_path.write_text(extracted, encoding="utf-8") chunks = ingest_file_for_rag( str(raw_path), persist_directory=_chroma_dir(notebook_id), source_name=original, ) append_source_event( notebook_id, { "kind": "file", "source_id": source_id, "original_filename": original, "raw_path": str(raw_path), "text_path": str(text_path), "chunks_added": chunks, "source_name": original, }, ) return _sources_table(notebook_id) def add_url_source(url, notebook_id: str): """ Ingest a URL into the RAG vector store and update the sources table. """ url = (url or "").strip() if not url: return _sources_table(notebook_id), "" notebook_id = (notebook_id or "").strip() ensure_notebook_layout(notebook_id) source_id = uuid.uuid4().hex raw_dir = notebook_dir(notebook_id) / "sources" / "raw" / source_id raw_dir.mkdir(parents=True, exist_ok=True) raw_path = raw_dir / "url.txt" raw_path.write_text(url + "\n", encoding="utf-8") extracted = extract_plain_text_from_url(url) text_path = notebook_dir(notebook_id) / "sources" / "text" / f"{source_id}.txt" text_path.write_text(extracted, encoding="utf-8") chunks = ingest_url_for_rag( url, persist_directory=_chroma_dir(notebook_id), source_name=url ) append_source_event( notebook_id, { "kind": "url", "source_id": source_id, "url": url, "raw_path": str(raw_path), "text_path": str(text_path), "chunks_added": chunks, "source_name": url, }, ) return _sources_table(notebook_id), "" def add_notebook(name, notebooks): name = (name or "").strip() if not name: return gr.update(), notebooks, "", gr.update(), [] notebooks = list(notebooks or []) existing_names = {str(n.get("name")) for n in notebooks} if name in existing_names: return gr.update(), notebooks, "", gr.update(), _sources_table(notebooks[0]["id"]) if notebooks else [] meta = create_notebook(name) notebooks.append({"id": meta.id, "name": meta.name}) notebooks.sort(key=lambda n: str(n.get("name", "")).lower()) return ( gr.update(choices=_notebook_choices(notebooks), value=meta.id), notebooks, "", meta.id, _sources_table(meta.id), ) def select_notebook(notebook_id: str): notebook_id = (notebook_id or "").strip() return notebook_id, _sources_table(notebook_id) def _gen(notebook_id, files): updated_files, md, status_text = generate_report(notebook_id, files) return ( updated_files, [[f] for f in updated_files], md, status_text ) def _gen_quiz(num_questions: int, notebook_id: str): """ Generate a quiz from the ingested sources using the RAG pipeline. """ quiz_markdown = generate_quiz( persist_directory=_chroma_dir(notebook_id), num_questions=num_questions ) is_error = quiz_markdown.startswith("No sources") or quiz_markdown.startswith( "Unable to retrieve" ) if is_error: return quiz_markdown, "Quiz could not be generated. See message above.", None quiz_path = _save_notebook_markdown( notebook_id=notebook_id, subdir="quizzes", prefix="quiz", content=quiz_markdown ) status = f"Quiz generated successfully. Saved as `{quiz_path.name}`." return quiz_markdown, status, str(quiz_path) DEFAULT_NOTEBOOKS = ["Notebook 1", "Notebook 2", "Notebook 3"] _boot = ensure_default_notebooks(DEFAULT_NOTEBOOKS) _notebooks_init = [{"id": n.id, "name": n.name} for n in _boot] _initial_id = _notebooks_init[0]["id"] if _notebooks_init else create_notebook("Notebook 1").id with gr.Blocks(title=settings.APP_TITLE) as demo: notebooks_state = gr.State(_notebooks_init) notebook_id_state = gr.State(_initial_id) Header() gr.Markdown("---") # Main layout: left sidebar + main content with gr.Row(): # LEFT SIDEBAR with gr.Column(scale=3, min_width=280): with gr.Group(elem_classes=["section-card"]): gr.Markdown("### Notebooks", elem_classes=["section-title"]) notebook = gr.Dropdown( choices=_notebook_choices(_notebooks_init), value=_initial_id, label="Select Notebook", interactive=True, ) new_name = gr.Textbox(placeholder="New notebook name", label="", interactive=True) add_nb = gr.Button("+ New", variant="primary") with gr.Group(elem_classes=["section-card"]): gr.Markdown("### Ingested Sources", elem_classes=["section-title"]) ingested_list = gr.Dataframe( headers=["Sources"], value=_sources_table(_initial_id), datatype=["str"], interactive=False, row_count=(1, "dynamic"), column_count=(1, "fixed"), elem_classes=["sources-list"], ) with gr.Group(elem_classes=["section-card"]): ManageNotebook(notebook, notebooks_state, notebook_id_state, ingested_list) # MAIN PANEL with gr.Column(scale=9, min_width=640): # Tabs: Artifacts first so it's the default view with gr.Tabs(): with gr.Tab("Sources"): upload = gr.File(label="Upload PDF, PPTX, or TXT", file_types=[".pdf", ".pptx", ".txt"], file_count="single", interactive=True) ingest_url = gr.Textbox(label="Ingest URL", placeholder="https://example.com/article", interactive=True) ingest_btn = gr.Button("Ingest URL", variant="primary") with gr.Tab("Artifacts"): with gr.Tabs(): with gr.Tab("Reports"): with gr.Group(elem_classes=["section-card"]): with gr.Row(): gen = gr.Button("Generate Report", variant="primary", elem_classes=["bigbtn"]) status = gr.Markdown("") with gr.Group(elem_classes=["section-card"]): gr.Markdown("### Report Files", elem_classes=["section-title"]) report_files = gr.Dataframe( headers=["Files"], value=[], datatype=["str"], interactive=False, row_count=(1, "dynamic"), column_count=(1, "fixed"), elem_classes=["report-files-list"], ) files_state = gr.State([]) with gr.Group(elem_classes=["section-card"]): report_preview = gr.Markdown("Generate a report to see it here.", elem_classes=["section-title"]) with gr.Tab("Quizzes"): with gr.Group(elem_classes=["section-card"]): gr.Markdown("### Generate Quiz from Sources", elem_classes=["section-title"]) num_questions = gr.Slider( minimum=1, maximum=20, value=5, step=1, label="Number of questions", ) gen_quiz_btn = gr.Button( "Generate Quiz", variant="primary", elem_classes=["bigbtn"], ) quiz_status = gr.Markdown("") with gr.Group(elem_classes=["section-card"]): gr.Markdown("### Quiz", elem_classes=["section-title"]) quiz_output = gr.Markdown( "Generate a quiz to see it here.", elem_classes=["section-title"] ) quiz_download = gr.File( label="Download quiz (.md)", interactive=False, ) with gr.Tab("Podcasts"): gr.Markdown("## Podcasts\n(Stub) Generate podcast scripts/audio from sources.", elem_classes=["section-title"]) with gr.Tab("Chat"): ChatInterface(notebook_id_state) # --- Wire up interactions --- add_nb.click( add_notebook, inputs=[new_name, notebooks_state], outputs=[notebook, notebooks_state, new_name, notebook_id_state, ingested_list], ) notebook.change( fn=select_notebook, inputs=[notebook], outputs=[notebook_id_state, ingested_list], ) # Upload source -> persist + ingest into selected notebook upload.change( fn=add_source, inputs=[upload, notebook_id_state], outputs=[ingested_list], ) ingest_btn.click( fn=add_url_source, inputs=[ingest_url, notebook_id_state], outputs=[ingested_list, ingest_url], ) gen.click( _gen, inputs=[notebook_id_state, files_state], outputs=[files_state, report_files, report_preview, status], ) gen_quiz_btn.click( _gen_quiz, inputs=[num_questions, notebook_id_state], outputs=[quiz_output, quiz_status, quiz_download], ) if __name__ == "__main__": demo.launch(css_paths=["./styles/styles.css"])