Max Saavedra commited on
Commit
6697007
·
1 Parent(s): 771e8b6

Flash cards artifact added.

Browse files
backend/api/artifacts.py CHANGED
@@ -3,8 +3,9 @@ from fastapi.responses import FileResponse
3
 
4
  from backend.models.schemas import ArtifactGenerateOut, ArtifactGenerateRequest, ArtifactListOut
5
  from backend.modules.artifacts import (
6
- generate_podcast,
7
- generate_quiz,
 
8
  generate_report,
9
  list_artifacts,
10
  resolve_artifact_path,
@@ -72,8 +73,31 @@ def create_quiz_artifact(
72
  raise HTTPException(status_code=404, detail="Notebook not found")
73
  except ValueError as exc:
74
  raise HTTPException(status_code=400, detail=str(exc)) from exc
75
- except Exception as exc:
76
- raise HTTPException(status_code=500, detail=f"Quiz generation failed: {exc}") from exc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
 
79
  @router.post("/{notebook_id}/artifacts/podcast", response_model=ArtifactGenerateOut)
 
3
 
4
  from backend.models.schemas import ArtifactGenerateOut, ArtifactGenerateRequest, ArtifactListOut
5
  from backend.modules.artifacts import (
6
+ generate_flashcards,
7
+ generate_podcast,
8
+ generate_quiz,
9
  generate_report,
10
  list_artifacts,
11
  resolve_artifact_path,
 
73
  raise HTTPException(status_code=404, detail="Notebook not found")
74
  except ValueError as exc:
75
  raise HTTPException(status_code=400, detail=str(exc)) from exc
76
+ except Exception as exc:
77
+ raise HTTPException(status_code=500, detail=f"Quiz generation failed: {exc}") from exc
78
+
79
+
80
+ @router.post("/{notebook_id}/artifacts/flashcards", response_model=ArtifactGenerateOut)
81
+ def create_flashcards_artifact(
82
+ notebook_id: str,
83
+ payload: ArtifactGenerateRequest,
84
+ current_user: User = Depends(get_current_user),
85
+ ) -> ArtifactGenerateOut:
86
+ try:
87
+ enforce_user_match(current_user, payload.user_id)
88
+ return generate_flashcards(
89
+ store,
90
+ user_id=current_user.user_id,
91
+ notebook_id=notebook_id,
92
+ prompt=payload.prompt,
93
+ num_questions=payload.num_questions,
94
+ )
95
+ except FileNotFoundError:
96
+ raise HTTPException(status_code=404, detail="Notebook not found")
97
+ except ValueError as exc:
98
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
99
+ except Exception as exc:
100
+ raise HTTPException(status_code=500, detail=f"Flashcards generation failed: {exc}") from exc
101
 
102
 
103
  @router.post("/{notebook_id}/artifacts/podcast", response_model=ArtifactGenerateOut)
backend/models/schemas.py CHANGED
@@ -99,6 +99,7 @@ class PodcastArtifactOut(BaseModel):
99
  class ArtifactListOut(BaseModel):
100
  reports: List[ArtifactFileOut]
101
  quizzes: List[ArtifactFileOut]
 
102
  podcasts: List[PodcastArtifactOut]
103
 
104
 
 
99
  class ArtifactListOut(BaseModel):
100
  reports: List[ArtifactFileOut]
101
  quizzes: List[ArtifactFileOut]
102
+ flashcards: List[ArtifactFileOut]
103
  podcasts: List[PodcastArtifactOut]
104
 
105
 
backend/modules/artifacts.py CHANGED
@@ -59,11 +59,12 @@ def _now() -> str:
59
  def _artifact_dirs(store: NotebookStore, user_id: str, notebook_id: str) -> Dict[str, Path]:
60
  notebook_dir = store.require_notebook_dir(user_id, notebook_id)
61
  root = notebook_dir / "artifacts"
62
- dirs = {
63
- "report": root / "reports",
64
- "quiz": root / "quizzes",
65
- "podcast": root / "podcasts",
66
- }
 
67
  for d in dirs.values():
68
  d.mkdir(parents=True, exist_ok=True)
69
  return dirs
@@ -167,7 +168,7 @@ def _quiz_fallback(sources: List[Dict[str, str]], num_questions: int) -> str:
167
  )
168
 
169
 
170
- def _podcast_transcript_fallback(sources: List[Dict[str, str]], extra_prompt: Optional[str]) -> str:
171
  focus = extra_prompt.strip() if extra_prompt else "core concepts from the notebook"
172
  lines = [
173
  "# Podcast Transcript",
@@ -182,7 +183,20 @@ def _podcast_transcript_fallback(sources: List[Dict[str, str]], extra_prompt: Op
182
  lines.append(f"**Host:** Great, and why does that matter in practice?")
183
  lines.append("**Co-Host:** That wraps the study summary. Review the report and quiz next.")
184
  lines.append("")
185
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
 
188
  def _encode_pcm_to_mp3(pcm_16le: bytes, sample_rate: int, channels: int) -> bytes:
@@ -286,7 +300,7 @@ def generate_report(
286
  )
287
 
288
 
289
- def generate_quiz(
290
  store: NotebookStore,
291
  *,
292
  user_id: str,
@@ -310,13 +324,47 @@ def generate_quiz(
310
  )
311
  content = _llm_or_fallback(llm_prompt, _quiz_fallback(sources, questions))
312
  out_path = _write_markdown_artifact(dirs["quiz"], "quiz", content)
313
- return ArtifactGenerateOut(
314
- artifact_type="quiz",
315
  message=f"Generated {out_path.name}",
316
  markdown_path=str(out_path.as_posix()),
317
  audio_path=None,
318
- created_at=_now(),
319
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
 
322
  def generate_podcast(
@@ -361,8 +409,9 @@ def generate_podcast(
361
  def list_artifacts(store: NotebookStore, *, user_id: str, notebook_id: str) -> ArtifactListOut:
362
  dirs = _artifact_dirs(store, user_id, notebook_id)
363
 
364
- reports = [_artifact_file_out(p) for p in sorted(dirs["report"].glob("report_*.md"))]
365
- quizzes = [_artifact_file_out(p) for p in sorted(dirs["quiz"].glob("quiz_*.md"))]
 
366
 
367
  podcast_indices: set[int] = set()
368
  for path in dirs["podcast"].glob("podcast_*.*"):
@@ -381,7 +430,7 @@ def list_artifacts(store: NotebookStore, *, user_id: str, notebook_id: str) -> A
381
  )
382
  )
383
 
384
- return ArtifactListOut(reports=reports, quizzes=quizzes, podcasts=podcasts)
385
 
386
 
387
  def resolve_artifact_path(
 
59
  def _artifact_dirs(store: NotebookStore, user_id: str, notebook_id: str) -> Dict[str, Path]:
60
  notebook_dir = store.require_notebook_dir(user_id, notebook_id)
61
  root = notebook_dir / "artifacts"
62
+ dirs = {
63
+ "report": root / "reports",
64
+ "quiz": root / "quizzes",
65
+ "flashcards": root / "flashcards",
66
+ "podcast": root / "podcasts",
67
+ }
68
  for d in dirs.values():
69
  d.mkdir(parents=True, exist_ok=True)
70
  return dirs
 
168
  )
169
 
170
 
171
+ def _podcast_transcript_fallback(sources: List[Dict[str, str]], extra_prompt: Optional[str]) -> str:
172
  focus = extra_prompt.strip() if extra_prompt else "core concepts from the notebook"
173
  lines = [
174
  "# Podcast Transcript",
 
183
  lines.append(f"**Host:** Great, and why does that matter in practice?")
184
  lines.append("**Co-Host:** That wraps the study summary. Review the report and quiz next.")
185
  lines.append("")
186
+ return "\n".join(lines)
187
+
188
+
189
+ def _flashcards_fallback(sources: List[Dict[str, str]], num_cards: int) -> str:
190
+ cards = max(3, min(20, int(num_cards)))
191
+ lines = ["# Flashcards", ""]
192
+ for i in range(1, cards + 1):
193
+ src = sources[(i - 1) % len(sources)]
194
+ snippet = src["text"].replace("\n", " ")[:200].strip()
195
+ lines.append(f"## Card {i}")
196
+ lines.append(f"Q: What key point appears in {src['source_name']}?")
197
+ lines.append(f"A: {snippet}")
198
+ lines.append("")
199
+ return "\n".join(lines).strip() + "\n"
200
 
201
 
202
  def _encode_pcm_to_mp3(pcm_16le: bytes, sample_rate: int, channels: int) -> bytes:
 
300
  )
301
 
302
 
303
+ def generate_quiz(
304
  store: NotebookStore,
305
  *,
306
  user_id: str,
 
324
  )
325
  content = _llm_or_fallback(llm_prompt, _quiz_fallback(sources, questions))
326
  out_path = _write_markdown_artifact(dirs["quiz"], "quiz", content)
327
+ return ArtifactGenerateOut(
328
+ artifact_type="quiz",
329
  message=f"Generated {out_path.name}",
330
  markdown_path=str(out_path.as_posix()),
331
  audio_path=None,
332
+ created_at=_now(),
333
+ )
334
+
335
+
336
+ def generate_flashcards(
337
+ store: NotebookStore,
338
+ *,
339
+ user_id: str,
340
+ notebook_id: str,
341
+ prompt: Optional[str] = None,
342
+ num_questions: int = 8,
343
+ ) -> ArtifactGenerateOut:
344
+ dirs = _artifact_dirs(store, user_id, notebook_id)
345
+ sources = _collect_source_texts(store, user_id, notebook_id)
346
+ if not sources:
347
+ raise ValueError("No ingested sources available. Upload or ingest sources first.")
348
+
349
+ cards = max(3, min(20, int(num_questions)))
350
+ focus = prompt.strip() if prompt else "Create concise study flashcards."
351
+ llm_prompt = (
352
+ "Create markdown flashcards from SOURCES.\n"
353
+ f"Include exactly {cards} cards in format:\n"
354
+ "## Card N\nQ: ...\nA: ...\n"
355
+ "Keep answers concise and grounded in source content.\n\n"
356
+ f"FOCUS:\n{focus}\n\n"
357
+ f"SOURCES:\n{_sources_block(sources)}\n"
358
+ )
359
+ content = _llm_or_fallback(llm_prompt, _flashcards_fallback(sources, cards))
360
+ out_path = _write_markdown_artifact(dirs["flashcards"], "flashcards", content)
361
+ return ArtifactGenerateOut(
362
+ artifact_type="flashcards",
363
+ message=f"Generated {out_path.name}",
364
+ markdown_path=str(out_path.as_posix()),
365
+ audio_path=None,
366
+ created_at=_now(),
367
+ )
368
 
369
 
370
  def generate_podcast(
 
409
  def list_artifacts(store: NotebookStore, *, user_id: str, notebook_id: str) -> ArtifactListOut:
410
  dirs = _artifact_dirs(store, user_id, notebook_id)
411
 
412
+ reports = [_artifact_file_out(p) for p in sorted(dirs["report"].glob("report_*.md"))]
413
+ quizzes = [_artifact_file_out(p) for p in sorted(dirs["quiz"].glob("quiz_*.md"))]
414
+ flashcards = [_artifact_file_out(p) for p in sorted(dirs["flashcards"].glob("flashcards_*.md"))]
415
 
416
  podcast_indices: set[int] = set()
417
  for path in dirs["podcast"].glob("podcast_*.*"):
 
430
  )
431
  )
432
 
433
+ return ArtifactListOut(reports=reports, quizzes=quizzes, flashcards=flashcards, podcasts=podcasts)
434
 
435
 
436
  def resolve_artifact_path(
frontend/app.py CHANGED
@@ -417,16 +417,18 @@ def send_message(message: str, history, user_id: str, notebook_id: str):
417
 
418
 
419
  def _empty_artifacts_payload() -> dict[str, Any]:
420
- return {"reports": [], "quizzes": [], "podcasts": []}
421
 
422
 
423
  def _artifact_outputs_from_payload(payload: dict[str, Any]):
424
  reports = payload.get("reports") or []
425
  quizzes = payload.get("quizzes") or []
 
426
  podcasts = payload.get("podcasts") or []
427
 
428
  latest_report = reports[-1].get("path") if reports else None
429
  latest_quiz = quizzes[-1].get("path") if quizzes else None
 
430
 
431
  latest_transcript = None
432
  latest_audio = None
@@ -436,14 +438,14 @@ def _artifact_outputs_from_payload(payload: dict[str, Any]):
436
  audio = last.get("audio") or {}
437
  latest_transcript = transcript.get("path")
438
  latest_audio = audio.get("path")
439
- return latest_report, latest_quiz, latest_transcript, latest_audio
440
 
441
 
442
  def refresh_artifacts(user_id: str, notebook_id: str):
443
  user_id = (user_id or "").strip()
444
  if not user_id or not notebook_id:
445
  payload = _empty_artifacts_payload()
446
- return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
447
  try:
448
  payload = _api_request(
449
  "GET",
@@ -451,23 +453,23 @@ def refresh_artifacts(user_id: str, notebook_id: str):
451
  params={"user_id": user_id},
452
  headers=_auth_headers(user_id),
453
  )
454
- report, quiz, transcript, audio = _artifact_outputs_from_payload(payload)
455
- return gr.JSON(value=payload), report, quiz, transcript, audio, ""
456
  except Exception as exc:
457
  payload = _empty_artifacts_payload()
458
- return gr.JSON(value=payload), None, None, None, None, str(exc)
459
 
460
 
461
  def sync_artifacts_on_notebook_change(user_id: str, notebook_id: str):
462
- payload_json, report, quiz, transcript, audio, _status = refresh_artifacts(user_id, notebook_id)
463
- return payload_json, report, quiz, transcript, audio
464
 
465
 
466
  def generate_report_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
467
  user_id = (user_id or "").strip()
468
  if not user_id or not notebook_id:
469
  payload = _empty_artifacts_payload()
470
- return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
471
  try:
472
  resp = _api_request(
473
  "POST",
@@ -476,18 +478,18 @@ def generate_report_artifact(user_id: str, notebook_id: str, artifact_prompt: st
476
  headers=_auth_headers(user_id),
477
  timeout=180,
478
  )
479
- payload_json, report, quiz, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
480
- return payload_json, report, quiz, transcript, audio, str(resp.get("message", "Generated report."))
481
  except Exception as exc:
482
  payload = _empty_artifacts_payload()
483
- return gr.JSON(value=payload), None, None, None, None, str(exc)
484
 
485
 
486
  def generate_quiz_artifact(user_id: str, notebook_id: str, artifact_prompt: str, num_questions: float):
487
  user_id = (user_id or "").strip()
488
  if not user_id or not notebook_id:
489
  payload = _empty_artifacts_payload()
490
- return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
491
  try:
492
  resp = _api_request(
493
  "POST",
@@ -500,18 +502,42 @@ def generate_quiz_artifact(user_id: str, notebook_id: str, artifact_prompt: str,
500
  headers=_auth_headers(user_id),
501
  timeout=180,
502
  )
503
- payload_json, report, quiz, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
504
- return payload_json, report, quiz, transcript, audio, str(resp.get("message", "Generated quiz."))
505
  except Exception as exc:
506
  payload = _empty_artifacts_payload()
507
- return gr.JSON(value=payload), None, None, None, None, str(exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
 
510
  def generate_podcast_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
511
  user_id = (user_id or "").strip()
512
  if not user_id or not notebook_id:
513
  payload = _empty_artifacts_payload()
514
- return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
515
  try:
516
  resp = _api_request(
517
  "POST",
@@ -520,11 +546,11 @@ def generate_podcast_artifact(user_id: str, notebook_id: str, artifact_prompt: s
520
  headers=_auth_headers(user_id),
521
  timeout=240,
522
  )
523
- payload_json, report, quiz, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
524
- return payload_json, report, quiz, transcript, audio, str(resp.get("message", "Generated podcast."))
525
  except Exception as exc:
526
  payload = _empty_artifacts_payload()
527
- return gr.JSON(value=payload), None, None, None, None, str(exc)
528
 
529
  def greet_user(profile: gr.OAuthProfile | None) -> tuple[str | None, str]:
530
  if profile is None:
@@ -593,10 +619,12 @@ with gr.Blocks(title="MemoriaLM") as demo:
593
  report_btn = gr.Button("Generate Report")
594
  with gr.Row():
595
  quiz_btn = gr.Button("Generate Quiz")
 
596
  podcast_btn = gr.Button("Generate Podcast")
597
  artifacts_json = gr.JSON(label="Generated Artifacts", value=_empty_artifacts_payload())
598
  latest_report_file = gr.File(label="Latest Report (.md)", interactive=False)
599
  latest_quiz_file = gr.File(label="Latest Quiz (.md)", interactive=False)
 
600
  latest_podcast_transcript_file = gr.File(label="Latest Podcast Transcript (.md)", interactive=False)
601
  latest_podcast_audio = gr.Audio(label="Latest Podcast Audio (.mp3)", type="filepath", interactive=False)
602
 
@@ -666,6 +694,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
666
  artifacts_json,
667
  latest_report_file,
668
  latest_quiz_file,
 
669
  latest_podcast_transcript_file,
670
  latest_podcast_audio,
671
  ],
@@ -678,6 +707,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
678
  artifacts_json,
679
  latest_report_file,
680
  latest_quiz_file,
 
681
  latest_podcast_transcript_file,
682
  latest_podcast_audio,
683
  ],
@@ -689,6 +719,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
689
  artifacts_json,
690
  latest_report_file,
691
  latest_quiz_file,
 
692
  latest_podcast_transcript_file,
693
  latest_podcast_audio,
694
  ],
@@ -736,6 +767,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
736
  artifacts_json,
737
  latest_report_file,
738
  latest_quiz_file,
 
739
  latest_podcast_transcript_file,
740
  latest_podcast_audio,
741
  status_box,
@@ -748,6 +780,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
748
  artifacts_json,
749
  latest_report_file,
750
  latest_quiz_file,
 
751
  latest_podcast_transcript_file,
752
  latest_podcast_audio,
753
  status_box,
@@ -760,6 +793,20 @@ with gr.Blocks(title="MemoriaLM") as demo:
760
  artifacts_json,
761
  latest_report_file,
762
  latest_quiz_file,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  latest_podcast_transcript_file,
764
  latest_podcast_audio,
765
  status_box,
@@ -772,6 +819,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
772
  artifacts_json,
773
  latest_report_file,
774
  latest_quiz_file,
 
775
  latest_podcast_transcript_file,
776
  latest_podcast_audio,
777
  status_box,
 
417
 
418
 
419
  def _empty_artifacts_payload() -> dict[str, Any]:
420
+ return {"reports": [], "quizzes": [], "flashcards": [], "podcasts": []}
421
 
422
 
423
  def _artifact_outputs_from_payload(payload: dict[str, Any]):
424
  reports = payload.get("reports") or []
425
  quizzes = payload.get("quizzes") or []
426
+ flashcards = payload.get("flashcards") or []
427
  podcasts = payload.get("podcasts") or []
428
 
429
  latest_report = reports[-1].get("path") if reports else None
430
  latest_quiz = quizzes[-1].get("path") if quizzes else None
431
+ latest_flashcards = flashcards[-1].get("path") if flashcards else None
432
 
433
  latest_transcript = None
434
  latest_audio = None
 
438
  audio = last.get("audio") or {}
439
  latest_transcript = transcript.get("path")
440
  latest_audio = audio.get("path")
441
+ return latest_report, latest_quiz, latest_flashcards, latest_transcript, latest_audio
442
 
443
 
444
  def refresh_artifacts(user_id: str, notebook_id: str):
445
  user_id = (user_id or "").strip()
446
  if not user_id or not notebook_id:
447
  payload = _empty_artifacts_payload()
448
+ return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
449
  try:
450
  payload = _api_request(
451
  "GET",
 
453
  params={"user_id": user_id},
454
  headers=_auth_headers(user_id),
455
  )
456
+ report, quiz, flashcards, transcript, audio = _artifact_outputs_from_payload(payload)
457
+ return gr.JSON(value=payload), report, quiz, flashcards, transcript, audio, ""
458
  except Exception as exc:
459
  payload = _empty_artifacts_payload()
460
+ return gr.JSON(value=payload), None, None, None, None, None, str(exc)
461
 
462
 
463
  def sync_artifacts_on_notebook_change(user_id: str, notebook_id: str):
464
+ payload_json, report, quiz, flashcards, transcript, audio, _status = refresh_artifacts(user_id, notebook_id)
465
+ return payload_json, report, quiz, flashcards, transcript, audio
466
 
467
 
468
  def generate_report_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
469
  user_id = (user_id or "").strip()
470
  if not user_id or not notebook_id:
471
  payload = _empty_artifacts_payload()
472
+ return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
473
  try:
474
  resp = _api_request(
475
  "POST",
 
478
  headers=_auth_headers(user_id),
479
  timeout=180,
480
  )
481
+ payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
482
+ return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated report."))
483
  except Exception as exc:
484
  payload = _empty_artifacts_payload()
485
+ return gr.JSON(value=payload), None, None, None, None, None, str(exc)
486
 
487
 
488
  def generate_quiz_artifact(user_id: str, notebook_id: str, artifact_prompt: str, num_questions: float):
489
  user_id = (user_id or "").strip()
490
  if not user_id or not notebook_id:
491
  payload = _empty_artifacts_payload()
492
+ return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
493
  try:
494
  resp = _api_request(
495
  "POST",
 
502
  headers=_auth_headers(user_id),
503
  timeout=180,
504
  )
505
+ payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
506
+ return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated quiz."))
507
  except Exception as exc:
508
  payload = _empty_artifacts_payload()
509
+ return gr.JSON(value=payload), None, None, None, None, None, str(exc)
510
+
511
+
512
+ def generate_flashcards_artifact(user_id: str, notebook_id: str, artifact_prompt: str, num_questions: float):
513
+ user_id = (user_id or "").strip()
514
+ if not user_id or not notebook_id:
515
+ payload = _empty_artifacts_payload()
516
+ return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
517
+ try:
518
+ resp = _api_request(
519
+ "POST",
520
+ f"/api/notebooks/{notebook_id}/artifacts/flashcards",
521
+ json_body={
522
+ "user_id": user_id,
523
+ "prompt": (artifact_prompt or "").strip() or None,
524
+ "num_questions": int(num_questions),
525
+ },
526
+ headers=_auth_headers(user_id),
527
+ timeout=180,
528
+ )
529
+ payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
530
+ return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated flashcards."))
531
+ except Exception as exc:
532
+ payload = _empty_artifacts_payload()
533
+ return gr.JSON(value=payload), None, None, None, None, None, str(exc)
534
 
535
 
536
  def generate_podcast_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
537
  user_id = (user_id or "").strip()
538
  if not user_id or not notebook_id:
539
  payload = _empty_artifacts_payload()
540
+ return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
541
  try:
542
  resp = _api_request(
543
  "POST",
 
546
  headers=_auth_headers(user_id),
547
  timeout=240,
548
  )
549
+ payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
550
+ return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated podcast."))
551
  except Exception as exc:
552
  payload = _empty_artifacts_payload()
553
+ return gr.JSON(value=payload), None, None, None, None, None, str(exc)
554
 
555
  def greet_user(profile: gr.OAuthProfile | None) -> tuple[str | None, str]:
556
  if profile is None:
 
619
  report_btn = gr.Button("Generate Report")
620
  with gr.Row():
621
  quiz_btn = gr.Button("Generate Quiz")
622
+ flashcards_btn = gr.Button("Generate Flashcards")
623
  podcast_btn = gr.Button("Generate Podcast")
624
  artifacts_json = gr.JSON(label="Generated Artifacts", value=_empty_artifacts_payload())
625
  latest_report_file = gr.File(label="Latest Report (.md)", interactive=False)
626
  latest_quiz_file = gr.File(label="Latest Quiz (.md)", interactive=False)
627
+ latest_flashcards_file = gr.File(label="Latest Flashcards (.md)", interactive=False)
628
  latest_podcast_transcript_file = gr.File(label="Latest Podcast Transcript (.md)", interactive=False)
629
  latest_podcast_audio = gr.Audio(label="Latest Podcast Audio (.mp3)", type="filepath", interactive=False)
630
 
 
694
  artifacts_json,
695
  latest_report_file,
696
  latest_quiz_file,
697
+ latest_flashcards_file,
698
  latest_podcast_transcript_file,
699
  latest_podcast_audio,
700
  ],
 
707
  artifacts_json,
708
  latest_report_file,
709
  latest_quiz_file,
710
+ latest_flashcards_file,
711
  latest_podcast_transcript_file,
712
  latest_podcast_audio,
713
  ],
 
719
  artifacts_json,
720
  latest_report_file,
721
  latest_quiz_file,
722
+ latest_flashcards_file,
723
  latest_podcast_transcript_file,
724
  latest_podcast_audio,
725
  ],
 
767
  artifacts_json,
768
  latest_report_file,
769
  latest_quiz_file,
770
+ latest_flashcards_file,
771
  latest_podcast_transcript_file,
772
  latest_podcast_audio,
773
  status_box,
 
780
  artifacts_json,
781
  latest_report_file,
782
  latest_quiz_file,
783
+ latest_flashcards_file,
784
  latest_podcast_transcript_file,
785
  latest_podcast_audio,
786
  status_box,
 
793
  artifacts_json,
794
  latest_report_file,
795
  latest_quiz_file,
796
+ latest_flashcards_file,
797
+ latest_podcast_transcript_file,
798
+ latest_podcast_audio,
799
+ status_box,
800
+ ],
801
+ )
802
+ flashcards_btn.click(
803
+ generate_flashcards_artifact,
804
+ inputs=[user_id, notebook_selector, artifact_prompt, quiz_questions],
805
+ outputs=[
806
+ artifacts_json,
807
+ latest_report_file,
808
+ latest_quiz_file,
809
+ latest_flashcards_file,
810
  latest_podcast_transcript_file,
811
  latest_podcast_audio,
812
  status_box,
 
819
  artifacts_json,
820
  latest_report_file,
821
  latest_quiz_file,
822
+ latest_flashcards_file,
823
  latest_podcast_transcript_file,
824
  latest_podcast_audio,
825
  status_box,
tests/test_api_artifacts.py CHANGED
@@ -45,6 +45,12 @@ def test_artifact_endpoints_generate_list_and_download(monkeypatch, tmp_path: Pa
45
  headers=AUTH_U1,
46
  )
47
  assert report.status_code == 200
 
 
 
 
 
 
48
 
49
  podcast = client.post(
50
  f"/api/notebooks/{nb.notebook_id}/artifacts/podcast",
@@ -62,6 +68,7 @@ def test_artifact_endpoints_generate_list_and_download(monkeypatch, tmp_path: Pa
62
  assert listed.status_code == 200
63
  payload = listed.json()
64
  assert len(payload["reports"]) == 1
 
65
  assert len(payload["podcasts"]) == 1
66
 
67
  dl = client.get(
 
45
  headers=AUTH_U1,
46
  )
47
  assert report.status_code == 200
48
+ flashcards = client.post(
49
+ f"/api/notebooks/{nb.notebook_id}/artifacts/flashcards",
50
+ json={"user_id": "u1", "num_questions": 6},
51
+ headers=AUTH_U1,
52
+ )
53
+ assert flashcards.status_code == 200
54
 
55
  podcast = client.post(
56
  f"/api/notebooks/{nb.notebook_id}/artifacts/podcast",
 
68
  assert listed.status_code == 200
69
  payload = listed.json()
70
  assert len(payload["reports"]) == 1
71
+ assert len(payload["flashcards"]) == 1
72
  assert len(payload["podcasts"]) == 1
73
 
74
  dl = client.get(
tests/test_artifacts.py CHANGED
@@ -34,15 +34,19 @@ def test_generate_report_quiz_and_list(monkeypatch, tmp_path: Path):
34
 
35
  report = artifacts.generate_report(store, user_id="u1", notebook_id=nb.notebook_id, prompt="Focus")
36
  quiz = artifacts.generate_quiz(store, user_id="u1", notebook_id=nb.notebook_id, num_questions=5)
 
37
 
38
  assert report.artifact_type == "report"
39
  assert quiz.artifact_type == "quiz"
 
40
  assert Path(report.markdown_path).exists()
41
  assert Path(quiz.markdown_path).exists()
 
42
 
43
  listed = artifacts.list_artifacts(store, user_id="u1", notebook_id=nb.notebook_id)
44
  assert len(listed.reports) == 1
45
  assert len(listed.quizzes) == 1
 
46
  assert listed.podcasts == []
47
 
48
 
 
34
 
35
  report = artifacts.generate_report(store, user_id="u1", notebook_id=nb.notebook_id, prompt="Focus")
36
  quiz = artifacts.generate_quiz(store, user_id="u1", notebook_id=nb.notebook_id, num_questions=5)
37
+ flashcards = artifacts.generate_flashcards(store, user_id="u1", notebook_id=nb.notebook_id, num_questions=6)
38
 
39
  assert report.artifact_type == "report"
40
  assert quiz.artifact_type == "quiz"
41
+ assert flashcards.artifact_type == "flashcards"
42
  assert Path(report.markdown_path).exists()
43
  assert Path(quiz.markdown_path).exists()
44
+ assert Path(flashcards.markdown_path).exists()
45
 
46
  listed = artifacts.list_artifacts(store, user_id="u1", notebook_id=nb.notebook_id)
47
  assert len(listed.reports) == 1
48
  assert len(listed.quizzes) == 1
49
+ assert len(listed.flashcards) == 1
50
  assert listed.podcasts == []
51
 
52