Appearance
Epic 6 — Discover Feed (AI Matching)
BE-DISCOVER-001 — AI Matching service (9-factor scoring)
- [x] Implemented
Files:
- Create:
src/services/ai/matching.ts— full implementation replacing stub - Edit:
src/graphql/resolvers/job.ts—discoverFeedcalls real matching service
9 scoring factors (per spec doc):
- Salary alignment — compare
job.paymentAmountrange vs talent's expected rate - Work authorization — match
job.requirements.workAuthorizationvstalent.workAuthorization - Physical attributes — match age range, gender, height, body type if job specifies
- Notable works relevance — semantic similarity (vector search) between talent's works and job genre/type
- Production experience — match production type + subtype experience
- Awards & nominations — presence and relevance
- Dream gig alignment — semantic match of
talent.dreamGigsvs job description - Language/accent proficiency — match required languages/accents
- Availability —
talent.availableFromvsjob.startDate
Dev mode: Falls back to weighted DB filter. Production: calls Qdrant (self-hosted, same Hetzner machine) for factors 4, 7.
Returns score (0–1) and matchReasons: string[] (human-readable explanation per matched factor).
BE-DISCOVER-002 — Vector embedding pipeline for talents
- [x] Implemented
Files:
- Create:
src/services/ai/embeddings.ts— OpenAItext-embedding-3-smallcalls - Create:
src/services/ai/vectorStore.ts— Qdrant upsert/query abstraction - Edit:
src/services/talent/index.ts— callembeddings.indexTalent(profile)after profile updates
Description: When a talent profile is created or updated, generate a text embedding from: displayName + primaryCategory + skills + experienceEntries + notableWorks + dreamGigs. Upsert into Qdrant with talentId as the key. The discover feed uses these embeddings for factors 4 and 7.
Infrastructure: Qdrant runs as a Docker container on the same Hetzner machine as the backend. Requires ~200 MB RAM idle — fits comfortably on the existing CX22 (4 GB RAM) alongside the backend and face-comparison sidecar. No separate node needed.
Environment variables:
env
QDRANT_URL=http://localhost:6333 # internal — not exposed to the internet
OPENAI_API_KEY=<key> # for text-embedding-3-smallCoolify setup: Add a Qdrant service to the existing Docker Compose stack:
yaml
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
restart: unless-stoppedBE-DISCOVER-003 — Extend AI matching to use new structured fields
- [x] Implemented
Files:
- Edit:
src/services/ai/matching.ts— update factors 1, 2, 8 to use new field data - Edit:
src/__tests__/services/matching.test.ts
Factor updates:
| Factor | Before | After |
|---|---|---|
| 1 — Salary alignment | job.paymentAmount vs rough talent expectation | job.paymentAmount vs talent.salaryExpectation.{ min, max, currency, period } — normalise to same period before comparing |
| 2 — Work authorization | job.requirements.workAuthorization[] vs talent.workAuthorization[] | + talent.passportVisa.{ passports[], visas[], workPermits[] } — broader match surface |
| 8 — Language/accent | language match only (accent field didn't exist) | + talent.accentDialect[] matched against job.requirements.languages[].accent |
No schema or API changes — purely service logic update.
BE-SEARCH-001 — Extend talent search filters (notable works, awards, dream job)
- [x] Implemented
Files:
- Edit:
src/graphql/schema/index.ts— extendTalentSearchFiltersinput type - Edit:
src/services/talent/search.ts(or whereversearchTalentsis resolved) — add filter logic - Edit:
src/__tests__/resolvers/search.test.ts
New filter fields:
graphql
input TalentSearchFilters {
# ...existing fields...
hasNotableWork: String # partial match on notableWorks[].title
hasAward: Boolean # talent has at least one award entry
dreamJobKeyword: String # semantic match against dreamJobs[].title
}Implementation:
hasNotableWork: PostgreSQLJSON_ARRAY_ELEMENTS+ILIKEonnotableWorksJSON array.hasAward:WHERE awards IS NOT NULL AND awards != '[]'.dreamJobKeyword: vector similarity search via Qdrant — embed the keyword and compare to talent dream-job embeddings. Falls back to JSONILIKEin dev mode.
FE-DISCOVER-001 — Discover feed UI with swipe stack (producer)
- [x] Implemented
Files:
- Edit:
apps/app/src/pages/discover/DiscoverPage.tsx— replace placeholder - Create:
apps/app/src/lib/queries/discover.ts—DISCOVER_FEED_QUERY - Create:
apps/app/src/hooks/useDiscoverFeed.ts - Create:
apps/app/src/hooks/useSwipeTalent.ts
Description: Replace "Home feed coming soon" with the real discover feed. Shows a stack of TalentCard components (from DS). Swipe right → shortlist, swipe left → skip, tap × → block. After each swipe, call swipeTalent mutation and advance to next card. When stack runs low (< 3 cards), pre-fetch more. Shows EmptyState when no more talents to show. Matches "Talento Fluxo" / "Produtor Fluxo" frames in Figma.
Note: This view is producer-only. Talent users see a different "home" (job listings or notifications feed). Route guard: redirect talents to /jobs.
FE-DISCOVER-002 — Talent home feed (job recommendations)
- [x] Implemented
Files:
- Create:
apps/app/src/pages/discover/TalentHomePage.tsx - Edit:
apps/app/src/pages/discover/DiscoverPage.tsx— switch view by role
Description: Talent users' home view: personalized job recommendations shown as JobCard list, ranked by relevance to their profile. Uses jobs query with talent-profile-based pre-filters. Matches the talent's "Jornada" home screen in Figma.