3232 "generate" : PROJECT_DIR / "note_generation.py" ,
3333}
3434
35- COURSES : dict [int , str ] = {
36- 85367 : "CS2101 Effective Communication" ,
37- 85377 : "CS2103/T Software Engineering" ,
38- 85397 : "CS2105 Computer Networks" ,
39- 85427 : "CS3210 Parallel Computing" ,
40- }
35+ COURSES : dict [int , str ] = {} # populated from Canvas API after token is entered
36+
37+ _SKIP_KEYWORDS = [
38+ "training" , "pdp" , "rmcpdp" , "osa" , "soct" , "travel" ,
39+ "essentials" , "respect" , "consent" ,
40+ ]
41+
42+
43+ def _load_courses_from_canvas () -> None :
44+ """Fetch active courses from Canvas and update the global COURSES dict."""
45+ COURSES .clear ()
46+ token_file = PROJECT_DIR / "canvas_token.txt"
47+ config_file = PROJECT_DIR / "config.json"
48+ token = token_file .read_text ().strip () if token_file .exists () else ""
49+ if not token :
50+ return
51+ cfg = json .load (open (config_file )) if config_file .exists () else {}
52+ canvas_url = cfg .get ("CANVAS_URL" , "" ).rstrip ("/" )
53+ if not canvas_url :
54+ return
55+ try :
56+ import requests
57+ resp = requests .get (
58+ f"{ canvas_url } /api/v1/courses" ,
59+ headers = {"Authorization" : f"Bearer { token } " },
60+ params = {"enrollment_state" : "active" , "enrollment_type[]" : "student" ,
61+ "per_page" : 100 },
62+ timeout = 10 ,
63+ )
64+ resp .raise_for_status ()
65+ for c in resp .json ():
66+ name = c .get ("name" ) or c .get ("course_code" ) or ""
67+ if not name :
68+ continue
69+ if any (kw in name .lower () for kw in _SKIP_KEYWORDS ):
70+ continue
71+ COURSES [c ["id" ]] = name
72+ except Exception :
73+ pass # silently leave COURSES empty; user sees the empty-state UI
4174
4275# ── Palette ───────────────────────────────────────────────────────────────────
4376
@@ -385,17 +418,20 @@ def _course_dropdown(value: str, on_select: callable,
385418 options .append (ft .dropdown .Option (key = "0" , text = "All courses" ))
386419 for cid , name in COURSES .items ():
387420 options .append (ft .dropdown .Option (key = str (cid ), text = f"{ name } ({ cid } )" ))
421+ if not options :
422+ options .append (ft .dropdown .Option (
423+ key = "" , text = "— no courses, add Canvas token in Settings —" ))
388424 return ft .Dropdown (
389425 options = options ,
390- value = value ,
426+ value = value if COURSES else None ,
391427 on_select = on_select ,
392428 bgcolor = C_SURFACE ,
393429 border_color = ft .Colors .with_opacity (0.25 , ft .Colors .WHITE ),
394430 focused_border_color = C_PRIMARY ,
395431 color = ft .Colors .WHITE ,
396432 label = "Course" ,
397433 label_style = ft .TextStyle (color = C_PRIMARY ),
398- expand = True , # responsive: fills available width
434+ expand = True ,
399435 )
400436
401437def _text_field (label : str , value : str = "" , hint : str = "" ,
@@ -516,16 +552,33 @@ def _go(_):
516552 on_click = _go ,
517553 )
518554
555+ if COURSES :
556+ items = list (COURSES .items ())
557+ course_rows = [
558+ ft .Row (controls = [_course_card (cid , name )
559+ for cid , name in items [i :i + 2 ]], spacing = 12 )
560+ for i in range (0 , len (items ), 2 )
561+ ]
562+ else :
563+ course_rows = [_card (ft .Column (controls = [
564+ ft .Container (height = 8 ),
565+ ft .Icon (ft .Icons .SCHOOL_OUTLINED , size = 52 ,
566+ color = ft .Colors .with_opacity (0.25 , ft .Colors .WHITE )),
567+ ft .Text ("No courses loaded" , size = 15 ,
568+ color = ft .Colors .with_opacity (0.45 , ft .Colors .WHITE ),
569+ weight = ft .FontWeight .W_500 ),
570+ ft .Text (
571+ "Go to Settings → enter your Canvas URL and API token,\n "
572+ "then save to load your courses automatically." ,
573+ size = 12 , text_align = ft .TextAlign .CENTER ,
574+ color = ft .Colors .with_opacity (0.35 , ft .Colors .WHITE ),
575+ ),
576+ ft .Container (height = 8 ),
577+ ], horizontal_alignment = ft .CrossAxisAlignment .CENTER , spacing = 10 ))]
578+
519579 scroll_content = [
520580 _section_title ("Course Overview" , ft .Icons .DASHBOARD_OUTLINED ),
521- ft .Row (
522- controls = [_course_card (cid , name ) for cid , name in list (COURSES .items ())[:2 ]],
523- spacing = 12 ,
524- ),
525- ft .Row (
526- controls = [_course_card (cid , name ) for cid , name in list (COURSES .items ())[2 :]],
527- spacing = 12 ,
528- ),
581+ * course_rows ,
529582 ft .Container (height = 4 ),
530583 _section_title ("Quick Actions" , ft .Icons .BOLT_OUTLINED ),
531584 ft .Row (controls = [
@@ -541,7 +594,7 @@ def _go(_):
541594# ── Page: Full Pipeline ───────────────────────────────────────────────────────
542595
543596def build_pipeline (page : ft .Page , console : OutputConsole ) -> ft .Column :
544- course_val = {"v" : str (list ( COURSES . keys ())[ 0 ] )}
597+ course_val = {"v" : str (next ( iter ( COURSES ), "" ) )}
545598
546599 course_dd = _course_dropdown (
547600 value = course_val ["v" ],
@@ -858,11 +911,11 @@ def _run_manual(_) -> None:
858911# ── Page: Generate Notes ──────────────────────────────────────────────────────
859912
860913def build_generate (page : ft .Page , console : OutputConsole ) -> ft .Column :
861- default_cid = list ( COURSES . keys ())[ 0 ]
862- course_val = {"v" : str (default_cid )}
914+ default_cid = next ( iter ( COURSES ), None )
915+ course_val = {"v" : str (default_cid ) if default_cid else "" }
863916
864917 course_name_f = _text_field ("Course name" ,
865- value = _course_name_from_notes (default_cid ))
918+ value = _course_name_from_notes (default_cid ) if default_cid else "" )
866919
867920 def _on_course_select (e ) -> None :
868921 # Bug fix: use e.data
@@ -971,7 +1024,8 @@ def _run(_) -> None:
9711024
9721025# ── Page: Settings ────────────────────────────────────────────────────────────
9731026
974- def build_settings (page : ft .Page ) -> ft .Column :
1027+ def build_settings (page : ft .Page ,
1028+ on_courses_changed : callable | None = None ) -> ft .Column :
9751029
9761030 def _snack (msg : str , ok : bool = True ) -> None :
9771031 page .snack_bar = ft .SnackBar (
@@ -1020,6 +1074,8 @@ def _save_canvas_url(_):
10201074 try :
10211075 _save_config ("CANVAS_URL" , tf_canvas_url .value .strip ())
10221076 _snack ("Canvas URL saved." )
1077+ if on_courses_changed :
1078+ on_courses_changed ()
10231079 except Exception as e :
10241080 _snack (f"Error: { e } " , ok = False )
10251081
@@ -1075,7 +1131,9 @@ def _save_panopto(_):
10751131 def _save_canvas (_ ):
10761132 try :
10771133 canvas_file .write_text (tf_canvas .value .strip ())
1078- _snack ("Canvas token saved to canvas_token.txt." )
1134+ _snack ("Canvas token saved." )
1135+ if on_courses_changed :
1136+ on_courses_changed ()
10791137 except Exception as e :
10801138 _snack (f"Error: { e } " , ok = False )
10811139
@@ -1320,15 +1378,25 @@ def navigate(idx: int) -> None:
13201378 if _nav_target [0 ]:
13211379 _nav_target [0 ](idx )
13221380
1323- pages = [
1324- build_dashboard (page , console , navigate = navigate ),
1325- build_pipeline (page , console ),
1326- build_download (page , console ),
1327- build_transcribe (page , console ),
1328- build_align (page , console ),
1329- build_generate (page , console ),
1330- build_settings (page ),
1331- ]
1381+ # Mutable ref so _rebuild can be passed to build_settings before it's defined
1382+ _rebuild_ref : list [callable | None ] = [None ]
1383+
1384+ def _build_pages () -> list :
1385+ return [
1386+ build_dashboard (page , console , navigate = navigate ),
1387+ build_pipeline (page , console ),
1388+ build_download (page , console ),
1389+ build_transcribe (page , console ),
1390+ build_align (page , console ),
1391+ build_generate (page , console ),
1392+ build_settings (page ,
1393+ on_courses_changed = lambda : _rebuild_ref [0 ] and _rebuild_ref [0 ]()),
1394+ ]
1395+
1396+ # Try to populate courses immediately if credentials are already on disk
1397+ _load_courses_from_canvas ()
1398+
1399+ pages = _build_pages ()
13321400
13331401 # page_content swaps between tab pages; console stays fixed at the bottom
13341402 page_content = ft .Container (
@@ -1362,6 +1430,17 @@ def _navigate(idx: int) -> None:
13621430
13631431 _nav_target [0 ] = _navigate
13641432
1433+ def _rebuild () -> None :
1434+ """Reload courses from Canvas and rebuild all course-dependent pages."""
1435+ _load_courses_from_canvas ()
1436+ new = _build_pages ()
1437+ pages .clear ()
1438+ pages .extend (new )
1439+ page_content .content = pages [rail .selected_index ]
1440+ page .update ()
1441+
1442+ _rebuild_ref [0 ] = _rebuild
1443+
13651444 rail = ft .NavigationRail (
13661445 selected_index = 0 ,
13671446 destinations = [
0 commit comments