"""Artifacts tab: Summary, Podcast, and Quiz generation.""" import os import tempfile from datetime import datetime from state import UserData, get_active_notebook, get_all_artifacts, get_latest_artifact def _save_artifact_to_temp_file(artifact) -> str | None: """Save artifact content to a temporary file. Returns the file path.""" try: if artifact.type == "podcast" and artifact.audio_path: if os.path.exists(artifact.audio_path): return artifact.audio_path else: safe_title = artifact.title.replace(" ", "_")[:50] suffix = '.html' if artifact.type == "quiz" else '.md' with tempfile.NamedTemporaryFile( mode='w', suffix=suffix, prefix=f'{safe_title}_', delete=False, encoding='utf-8' ) as f: f.write(artifact.content) return f.name except Exception as e: import logging logging.error(f"Failed to save artifact: {e}") return None def _format_time(iso_str: str) -> str: try: dt = datetime.fromisoformat(iso_str) return dt.strftime("%b %d at %H:%M") except (ValueError, KeyError): return "" def _render_artifact_content(artifact) -> str: """Render a single artifact as HTML with metadata.""" time_str = _format_time(artifact.created_at) html = ( f'
' f'Generated {time_str}
' ) html += ( f'
' ) html += f'
{artifact.content}
' html += '
' if artifact.type == "podcast": if artifact.audio_path: html += ( f'
' f'
🎧 Podcast Audio
' f'' f'
' ) else: html += ( '
' '🔇' 'Audio generation failed or TTS is unavailable.' '
' ) return html def _render_history(artifacts: list, label: str) -> str: if len(artifacts) <= 1: return "" html = f'
Previous {label} ({len(artifacts) - 1})' for a in artifacts[1:]: time_str = _format_time(a.created_at) html += f'
' html += f'{a.title} — {time_str}' html += f'
{a.content}
' html += '
' html += '
' return html # ── No-sources gate ────────────────────────────────────────────────────────── def render_no_sources_gate(state: UserData) -> str: nb = get_active_notebook(state) if not nb or not nb.sources: return ( '
' '
🎯
' '

Add sources first

' '

Upload documents in the Sources tab to unlock ' 'summary, quiz, and podcast generation.

' '
' ) return "" def has_sources(state: UserData) -> bool: nb = get_active_notebook(state) return nb is not None and len(nb.sources) > 0 # ── Conversation Summary ───────────────────────────────────────────────────── def render_conv_summary_section(state: UserData) -> str: nb = get_active_notebook(state) if not nb: return "" if not nb.messages: return "*No conversation yet. Start chatting in the **Chat** tab first.*" summaries = get_all_artifacts(nb, "conversation_summary") if not summaries: return "*Click Generate to create a conversation summary.*" latest = summaries[0] time_str = _format_time(latest.created_at) md = "*Generated " + time_str + "*\n\n---\n\n" + latest.content if len(summaries) > 1: prev = ", ".join(_format_time(a.created_at) for a in summaries[1:]) md += "\n\n---\n\n**Previous summaries:** " + prev return md def handle_gen_conv_summary(style: str, state: UserData) -> tuple[UserData, str]: nb = get_active_notebook(state) if not nb or not nb.messages: return state, render_conv_summary_section(state) from services.summary_service import generate_conversation_summary artifact = generate_conversation_summary(nb, style or "detailed") nb.artifacts.append(artifact) return state, render_conv_summary_section(state) # ── Document Summary ───────────────────────────────────────────────────────── def render_doc_summary_section(state: UserData) -> str: nb = get_active_notebook(state) if not nb: return "" summaries = get_all_artifacts(nb, "document_summary") if not summaries: return "*Click Generate to create a document summary.*" latest = summaries[0] time_str = _format_time(latest.created_at) md = "*Generated " + time_str + "*\n\n---\n\n" + latest.content if len(summaries) > 1: prev = ", ".join(_format_time(a.created_at) for a in summaries[1:]) md += "\n\n---\n\n**Previous summaries:** " + prev return md def handle_gen_doc_summary(style: str, selected_sources: list[str] | None, state: UserData) -> tuple[UserData, str]: nb = get_active_notebook(state) if not nb: return state, render_doc_summary_section(state) from services.summary_service import generate_document_summary source_ids = [s.id for s in nb.sources if s.filename in (selected_sources or [])] artifact = generate_document_summary(nb, style or "detailed", source_ids=source_ids or None) nb.artifacts.append(artifact) return state, render_doc_summary_section(state) # ── Podcast ────────────────────────────────────────────────────────────────── def has_any_summary(state: UserData) -> bool: nb = get_active_notebook(state) if not nb: return False return ( get_latest_artifact(nb, "document_summary") is not None or get_latest_artifact(nb, "conversation_summary") is not None ) def render_podcast_locked() -> str: return ( '
' '
🔒
' '

Summary Required

' '

' 'Generate a summary first in the Summary tab.
' 'The podcast is created from your summary to ensure accuracy.

' '
' '📝 Summary  →  🎙️ Podcast
' '
' ) def render_podcast_section(state: UserData) -> str: nb = get_active_notebook(state) if not nb: return "" if not has_any_summary(state): return render_podcast_locked() latest_doc = get_latest_artifact(nb, "document_summary") latest_conv = get_latest_artifact(nb, "conversation_summary") latest = latest_doc or latest_conv sum_title = latest.title sum_time = _format_time(latest.created_at) html = ( f'
' f'📝' f'
Based on: ' f'{sum_title}' f' ({sum_time})
' f'
' ) podcasts = get_all_artifacts(nb, "podcast") if podcasts: html += _render_artifact_content(podcasts[0]) html += _render_history(podcasts, "podcasts") else: html += ( '
' '
🎙️
' '

No podcast generated yet.
Click Generate Podcast to create one.

' '
' ) return html def handle_gen_podcast(state: UserData) -> tuple[UserData, str]: """Generate a podcast using Claude script + HF TTS audio.""" from services.podcast_service import generate_podcast nb = get_active_notebook(state) if not nb or not has_any_summary(state): return state, render_podcast_section(state) latest_doc = get_latest_artifact(nb, "document_summary") latest_conv = get_latest_artifact(nb, "conversation_summary") latest_summary = latest_doc or latest_conv artifact = generate_podcast(nb, latest_summary.content) nb.artifacts.append(artifact) return state, render_podcast_section(state) # ── Quiz ───────────────────────────────────────────────────────────────────── def render_quiz_section(state: UserData) -> str: nb = get_active_notebook(state) if not nb: return "" quizzes = get_all_artifacts(nb, "quiz") if not quizzes: return ( '
' '
' '

No quiz generated yet.
Choose the number of questions and click Generate Quiz.

' '
' ) html = _render_artifact_content(quizzes[0]) html += _render_history(quizzes, "quizzes") return html def handle_gen_quiz(num_questions: int, state: UserData) -> tuple[UserData, str]: """Generate an interactive quiz using Claude.""" from services.quiz_service import generate_quiz nb = get_active_notebook(state) if not nb: return state, render_quiz_section(state) num_q = int(num_questions) if num_questions else 5 artifact = generate_quiz(nb, num_q) nb.artifacts.append(artifact) return state, render_quiz_section(state) # ── Download Handlers ──────────────────────────────────────────────────────── def _find_artifact_by_id(state: UserData, artifact_id: str): """Find an artifact by ID in the active notebook.""" nb = get_active_notebook(state) if not nb: return None for artifact in nb.artifacts: if artifact.id == artifact_id: return artifact return None def download_artifact(artifact_id: str, state: UserData) -> str | None: """Download an artifact and return the file path for Gradio download.""" artifact = _find_artifact_by_id(state, artifact_id) if not artifact: return None return _save_artifact_to_temp_file(artifact)