# Project export: SlugCourses

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2024
- Tagline: UCSC class search app with chatbot helps you find courses.
- Devpost: https://devpost.com/software/slug-courses
- GitHub: https://github.com/prapooskur/SlugCourses
- Video: https://www.youtube.com/embed/Sz_3OEXamQA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best Slug Hacks)
- Team: 3 GitHub contributor(s) — prapooskur (65 commits), iconsumeplutonium (40 commits), Rahul Amudhasagaran (1 commits)

## Devpost submission (written by the team)

### Inspiration

Like students at every university, the four of us have to sign up for courses at the start of each quarter. In order to determine what classes are offered or the status of classes we're interested in, we must use Pisa, UCSC's class search website. It does exactly what it says on the tin: allows you to search for classes with a variety of filters, such as whether the class is open or waitlisted, the subject, the name of the class, and the number of credits. As a tool, it has numerous issues from a mobile interface standpoint: The mobile website is not user-friendly at all It does not have a smooth and responsive design The design lacks an ease of use for those with accessibility issues It does not follow the best practices of mobile usability It is difficult to navigate through and search for classes The interface was clearly designed to be navigated with a mouse and not a touch screen We believe that we could make an improved version of the class search website's mobile interface, making it accessible to mobile users who want to access the UCSC catalogue anytime and anywhere. Among other things, we also realized was that finding classes among the 1500+ courses offered at UCSC every quarter was a challenge. Pisa does not offer any easy way of searching or comparing classes with natural language. In order to search for a class, you must have prior knowledge of the class in question ranging from its name, department, catalog number, or the professor in charge. To make this easier, we envisioned a chatbot powered by a large language model (LLM) which could use a technique called retrieval-augmented generation (RAG) to scan a database of UCSC courses and return the courses most relevant to a user's course.

### What it does

The app we created allows users to search for classes intuitively in a simple and easy to navigate interface. The results page presents information in a similarly easy-to-read manner. Tapping on any result brings up a more detailed results page. If the user still wants even more details, they can tap on a button to take them to Pisa's page for it. Advanced filters on the main search page also allow for more fine tuned results. The integrated LLM-based chatbot can be talked to via a button on the bottom navigation bar. It can be asked questions about courses to take.

### How we built it

First, we set our sights on creating a database of all UCSC courses. To do this, we wrote a webscraper in Python that scraped the entire website for all 1,456 courses offered this quarter, as well as the 1000+ courses offered in each of the prior four quarters for a total of close to 6000 courses. We then pushed this to Supabase, a PostgresSQL database. In order to update the database with the most relevant information, the scraper is re-run at the top of every hour. Within the app itself, whenever a user searches for a course, this database is queried for the most relevant courses and all its information. Next up was the LLM chatbot. The model we chose was Google's Gemini, the same model that powers Bard, Google's ChatGPT competitor. Now we couldn't simply give it our entire database and tell it to extract the most relevant courses (though that didn't stop us from trying). We needed a way to extract the classes that closely matched the user's input query, whether that be a question ("What astronomy courses are offered?") or an imperative statement ("Recommend me classes about chemistry"). In order to do this, we decided on Haystack, an open source Python framework for implementing retrieval-augmented generation. We wrote Python code that pulled our entire repository of course data into a single file. Using Haystack, we wrote a seven-stage pipeline for getting user input: Load data about all classes into document storage Generate text embedding for user input This turns the user's input into a vector that can be processed This turns the user's input into a vector that can be processed Create a list of documents that match the input (sparse document retrieval) Uses the "bag of words" technique: simply matches keywords without taking context into account Uses the "bag of words" technique: simply matches keywords without taking context into account Create a second list of courses that match the input (dense document retrieval) Neural encoder learns the best way to encode text into vectors, taking into account context and semantic meaning of words Neural encoder learns the best way to encode text into vectors, taking into account context and semantic meaning of words Join sparse/dense documents into one list of documents, ranked by score Insert merged document list and user input into prompt Prompt was specifically engineered to make Gemini returns its recommendations in a specific and consistent format with only relevant information Prompt was specifically engineered to make Gemini returns its recommendations in a specific and consistent format with only relevant information Query Gemini LLM with final prompt and return response. Once we got the pipeline running, we used the Python FastAPI and Uvicorn libraries to create an API endpoint that the app could use to send the user's query and get back Gemini's response.

### Challenges we ran into

