An AI-powered resource finder for University of Washington students. Describe your situation in plain language and get pointed to the right official UW resources, with explanations and a clear next step.
CSS 382 — Introduction to Artificial Intelligence · DYOP group project
- App: uw-compass-c49yf22kw-aryank09s-projects.vercel.app — or run locally with
npm run dev - About / how it works: /about on the deployed app
- Repository: https://github.com/aryank09/UW-Compass
npm install
cp .env.example .env.local # then put your OpenAI key in .env.local
npm run seed # generates embeddings for the curated resource set
npm run dev # http://localhost:3000You need an OpenAI API key with billing enabled. Seeding all 31 resources costs a fraction of a cent on text-embedding-3-small.
- Student types something like "I'm overwhelmed, behind in math, and need a quiet place to study."
- The app extracts structured needs (
academic,study_space) with intensities and supporting evidence usinggpt-4o-mini+ function calling. - In parallel, it embeds the same input with
text-embedding-3-small. - It computes a combined score against every resource — embedding similarity, category match, tag overlap, urgency boost — then takes the top 5 with category diversification.
gpt-4o-miniwrites a short per-resource explanation and a 2–4 step action plan.- The UI shows the extracted needs, ranked recommendations with links to official UW pages, and the next-step plan.
flowchart TB
Browser["Browser (page.tsx)"]
API["app/api/recommend/route.ts"]
Embed["lib/ai.ts → text-embedding-3-small"]
Extract["lib/ai.ts → gpt-4o-mini<br/>(need extraction, function calling)"]
DB["lib/db.ts<br/>data/resources.embedded.json"]
Rank["lib/recommend.ts<br/>cosine + tags + category + urgency"]
Summarize["lib/ai.ts → gpt-4o-mini<br/>(per-resource why + next steps)"]
Browser -- "POST { input }" --> API
API --> Embed
API --> Extract
Embed --> Rank
Extract --> Rank
DB --> Rank
Rank -- "top 5 + needs" --> Summarize
Summarize -- "{ needs, recommendations, next_steps }" --> Browser
Three OpenAI calls per request: 1 embedding + 2 chat completions (embed and need extraction run in parallel). Resource embeddings are computed once at seed time. Optional power-user flags (two_pass, use_ai_ranker) add 1–2 extra calls.
app/
layout.tsx root layout
page.tsx client-side form + results UI
about/page.tsx project website (overview, AI, user guide)
globals.css tailwind entrypoint
api/recommend/route.ts POST endpoint
lib/
types.ts Resource, ExtractedNeed, Recommendation, Category
db.ts reads data/resources.embedded.json at build time
ai.ts OpenAI calls: embed, extractNeeds, summarize
recommend.ts cosine similarity + ranking + diversification
data/
resources.json curated UW resources (edit this to add/update)
resources.embedded.json generated by `npm run seed` — committed so Vercel builds work
scripts/
seed.ts reads resources.json, computes embeddings, writes embedded JSON
check-links.ts HEAD-checks every URL in resources.json
tests/
recommend.test.ts ranker unit tests
resources.test.ts schema validation + UW-domain check
scenarios.test.ts the 5 proposal evaluation scenarios
Resources live in data/resources.json. Each entry has:
{
id: string; // stable slug, used as primary key
name: string; // display name
category: 'academic' | 'wellness' | 'basic_needs' | 'transportation'
| 'study_space' | 'career' | 'financial';
campus: 'seattle' | 'bothell' | 'tacoma' | 'all';
description: string; // 1–3 sentences; this is what gets embedded
url: string; // official UW page
tags: string[]; // snake_case fine-grained tags
urgent: boolean; // true for crisis-time resources (SafeCampus, Husky HelpLine, etc.)
}After editing, re-run npm run seed to refresh embeddings, then npm run check-links to verify URLs.
Important:
data/resources.embedded.jsonis a generated artefact that is committed to the repository so Vercel builds work without an OpenAI key. If you editdata/resources.jsonand forget to runnpm run seed, the deployed app will silently use stale embeddings — the resource list will look correct but semantic search quality degrades. Always commit the updatedresources.embedded.jsonalongside anyresources.jsonchanges.
Weights live in lib/recommend.ts:
weights: {
embedding: 0.5, // semantic similarity (cosine of student input vs. resource description)
categoryMatch: 0.25, // does the resource's category match an extracted need?
tagOverlap: 0.15, // share of extracted tags that appear in the resource's tag list
urgencyBoost: 0.1, // student appears urgent AND resource is flagged urgent
}Adjust and re-run npm test to check that all 5 evaluation scenarios still pass.
Returns a streaming NDJSON response (one JSON object per line). Non-success responses (4xx / 5xx) are plain JSON.
Request body
| Field | Type | Default | Description |
|---|---|---|---|
input |
string (3–2000 chars) |
required | Student's free-text description |
campus |
"all" | "seattle" | "bothell" | "tacoma" |
"all" |
Filter resources by campus |
advisor |
boolean |
false |
Include per-signal scores and full ranked list |
shareQuery |
boolean |
false |
Anonymously log the query to the gallery |
prior_needs |
ExtractedNeed[] |
— | Needs from a previous turn; merged with fresh extraction |
student_context |
{ year?, commuter?, first_gen? } |
— | Persona hints for need extraction |
two_pass |
boolean |
false |
Run a second self-critique pass on extracted needs |
use_ai_ranker |
boolean |
false |
Add an LLM re-ranking pass on top of cosine |
Stream events
type |
Payload | When |
|---|---|---|
needs |
{ needs: ExtractedNeed[] } |
After embed + extract complete (~1 s) |
done |
Full RecommendResponse fields |
After summarization completes |
error |
{ error: string } |
If the pipeline fails mid-stream |
Advisor mode — send ?advisor=1 in the browser URL or advisor: true in the body to receive advisorData.allResults (all resources with per-signal scores) and meta (token counts, latency, ranker label).
| Field | Type |
|---|---|
resourceId |
string |
query |
string |
campus |
string |
helpful |
boolean |
Rate-limited to 50 votes/min per IP.
Returns { queries: string[] } — the last 50 anonymously shared queries (requires KV; returns [] without it).
npm test # full test suite (Vitest)
npm run check-links # HEAD-checks every URL in resources.json
npm run typecheck # tsc --noEmit
npm run lint # ESLint
npm run format # Prettier (auto-fix)The scenario tests bypass OpenAI by hand-crafting expected ExtractedNeed[] for each of the proposal's five evaluation scenarios (§9) and asserting the ranker surfaces matching resources in the top 5.
Manual smoke test against a running dev server:
curl -s -X POST http://localhost:3000/api/recommend \
-H "content-type: application/json" \
-d '{"input":"I am failing calculus and need help."}' | jqThe app deploys directly to Vercel — no database server needed because resource embeddings are bundled as data/resources.embedded.json at build time.
- Push to
main. - Import the repo at vercel.com/new (you need push access).
- Add
OPENAI_API_KEYunder Environment Variables. - Deploy. Every subsequent push to
mainauto-deploys.
If your GitHub account doesn't own the repo, the CLI alternative (vercel login && vercel --prod) works from any clone.
Set a monthly spend cap at platform.openai.com/settings/organization/limits so a runaway script can't drain your account.
- Week 1 — repo + seeded resource DB + working
/api/recommend - Week 2 — end-to-end AI pipeline + frontend + 5 scenarios verified
- Week 3 — public deployment, project website, final polish
- 31 curated resources across all 7 proposal categories
- OpenAI embedding + need-extraction + summarization pipeline
- Ranked recommendations with multi-signal scoring + category diversification
- Single-page React UI with example prompts, urgent banner, next-step plan
- Project website at
/about(overview, impact, architecture, AI explanation, user guide) - Vitest test suite — 60+ tests covering ranker, schema, scenarios, i18n, iCal, and rate limiter
- Link-health checker script (mitigates the "resources go stale" risk from §15)
- Campus filter (All / Seattle / Bothell / Tacoma) — pill selector above the input
- Feedback buttons ("helpful" / "not helpful") with anonymous logging (KV or local
.jsonl) - Last result persisted in
localStorage— automatically restored on next visit - Multilingual UI (en / es / zh / vi / ko); AI summarization also responds in the student's language
Educational project. Resource content links to official UW pages and is not a substitute for them. For emergencies call 911 or contact SafeCampus.