File size: 13,079 Bytes
a21bcde
e90d887
cc25091
 
e90d887
 
 
 
cc25091
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e90d887
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a21bcde
 
 
 
 
 
 
 
cc25091
 
a21bcde
 
 
 
 
 
 
 
 
 
220d268
a21bcde
 
e90d887
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220d268
e90d887
 
220d268
 
 
 
 
 
 
 
cc25091
 
e90d887
 
 
 
47377b6
 
e90d887
 
 
 
 
 
 
 
 
 
 
 
220d268
 
 
 
 
 
 
 
cc25091
 
690fe5e
e90d887
 
 
47377b6
690fe5e
 
e90d887
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220d268
a21bcde
 
e90d887
 
 
a21bcde
 
 
 
 
 
e90d887
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220d268
a21bcde
 
e90d887
 
 
a21bcde
e90d887
a21bcde
e90d887
cc25091
 
 
 
 
 
220d268
cc25091
 
 
 
 
 
 
 
 
 
 
 
 
 
220d268
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
"""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'<div style="padding:4px 0 8px 0; font-size:0.8rem; color:#707088;">'
        f'Generated {time_str}</div>'
    )
    html += (
        f'<div style="max-height:400px; overflow-y:auto; padding:16px; '
        f'background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.08); '
        f'border-radius:14px;">'
    )
    html += f'<div class="artifact-content">{artifact.content}</div>'
    html += '</div>'

    if artifact.type == "podcast":
        if artifact.audio_path:
            html += (
                f'<div style="margin-top:12px; padding:14px 16px; '
                f'background:rgba(102,126,234,0.06); border:1px solid rgba(102,126,234,0.15); '
                f'border-radius:10px;">'
                f'<div style="font-size:0.82rem; color:#8090d0; margin-bottom:8px;">🎧 Podcast Audio</div>'
                f'<audio controls style="width:100%; border-radius:6px;">'
                f'<source src="/gradio_api/file={artifact.audio_path}" type="audio/mpeg">'
                f'<source src="/file={artifact.audio_path}" type="audio/mpeg">'
                f'Your browser does not support the audio element.'
                f'</audio>'
                f'</div>'
            )
        else:
            html += (
                '<div style="display:flex; align-items:center; gap:10px; padding:12px 16px; '
                'background:rgba(102,126,234,0.06); border:1px solid rgba(102,126,234,0.15); '
                'border-radius:10px; margin-top:8px;">'
                '<span style="font-size:1.3rem;">πŸ”‡</span>'
                '<span style="font-size:0.85rem; color:#8888aa;">Audio generation failed or TTS is unavailable.</span>'
                '</div>'
            )
    return html


def _render_history(artifacts: list, label: str) -> str:
    if len(artifacts) <= 1:
        return ""
    html = f'<details><summary style="cursor:pointer; color:#a0a0b8; font-size:0.85rem; margin-top:12px;">Previous {label} ({len(artifacts) - 1})</summary>'
    for a in artifacts[1:]:
        time_str = _format_time(a.created_at)
        html += f'<div style="margin-top:12px; padding:12px; border:1px solid rgba(255,255,255,0.06); border-radius:10px;">'
        html += f'<strong>{a.title}</strong> β€” {time_str}'
        html += f'<div style="max-height:200px; overflow-y:auto; margin-top:8px; font-size:0.85rem;">{a.content}</div>'
        html += '</div>'
    html += '</details>'
    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 (
            '<div class="empty-state">'
            '<div style="font-size:3rem; margin-bottom:16px;">🎯</div>'
            '<h3>Add sources first</h3>'
            '<p>Upload documents in the <strong>Sources</strong> tab to unlock '
            'summary, quiz, and podcast generation.</p>'
            '</div>'
        )
    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 (
        '<div class="locked-state">'
        '<div style="font-size:2.5rem; margin-bottom:16px;">πŸ”’</div>'
        '<h3 style="color:#a0a0b8; font-weight:600; margin-bottom:8px;">Summary Required</h3>'
        '<p style="color:#707088; font-size:0.9rem; line-height:1.6;">'
        'Generate a summary first in the <strong>Summary</strong> tab.<br>'
        'The podcast is created from your summary to ensure accuracy.</p>'
        '<div style="margin-top:20px; display:inline-flex; align-items:center; gap:8px; '
        'padding:8px 18px; background:rgba(102,126,234,0.08); '
        'border:1px solid rgba(102,126,234,0.15); border-radius:20px; '
        'font-size:0.82rem; color:#8090d0;">'
        'πŸ“ Summary &nbsp;β†’&nbsp; πŸŽ™οΈ Podcast</div>'
        '</div>'
    )


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'<div style="display:flex; align-items:center; gap:12px; padding:12px 18px; '
        f'background:rgba(34,197,94,0.06); border:1px solid rgba(34,197,94,0.15); '
        f'border-radius:12px; margin-bottom:16px;">'
        f'<span style="font-size:1.2rem;">πŸ“</span>'
        f'<div><span style="font-size:0.85rem; color:#a0b8a0;">Based on: </span>'
        f'<strong style="color:#c0e0c0;">{sum_title}</strong>'
        f'<span style="color:#708070; font-size:0.8rem;"> ({sum_time})</span></div>'
        f'</div>'
    )

    podcasts = get_all_artifacts(nb, "podcast")
    if podcasts:
        html += _render_artifact_content(podcasts[0])
        html += _render_history(podcasts, "podcasts")
    else:
        html += (
            '<div style="text-align:center; padding:40px 20px; color:#606078; margin-top:16px; '
            'border:1px dashed rgba(255,255,255,0.08); border-radius:14px;">'
            '<div style="font-size:2rem; margin-bottom:10px;">πŸŽ™οΈ</div>'
            '<p>No podcast generated yet.<br>Click <strong>Generate Podcast</strong> to create one.</p>'
            '</div>'
        )
    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 (
            '<div style="text-align:center; padding:40px 20px; color:#606078; margin-top:16px; '
            'border:1px dashed rgba(255,255,255,0.08); border-radius:14px;">'
            '<div style="font-size:2rem; margin-bottom:10px;">❓</div>'
            '<p>No quiz generated yet.<br>Choose the number of questions and click <strong>Generate Quiz</strong>.</p>'
            '</div>'
        )
    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)