One of the hurdles we ran into was attempting to find ways to fine tune the LLM response. Give it too simple of a prompt and it would be saying too much to even fit on screen. Give it too restrictive of a prompt and it wouldn't give enough information. We had to engineering the prompt in such a way that it would balance just the right amount of information given. Another challenge we ran into was turning Gemini's response into a proper API. Because Gemini returned its response in chunks at a time, we had two choices: either wait for it to return its entire response before handing the response to the app (which would be easier but result in longer wait times), or attempt to stream the response API to the app as the responses come in (harder, but would reduce wait times). We tried various methods of passing the responses to the app as it was returned from Gemini, such as chunking the data into separate JSON responses, but to no avail. Eventually, in the interest of time we decided to go with the former option of returning the response all at once so we could make the rest of the app function.

### Accomplishments we're proud of

We're really proud of the UI/UX of the app. The mobile experience is significantly improved over the website. The UI was modernized to be more aesthetically pleasing, and content is presented in a easily digestible manner. The UX is markedly enhanced with proper support for phones and touch screen devices. It up to date with modern UI principles (we follow Google's Material Design 3 guidelines) and was designed with modern UI prototyping tools such as Figma. It is also integrated with a lot of common touch screen actions such as swiping to the left of the screen to go back the previous screen. We are also incredibly proud of the LLM integration. It was a massive undertaking to delve into the realm of large language models, and especially something as complicated as retrieval-augmented generation. By creating our own pipeline instead of going with a cloud solution, we learned an incredible amount of information about the underlying workings of language models and retrieval pipelines.

### What we learned

We definitely learned a lot of stuff. We learned to work with a proper cloud SQL database for our backend data and how to integrate it with webscraped data. We also learned a lot about how LLMs could be used to search documents in addition to simple conversations. We also learned the complexity and depth of UX/UI design on Figma. We also learned a lot about app development and integrating it with all of our complex moving parts.

### What's next

There are numerous future features and improvements that we can make to SlugCourses. This include the following: Refine the current search functionality Improving the functionality of the chatbot Create an integration using Rate My Professor Develop iOS/iPadOS and desktop versions of the app This experience was invaluable and taught us a lot about app development, user interface design, and natural language processing. We are proud of what we have accomplished and we hope to continue working on SlugCourses to make it even better for UCSC students.

## README (from the GitHub repository)

## Inspiration

Like students at every university, the four of us have to sign up for courses at the start of each quarter. In order to determine what classes are offered or the status of classes we're interested in, we must use [Pisa](https://pisa.ucsc.edu/class_search/), UCSC's class search website. It does exactly what it says on the tin: allows you to search for classes with a variety of filters, such as whether the class is open or waitlisted, the subject, the name of the class, and the number of credits. As a tool, it has numerous issues from a mobile interface standpoint:

* The mobile website is not user-friendly at all
* It does not have a smooth and responsive design
* The design lacks an ease of use for those with accessibility issues
* It does not follow the best practices of mobile usability
* It is difficult to navigate through and search for classes
* The interface was clearly designed to be navigated with a mouse and not a touch screen


<img src="https://raw.githubusercontent.com/prapooskur/SlugCourses/main/images/pisasearch.gif" width=30% height=30% alt="pisa current mobile interface">


We believe that we could make an improved version of the class search website's mobile interface, making it accessible to mobile users who want to access the UCSC catalogue anytime and anywhere. 

Among other things, we also realized was that finding classes among the 1500+ courses offered at UCSC every quarter was a challenge. Pisa does not offer any easy way of searching or comparing classes with natural language. In order to search for a class, you must have prior knowledge of the class in question ranging from its name, department, catalog number, or the professor in charge. To make this easier, we envisioned a chatbot powered by a large language model (LLM) which could use a technique called [retrieval-augmented generation](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/) (RAG) to scan a database of UCSC courses and return the courses most relevant to a user's course. 


## What it does

The app we created allows users to search for classes intuitively in a simple and easy to navigate interface.


<img src="https://raw.githubusercontent.com/prapooskur/SlugCourses/main/images/search%20for%20cse%20100.gif" width=30% height=30% alt="searhing for a class">


The results page presents information in a similarly easy-to-read manner. Tapping on any result brings up a more detailed results page. If the user still wants even more details, they can tap on a button to take them to Pisa's page for it.


<img src="https://raw.githubusercontent.com/prapooskur/SlugCourses/main/images/result%20to%20pisa.gif" width=30% height=30% alt="result to pisa">


Advanced filters on the main search page also allow for more fine tuned results.

<img src="https://raw.githubusercontent.com/prapooskur/SlugCourses/main/images/ge%20filter%20search.gif" width=30% height=30% alt="result to pisa">


The integrated LLM-based chatbot can be talked to via a button on the bottom navigation bar. It can be asked questions about courses to take.


