Add flashcards feature with spaced repetition scheduling

Adds a full flashcards system: FSRS-based scheduling, cloze/basic
card types, CSV import/export, media storage, and study UI, wired
into navigation and the dashboard alongside existing question flows.
This commit is contained in:
hz
2026-07-21 15:08:52 -05:00
parent 29b89dc404
commit 48a8c8361d
41 changed files with 4458 additions and 8 deletions
+33 -1
View File
@@ -2,6 +2,7 @@ extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
const FlashcardPrefillDialog := preload("res://scripts/ui/flashcard_prefill_dialog.gd")
const CLEAR_HIGHLIGHT_DARK_ICON := preload("res://assets/icons/medmind/clear-highlight-dark.svg")
const CLEAR_HIGHLIGHT_LIGHT_ICON := preload("res://assets/icons/medmind/clear-highlight-light.svg")
const LABS_DARK_ICON := preload("res://assets/icons/medmind/labs-dark.svg")
@@ -84,6 +85,8 @@ var clear_highlight_button: Button
var flag_button: Button
var report_button: Button
var notes_button: Button
var flashcard_button: Button
var flashcard_prefill_dialog: Control
var bookmark_button: Button
var app_feedback_button: Button
var theme_button: Button
@@ -280,6 +283,22 @@ func _ready() -> void:
)
question_toolbar.add_child(flag_button)
flashcard_button = Button.new()
flashcard_button.tooltip_text = "Save as flashcard (available once answered)"
flashcard_button.icon = preload("res://assets/icons/lucide/clipboard-plus.svg")
flashcard_button.custom_minimum_size = Vector2(44, 44)
flashcard_button.add_theme_constant_override("icon_max_width", 22)
flashcard_button.alignment = HORIZONTAL_ALIGNMENT_CENTER
flashcard_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
flashcard_button.focus_mode = Control.FOCUS_NONE
flashcard_button.disabled = true
UISoundService.set_button_sound(flashcard_button, "toggle")
flashcard_button.pressed.connect(_toggle_flashcard_current_question)
question_toolbar.add_child(flashcard_button)
flashcard_prefill_dialog = FlashcardPrefillDialog.new()
flashcard_prefill_dialog.name = "FlashcardPrefillDialog"
add_child(flashcard_prefill_dialog)
bookmark_button = Button.new()
bookmark_button.tooltip_text = "Save question"
bookmark_button.icon = preload("res://assets/icons/lucide/bookmark.svg")
@@ -865,7 +884,7 @@ func _apply_question_theme() -> void:
var navigator_heading := navigator_popup.find_child("NavigatorHeading", true, false) as Label
if navigator_heading != null:
navigator_heading.add_theme_color_override("font_color", Brand.text_color(dark))
for control in [text_smaller_button, text_larger_button, flag_button, bookmark_button, notes_button, app_feedback_button]:
for control in [text_smaller_button, text_larger_button, flag_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]:
if control == null:
continue
var active: bool = (control == flag_button and flagged) or (control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", ""))))
@@ -1423,6 +1442,7 @@ func _show_answer_review() -> void:
skip_button.disabled = false if BlockSessionService.session_kind == "bookmark" else true
if question_scroll != null:
question_scroll.call_deferred("ensure_control_visible", explanation_label)
_refresh_toolbar_state()
func _set_answer_review_timer_visibility() -> void:
@@ -1969,6 +1989,9 @@ func _refresh_toolbar_state() -> void:
report_button.tooltip_text = "Report submitted; report another issue" if reported_question_ids.has(question_id) else "Report question"
if notes_button != null:
notes_button.visible = BlockSessionService.session_kind != "simulation"
if flashcard_button != null:
flashcard_button.visible = BlockSessionService.session_kind != "simulation"
flashcard_button.disabled = current_answer.is_empty()
if bookmark_button != null:
bookmark_button.visible = BlockSessionService.session_kind != "simulation"
var bookmark_question_id := str(BlockSessionService.get_current_question().get("id", ""))
@@ -1984,6 +2007,15 @@ func _toggle_bookmark_current_question() -> void:
BookmarkService.toggle_question(question)
func _toggle_flashcard_current_question() -> void:
if current_answer.is_empty():
return
var question := BlockSessionService.get_current_question()
if question.is_empty() or flashcard_prefill_dialog == null:
return
flashcard_prefill_dialog.open_for_question(question, current_answer)
func _on_bookmark_status_changed(question_id: String, value: bool, pending: bool) -> void:
if question_id != str(BlockSessionService.get_current_question().get("id", "")) or bookmark_button == null:
return