"""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)