<img src="https://raw.githubusercontent.com/prapooskur/SlugCourses/main/images/llm%20response.gif" width=30% height=30% alt="llm response">


## How we built it

First, we set our sights on creating a database of all UCSC courses. To do this, we wrote a webscraper in Python that scraped the entire website for all 1,456 courses offered this quarter, as well as the 1000+ courses offered in each of the prior four quarters for a total of close to 6000 courses. We then pushed this to [Supabase](https://supabase.com/), a PostgresSQL database. In order to update the database with the most relevant information, the scraper is re-run at the top of every hour. Within the app itself, whenever a user searches for a course, this database is queried for the most relevant courses and all its information. 


<div>
<img src="https://raw.githubusercontent.com/prapooskur/SlugCourses/main/images/database.png" width=100% height=30% alt="pisa current mobile interface">

<img src="https://raw.githubusercontent.com/prapooskur/SlugCourses/main/images/search%20results.png" width=30% height=30% alt="pisa current mobile interface">
</div>

Next up was the LLM chatbot. The model we chose was Google's [Gemini](https://blog.google/technology/ai/google-gemini-ai/), the same model that powers Bard, Google's ChatGPT competitor. Now we couldn't simply give it our entire database and tell it to extract the most relevant courses (though that didn't stop us from trying). We needed a way to extract the classes that closely matched the user's input query, whether that be a question ("What astronomy courses are offered?") or an imperative statement ("Recommend me classes about chemistry"). In order to do this, we decided on [Haystack](https://haystack.deepset.ai/), an open source Python framework for implementing retrieval-augmented generation. We wrote Python code that pulled our entire repository of course data into a single file. Using Haystack, we wrote a seven-stage pipeline for getting user input:

1. Load data about all classes into document storage 
2. Generate text embedding for user input
    * This turns the user's input into a vector that can be processed 
3. Create a list of documents that match the input (sparse document retrieval) 
    * Uses the "bag of words" technique: simply matches keywords without taking context into account
4. Create a second list of courses that match the input (dense document retrieval) 
    * Neural encoder learns the best way to encode text into vectors, taking into account context and semantic meaning of words
5. Join sparse/dense documents into one list of documents, ranked by score
6. Insert merged document list and user input into prompt
    * Prompt was specifically engineered to make Gemini returns its recommendations in a specific and consistent format with only relevant information
7. Query Gemini LLM with final prompt and return response. 

Once we got the pipeline running, we used the Python FastAPI and Uvicorn libraries to create an API endpoint that the app could use to send the user's query and get back Gemini's response. 


## Challenges we ran into

One of the hurdles we ran into was attempting to find ways to fine tune the LLM response. Give it too simple of a prompt and it would be saying too much to even fit on screen. Give it too restrictive of a prompt and it wouldn't give enough information. We had to engineering the prompt in such a way that it would balance just the right amount of information given.

Another challenge we ran into was turning Gemini's response into a proper API. Because Gemini returned its response in chunks at a time, we had two choices: either wait for it to return its *entire* response before handing the response to the app (which would be easier but result in longer wait times), or attempt to stream the response API to the app as the responses come in (harder, but would reduce wait times). We tried various methods of passing the responses to the app as it was returned from Gemini, such as chunking the data into separate JSON responses, but to no avail. Eventually, in the interest of time we decided to go with the former option of returning the response all at once so we could make the rest of the app function. 

## Accomplishments that we're proud of

We're really proud of the UI/UX of the app. The mobile experience is significantly improved over the website. The UI was modernized to be more aesthetically pleasing, and content is presented in a easily digestible manner. The UX is markedly enhanced with proper support for phones and touch screen devices. It up to date with modern UI principles (we follow Google's Material Design 3 guidelines) and was designed with modern UI prototyping tools such as Figma. It is also integrated with a lot of common touch screen actions such as swiping to the left of the screen to go back the previous screen. 

We are also incredibly proud of 

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 91 recognized source files, 470 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Hugging Face (technology) — detected in the code
- JavaScript (language) — detected in the code
- Kotlin (language) — detected in the code
- OpenAI (technology) — detected in the code
- PostgreSQL (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- Supabase (technology) — detected in the code
- Swift (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 122)

```
.github/workflows/build-app.yml
.gitignore
app/.gitignore
app/build.gradle.kts
app/composeApp/build.gradle.kts
app/composeApp/common-rules.pro
app/composeApp/desktop-rules.pro
app/composeApp/hs_err_pid18704.log
app/composeApp/release/output-metadata.json
app/composeApp/src/androidMain/AndroidManifest.xml
app/composeApp/src/androidMain/kotlin/App.android.kt
app/composeApp/src/androidMain/kotlin/com/pras/slugcourses/MainActivity.kt
app/composeApp/src/androidMain/kotlin/DbDriver.kt
app/composeApp/src/androidMain/kotlin/ui/theme/Theme.android.kt
app/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
app/composeApp/src/androidMain/res/drawable/ic_launcher_foreground.xml
app/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
app/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
app/composeApp/src/androidMain/res/values/ic_launcher_background.xml
app/composeApp/src/androidMain/res/values/strings.xml
app/composeApp/src/commonMain/composeResources/drawable/books.xml
app/composeApp/src/commonMain/composeResources/drawable/chat_filled.xml
app/composeApp/src/commonMain/composeResources/drawable/chat_outlined.xml
app/composeApp/src/commonMain/composeResources/drawable/clock.xml
app/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
app/composeApp/src/commonMain/composeResources/drawable/group.xml
app/composeApp/src/commonMain/composeResources/drawable/home_filled.xml
app/composeApp/src/commonMain/composeResources/drawable/home_outlined.xml
app/composeApp/src/commonMain/composeResources/drawable/ic_launcher_background.xml
app/composeApp/src/commonMain/composeResources/drawable/ic_launcher_foreground.xml
app/composeApp/src/commonMain/composeResources/drawable/location_on.xml
app/composeApp/src/commonMain/composeResources/drawable/open_in_new.xml
app/composeApp/src/commonMain/composeResources/drawable/pin_drop.xml
app/composeApp/src/commonMain/composeResources/drawable/settings_fill.xml
app/composeApp/src/commonMain/composeResources/drawable/settings.xml
app/composeApp/src/commonMain/composeResources/drawable/share_location.xml
app/composeApp/src/commonMain/composeResources/drawable/slug.xml
app/composeApp/src/commonMain/composeResources/drawable/star_filled.xml
app/composeApp/src/commonMain/composeResources/drawable/star.xml
app/composeApp/src/commonMain/composeResources/drawable/stop.xml
app/composeApp/src/commonMain/composeResources/drawable/videocam.xml
app/composeApp/src/commonMain/kotlin/api/ChatAPI.kt
app/composeApp/src/commonMain/kotlin/api/DetailedClassAPI.kt
app/composeApp/src/commonMain/kotlin/api/SettingsAPI.kt
app/composeApp/src/commonMain/kotlin/api/SupabaseAPI.kt
app/composeApp/src/commonMain/kotlin/App.kt
app/composeApp/src/commonMain/kotlin/ChatScreen.kt
app/composeApp/src/commonMain/kotlin/DbDriver.kt
app/composeApp/src/commonMain/kotlin/DetailedResultsScreen.kt
app/composeApp/src/commonMain/kotlin/FavoritesScreen.kt
app/composeApp/src/commonMain/kotlin/HomeScreen.kt
app/composeApp/src/commonMain/kotlin/ResultsScreen.kt
app/composeApp/src/commonMain/kotlin/SettingsScreen.kt
app/composeApp/src/commonMain/kotlin/ui/data/ChatScreenModel.kt
app/composeApp/src/commonMain/kotlin/ui/data/DetailedResultsScreenModel.kt
app/composeApp/src/commonMain/kotlin/ui/data/FavoritesScreenModel.kt
app/composeApp/src/commonMain/kotlin/ui/data/HomeScreenModel.kt
app/composeApp/src/commonMain/kotlin/ui/data/NavigatorScreenModel.kt
app/composeApp/src/commonMain/kotlin/ui/data/ResultsScreenModel.kt
app/composeApp/src/commonMain/kotlin/ui/data/SettingsScreenModel.kt
app/composeApp/src/commonMain/kotlin/ui/elements/BetterDropdown.kt
app/composeApp/src/commonMain/kotlin/ui/elements/BetterDropdownMultiSelect.kt
app/composeApp/src/commonMain/kotlin/ui/elements/CourseCard.kt
app/composeApp/src/commonMain/kotlin/ui/elements/TopBar.kt
app/composeApp/src/commonMain/kotlin/ui/Helpers.kt
app/composeApp/src/commonMain/kotlin/ui/theme/Color.kt
app/composeApp/src/commonMain/kotlin/ui/theme/Theme.kt
app/composeApp/src/commonMain/kotlin/ui/theme/Type.kt
app/composeApp/src/commonMain/sqldelight/com/pras/courses/favorites.sq
app/composeApp/src/commonMain/sqldelight/com/pras/courses/suggestions.sq
app/composeApp/src/commonMain/sqldelight/com/pras/courses/terms.sq
app/composeApp/src/commonMain/sqldelight/migrations/1.sqm
app/composeApp/src/desktopMain/kotlin/App.desktop.kt
app/composeApp/src/desktopMain/kotlin/DbDriver.desktop.kt
app/composeApp/src/desktopMain/kotlin/main.kt
app/composeApp/src/desktopMain/kotlin/ui/theme/Theme.desktop.kt
app/composeApp/src/iosMain/kotlin/DbDriver.kt
app/composeApp/src/iosMain/kotlin/MainViewController.kt
app/composeApp/src/nativeMain/kotlin/App.native.kt
app/composeApp/src/nativeMain/kotlin/ui/theme/Theme.native.kt
app/composeApp/src/wasmJsMain/kotlin/App.wasmJs.kt
app/composeApp/src/wasmJsMain/kotlin/DbDriver.wasmJs.kt
app/composeApp/src/wasmJsMain/kotlin/main.kt
app/composeApp/src/wasmJsMain/kotlin/ui/theme/Theme.wasmJs.kt
app/composeApp/src/wasmJsMain/resources/index.html
app/composeApp/src/wasmJsMain/resources/styles.css
app/composeApp/webpack.config.d/sqljs.js
app/gradle.properties
app/gradle/libs.versions.toml
app/gradle/wrapper/gradle-wrapper.properties
app/gradlew
app/gradlew.bat
app/iosApp/Configuration/Config.xcconfig
app/iosApp/iosApp.xcodeproj/project.pbxproj
app/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
app/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
app/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json
app/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
app/iosApp/iosApp/Assets.xcassets/Contents.json
app/iosApp/iosApp/ContentView.swift
app/iosApp/iosApp/Info.plist
app/iosApp/iosApp/iOSApp.swift
app/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json
app/README.md
app/settings.gradle.kts
backend/.env.example
backend/.gitignore
backend/cache/.gitignore
backend/compose.yml
backend/course.py
backend/generate_requirements.sh
backend/populate_embeddings.py
backend/process_grades.py
backend/pyproject.toml
backend/recommendation.py
backend/requirements.txt
backend/run.sh
backend/scraper.py
backend/uv.lock
LICENSE
[2 more files omitted for size]
```

### Dependencies

- backend/pyproject.toml: beautifulsoup4@>=4.12.3,<5, fastapi[standard]@>=0.112.1,<0.116, google-genai@>=1.20.0, google-genai-haystack@>=1.0.2, haystack-ai@>=2.4.0,<3, lxml@>=5.3.0,<6, pgvector-haystack@>=3.4.0, psycopg2-binary@>=2.9.9,<3, python-dotenv@>=1.0.1,<2, requests@>=2.32.3,<3, sentence-transformers[openvino]@>=3.4.1, supabase@>=2.7.1,<3, torch@~=2.7.1, tqdm@>=4.66.5,<5, transformers[sentencepiece, torch]@>=4.44.1,<5, uvicorn[standard]@>=0.30.6,<0.31
- backend/requirements.txt: accelerate@==1.7.0, aiohappyeyeballs@==2.6.1, aiohttp@==3.12.7, aiosignal@==1.3.2, annotated-types@==0.7.0, anyio@==4.9.0, attrs@==25.3.0, backoff@==2.2.1, beautifulsoup4@==4.13.4, cachetools@==5.5.2, certifi@==2025.4.26, charset-normalizer@==3.4.2, click@==8.2.1, colorama@==0.4.6, deprecation@==2.1.0, distro@==1.9.0, fastapi@==0.112.4, filelock@==3.18.0, filetype@==1.2.0, frozenlist@==1.6.2, fsspec@==2025.5.1, google-ai-generativelanguage@==0.6.6, google-api-core@==2.25.0, google-api-python-client@==2.171.0, google-auth@==2.40.2, google-auth-httplib2@==0.2.0, google-generativeai@==0.7.2, googleapis-common-protos@==1.70.0, gotrue@==2.12.0, grpcio@==1.72.1, grpcio-status@==1.62.3, h11@==0.16.0, h2@==4.2.0, haystack-ai@==2.14.1, haystack-experimental@==0.10.0, hf-xet@==1.1.3, hpack@==4.1.0, httpcore@==1.0.9, httplib2@==0.22.0, httptools@==0.6.4, httpx@==0.28.1, huggingface-hub@==0.32.4, hyperframe@==6.1.0, idna@==3.10, iniconfig@==2.1.0, jinja2@==3.1.6, jiter@==0.10.0, joblib@==1.5.1, jsonschema@==4.24.0, jsonschema-specifications@==2025.4.1, lazy-imports@==0.4.0, lxml@==5.4.0, markupsafe@==3.0.2, more-itertools@==10.7.0, mpmath@==1.3.0, multidict@==6.4.4, networkx@==3.5, numpy@==2.2.6, nvidia-cublas-cu12@==12.1.3.1, nvidia-cuda-cupti-cu12@==12.1.105, nvidia-cuda-nvrtc-cu12@==12.1.105, nvidia-cuda-runtime-cu12@==12.1.105, nvidia-cudnn-cu12@==9.1.0.70, nvidia-cufft-cu12@==11.0.2.54, nvidia-curand-cu12@==10.3.2.106, nvidia-cusolver-cu12@==11.4.5.107, nvidia-cusparse-cu12@==12.1.0.106, nvidia-nccl-cu12@==2.20.5, nvidia-nvjitlink-cu12@==12.9.41, nvidia-nvtx-cu12@==12.1.105, openai@==1.84.0, packaging@==25.0, pgvector@==0.4.1, pgvector-haystack@==0.5.1, pillow@==11.2.1, pluggy@==1.6.0, postgrest@==1.0.2, posthog@==4.2.0, propcache@==0.3.1, proto-plus@==1.26.1, protobuf@==4.25.8, psutil@==7.0.0, psycopg@==3.2.9, psycopg-binary@==3.2.9, psycopg2-binary@==2.9.10, pyasn1@==0.6.1, pyasn1-modules@==0.4.2, pydantic@==2.11.5, pydantic-core@==2.33.2, pygments@==2.19.1, pyjwt@==2.10.1, pyparsing@==3.2.3, pytest@==8.4.0, pytest-mock@==3.14.1, python-dateutil@==2.9.0.post0, python-dotenv@==1.1.0, pyyaml@==6.0.2, realtime@==2.4.3, referencing@==0.36.2, regex@==2024.11.6, requests@==2.32.3, rpds-py@==0.25.1, rsa@==4.9.1, safetensors@==0.5.3, scikit-learn@==1.6.1, scipy@==1.15.3, sentence-transformers@==3.4.1, sentencepiece@==0.2.0, setuptools@==80.9.0, six@==1.17.0, sniffio@==1.3.1, soupsieve@==2.7, starlette@==0.38.6, storage3@==0.11.3, strenum@==0.4.15, supabase@==2.15.2, supafunc@==0.9.4, sympy@==1.14.0, tenacity@==9.1.2, threadpoolctl@==3.6.0, tokenizers@==0.21.1, torch@==2.4.0+cu121, tqdm@==4.67.1, transformers@==4.52.4, triton@==3.0.0, typing-extensions@==4.14.0, typing-inspection@==0.4.1, tzdata@==2025.2, uritemplate@==4.2.0, urllib3@==2.4.0, uvicorn@==0.30.6, uvloop@==0.21.0, watchfiles@==1.0.5, websockets@==14.2, yarl@==1.20.0

### Recent commits (newest first)

- readme
- Update image links in README for SlugCourses
- dep bump
- add auto update
- minor update
- test fix
- updated to use google embeddings, updated deps
- more dep
- more recommendation stuff
- fix build
- does this work?
- boilerplate fix
- dep update
- backend update, rework to use uv
- clean up grades rendering
- clean up code, fix grades rendering, fix initial fouc on web
- fix emoji not rendering
- avoid creating multiple settings objects
- fix screen dimension checks on non-android platforms
- maybe fix wasm screen size check

## Key source files (fetched from GitHub, selected and truncated for size)

### PRIVACY.md

```markdown
This app does not collect, store, or share any personal information. Cached menu data is stored on-device and never uploaded elsewhere.

No advertisement targeting, data mining, or other activities that may compromise user privacy is included in the app.

```

### backend/pyproject.toml

```
[project]
name = "SlugCourses"
version = "1.0"
description = "UCSC course search backend"
authors = [{ name = "prapooskur", email = "prapooskur@gmail.com" }]
requires-python = ">=3.10, ~=3.12"
readme = "README.md"
dependencies = [
    "beautifulsoup4>=4.12.3,<5",
    "lxml>=5.3.0,<6",
    "fastapi[standard]>=0.112.1,<0.116",
    "uvicorn[standard]>=0.30.6,<0.31",
    "tqdm>=4.66.5,<5",
    "supabase>=2.7.1,<3",
    "requests>=2.32.3,<3",
    "haystack-ai>=2.4.0,<3",
    "python-dotenv>=1.0.1,<2",
    "psycopg2-binary>=2.9.9,<3",
    "torch~=2.7.1",
    "transformers[sentencepiece, torch]>=4.44.1,<5",
    "pgvector-haystack>=3.4.0",
    "google-genai>=1.20.0",
    "sentence-transformers[openvino]>=3.4.1",
    "google-genai-haystack>=1.0.2",
]

[tool.uv]
package = false

[[tool.uv.index]]
name = "pytorch"
url = "https://download.pytorch.org/whl/cpu"
explicit = true

[tool.uv.sources]
torch = { index = "pytorch" }

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

```

### backend/requirements.txt

```
accelerate==1.7.0
aiohappyeyeballs==2.6.1
aiohttp==3.12.7
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.9.0
attrs==25.3.0
backoff==2.2.1
beautifulsoup4==4.13.4
cachetools==5.5.2
certifi==2025.4.26
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6 ; sys_platform == 'win32'
deprecation==2.1.0
distro==1.9.0
fastapi==0.112.4
filelock==3.18.0
filetype==1.2.0
frozenlist==1.6.2
fsspec==2025.5.1
google-ai-generativelanguage==0.6.6
google-api-core==2.25.0
google-api-python-client==2.171.0
google-auth==2.40.2
google-auth-httplib2==0.2.0
google-generativeai==0.7.2
googleapis-common-protos==1.70.0
gotrue==2.12.0
grpcio==1.72.1
grpcio-status==1.62.3
h11==0.16.0
h2==4.2.0
haystack-ai==2.14.1
haystack-experimental==0.10.0
hf-xet==1.1.3 ; platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'
hpack==4.1.0
httpcore==1.0.9
httplib2==0.22.0
httptools==0.6.4
httpx==0.28.1
huggingface-hub==0.32.4
hyperframe==6.1.0
idna==3.10
iniconfig==2.1.0
jinja2==3.1.6
jiter==0.10.0
joblib==1.5.1
jsonschema==4.24.0
jsonschema-specifications==2025.4.1
lazy-imports==0.4.0
lxml==5.4.0
markupsafe==3.0.2
more-itertools==10.7.0
mpmath==1.3.0
multidict==6.4.4
networkx==3.5
numpy==2.2.6
nvidia-cublas-cu12==12.1.3.1 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-cuda-cupti-cu12==12.1.105 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-cuda-nvrtc-cu12==12.1.105 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-cuda-runtime-cu12==12.1.105 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-cudnn-cu12==9.1.0.70 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-cufft-cu12==11.0.2.54 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-curand-cu12==10.3.2.106 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-cusolver-cu12==11.4.5.107 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-cusparse-cu12==12.1.0.106 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-nccl-cu12==2.20.5 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-nvjitlink-cu12==12.9.41 ; platform_machine == 'x86_64' and sys_platform == 'linux'
nvidia-nvtx-cu12==12.1.105 ; platform_machine == 'x86_64' and sys_platform == 'linux'
openai==1.84.0
packaging==25.0
pgvector==0.4.1
pgvector-haystack==0.5.1
pillow==11.2.1
pluggy==1.6.0
postgrest==1.0.2
posthog==4.2.0
propcache==0.3.1
proto-plus==1.26.1
protobuf==4.25.8
psutil==7.0.0
psycopg==3.2.9
psycopg-binary==3.2.9 ; implementation_name != 'pypy'
psycopg2-binary==2.9.10
pyasn1==0.6.1
pyasn1-modules==0.4.2
pydantic==2.11.5
pydantic-core==2.33.2
pygments==2.19.1
pyjwt==2.10.1
pyparsing==3.2.3
pytest==8.4.0
pytest-mock==3.14.1
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
pyyaml==6.0.2
realtime==2.4.3
referencing==0.36.2
regex==2024.11.6
requests==2.32.3
rpds-py==0.25.1
rsa==4.9.1
safetensors==0.5.3
scikit-learn==1.6.1
scipy==1.15.3
sentence-transformers==3.4.1
sentencepiece==0.2.0
setuptools==80.9.0
six==1.17.0
sniffio==1.3.1
soupsieve==2.7
starlette==0.38.6
storage3==0.11.3
strenum==0.4.15
supabase==2.15.2
supafunc==0.9.4
sympy==1.14.0
tenacity==9.1.2
threadpoolctl==3.6.0
tokenizers==0.21.1
torch==2.4.0+cu121
tqdm==4.67.1
transformers==4.52.4
triton==3.0.0 ; python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'
typing-extensions==4.14.0
typing-inspection==0.4.1
tzdata==2025.2 ; sys_platform == 'win32'
uritemplate==4.2.0
urllib3==2.4.0
uvicorn==0.30.6
uvloop==0.21.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'
watchfiles==1.0.5
websockets==14.2
yarl==1.20.0

```

### backend/generate_requirements.sh

```shell
#!/bin/bash
toml-to-req --toml-file pyproject.toml --poetry -r requirements.txt

```

### backend/run.sh

```shell
#!/bin/bash
poetry run uvicorn recommendation:classRecommender --host 0.0.0.0 --port 8020 --reload

```

### backend/compose.yml

```yaml
services:
  pgvector:
    container_name: slugcourses_pgvector
    image: pgvector/pgvector:pg17
    shm_size: 128mb
    environment:
      POSTGRES_PASSWORD: Overwrite-Catering5-Shredding-Curfew
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - ./pgvector:/var/lib/postgresql/data
    ports:
      - 34710:5432
    restart: unless-stopped


```

### app/build.gradle.kts

```kotlin
plugins {
    // this is necessary to avoid the plugins to be loaded multiple times
    // in each subproject's classloader
    alias(libs.plugins.androidApplication) apply false
    alias(libs.plugins.androidLibrary) apply false
    alias(libs.plugins.jetbrainsCompose) apply false
    alias(libs.plugins.composeCompiler) apply false
    alias(libs.plugins.kotlinMultiplatform) apply false
    alias(libs.plugins.kotlinSerialization) apply false
    alias(libs.plugins.sqlDelight) apply false
}
```

### backend/course.py

```python
class Course:
    def __init__(self, term, type, subject, number, title, description, gened, requirements, notes):
        self.term = term
        self.type = type
        self.subject = subject
        self.number = number
        self.title = title
        self.description = description
        self.gened = gened
        self.requirements = requirements
        self.notes = notes
    
    def to_dict(self):
        return self.__dict__

    def __str__(self):
        return f"{', '.join(f'{key}={value}' for key, value in self.__dict__.items())}"
```

### app/settings.gradle.kts

```kotlin
rootProject.name = "SlugCourses"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

pluginManagement {
    repositories {
        google {
            mavenContent {
                includeGroupAndSubgroups("androidx")
                includeGroupAndSubgroups("com.android")
                includeGroupAndSubgroups("com.google")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositories {
        google {
            mavenContent {
                includeGroupAndSubgroups("androidx")
                includeGroupAndSubgroups("com.android")
                includeGroupAndSubgroups("com.google")
            }
        }
        mavenCentral()
    }
}

include(":composeApp")
```

### backend/process_grades.py

```python
from dotenv import load_dotenv
from supabase import create_client, Client
from tqdm import tqdm
import os, json, re

def format_name(full_name: str):
    """
    Convert full name (e.g. 'Caroline Brett Casey') to format 'Casey,C.B.'
    
    Args:
        full_name (str): Full name with optional middle name(s)
        
    Returns:
        str: Formatted name with last name, first and middle initials
    """
    name_parts = full_name.split()
    last_name = name_parts[-1]
    initials = '.'.join(part[0] for part in name_parts[:-1])
    return f"{last_name},{initials}."

if __name__ == "__main__":
    load_dotenv()
    url: str = os.environ.get("SUPABASE_URL")
    key: str = os.environ.get("SUPABASE_KEY")
    supabase: Client = create_client(url, key)
    # print(supabase)

    with open('./grades.json', 'r') as grades_file:
        grades = json.load(grades_file)
    
    # print(grades)

    processed_grades = []
    grade_map = {}
    for grade in tqdm(grades, desc="Processing Grades", unit="grade"):
        # print(grade["gradeCounts"])
        dept_number = grade["class"].split(" ")
        # last_names = [name.split()[-1] for name in grade["instructors"]]

        names = [format_name(name) for name in grade["instructors"]]


        full_course_number = dept_number[1]
        course_number = full_course_number

        course_match = re.match(r'(\d+)(\D*)', full_course_number)
        course_letter = ""

        if course_match:
            course_number = course_match.group(1)
            course_letter = course_match.group(2)
        
        processed_grade = {
            "term": str(grade["termCode"]),
            "department": dept_number[0],
            "course_number": course_number,
            "course_letter": course_letter,
            "short_name": grade["title"],
            # join names in reverse order, since that's how pisa does it
            "instructor": ", ".join(names[::-1]),
        }
        for key in grade["gradeCounts"].keys():
            if key != "-":
                if key not in grade_map:
                    grade_map[key] = key.lower().replace("+", "_plus").replace("-", "_minus")
                processed_grade[grade_map[key]] = grade["gradeCounts"][key]
        # print(processed_grade)
        processed_grades.append(processed_grade)
        # print(processed_grade)
    supabase.table("grades").upsert(processed_grades).execute()

```

[78 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]