Compare commits
4 commits
6cd0e0ba0f
...
411421f2b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
411421f2b6 | ||
|
|
18734726cb | ||
|
|
608bf086ec | ||
|
|
4d9af1d25c |
13 changed files with 270 additions and 5 deletions
3
.env.example
Normal file
3
.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
||||||
|
NEXT_PUBLIC_N8N_WEBHOOK_URL=your_n8n_webhook_url
|
||||||
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
17
README.md
17
README.md
|
|
@ -1,7 +1,14 @@
|
||||||
# Semillero 2 - AI ATS
|
# AI Recruitment Platform (ATS)
|
||||||
|
|
||||||
This repository contains the AI ATS project.
|
## Project Description
|
||||||
|
A modern ATS platform designed to parse PDF CVs using multimodal AI, rank candidates against job vacancies using semantic vector search, and orchestrate automated recruitment stages via an external n8n hub.
|
||||||
|
|
||||||
## Remote Sync Details
|
## Core User Stories
|
||||||
- **Fetch:** Private Git server (`git.gaboggamer.online`)
|
- **Recruiter - CV Upload:** As a recruiter, I want to upload CVs in PDF format so they can be evaluated automatically.
|
||||||
- **Push:** Private Git server and GitHub (`github.com`)
|
- **Recruiter - Vacancy Ranking:** As a recruiter, I want a ranking of candidates per job vacancy.
|
||||||
|
- **Recruiter - Seniority Detection:** As a recruiter, I want the system to detect seniority to adjust interviews.
|
||||||
|
- **Recruiter - Profile Summary:** As a recruiter, I want a concise AI summary of the profile for quick review.
|
||||||
|
- **Hiring Manager - Comparative Scoring:** As a hiring manager, I want a comparative score between candidates.
|
||||||
|
- **Recruiter - Stage Automation:** As a recruiter, I want candidates to move through recruitment stages automatically.
|
||||||
|
- **Candidate - Automated Emails:** As a candidate, I want to receive automated email confirmations for every stage change.
|
||||||
|
- **Talent Team - Vacancy Metrics:** As a talent team, we want metrics on the progress per vacancy.
|
||||||
|
|
|
||||||
8
app/(dashboard)/candidates/page.tsx
Normal file
8
app/(dashboard)/candidates/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default function CandidatesPage() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-200">
|
||||||
|
<h1 className="text-xl font-bold text-slate-900 mb-2">Candidates</h1>
|
||||||
|
<p className="text-slate-600 text-sm">View and evaluate parsed candidate profiles.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
app/(dashboard)/interviews/page.tsx
Normal file
8
app/(dashboard)/interviews/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default function InterviewsPage() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-200">
|
||||||
|
<h1 className="text-xl font-bold text-slate-900 mb-2">Interviews</h1>
|
||||||
|
<p className="text-slate-600 text-sm">Schedule and monitor candidate evaluations.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
app/(dashboard)/jobs/page.tsx
Normal file
8
app/(dashboard)/jobs/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default function JobsPage() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-200">
|
||||||
|
<h1 className="text-xl font-bold text-slate-900 mb-2">Jobs</h1>
|
||||||
|
<p className="text-slate-600 text-sm">Manage job vacancies and candidate matches.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/(dashboard)/layout.tsx
Normal file
36
app/(dashboard)/layout.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function DashboardLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-50 flex flex-col">
|
||||||
|
<header className="bg-white border-b border-slate-200 shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<span className="text-slate-900 font-bold text-lg">AI Recruitment</span>
|
||||||
|
<nav className="flex gap-4">
|
||||||
|
<Link href="/jobs" className="text-sm text-slate-600 hover:text-slate-900 font-medium">
|
||||||
|
Jobs
|
||||||
|
</Link>
|
||||||
|
<Link href="/candidates" className="text-sm text-slate-600 hover:text-slate-900 font-medium">
|
||||||
|
Candidates
|
||||||
|
</Link>
|
||||||
|
<Link href="/interviews" className="text-sm text-slate-600 hover:text-slate-900 font-medium">
|
||||||
|
Interviews
|
||||||
|
</Link>
|
||||||
|
<Link href="/workflows" className="text-sm text-slate-600 hover:text-slate-900 font-medium">
|
||||||
|
Workflows
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="flex-1 max-w-7xl mx-auto w-full p-6 sm:p-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
app/(dashboard)/workflows/page.tsx
Normal file
8
app/(dashboard)/workflows/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default function WorkflowsPage() {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-200">
|
||||||
|
<h1 className="text-xl font-bold text-slate-900 mb-2">Workflows</h1>
|
||||||
|
<p className="text-slate-600 text-sm">Configure automated recruitment pipelines orchestrated by n8n.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
5
app/globals.css
Normal file
5
app/globals.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-slate-50 text-slate-600 antialiased;
|
||||||
|
}
|
||||||
33
app/layout.tsx
Normal file
33
app/layout.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
|
>
|
||||||
|
<body className="min-h-full flex flex-col">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
app/page.tsx
Normal file
22
app/page.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-center bg-slate-50 p-6">
|
||||||
|
<div className="w-full max-w-md bg-white p-8 rounded-lg shadow-sm border border-slate-200 text-center">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900 mb-2">
|
||||||
|
AI Recruitment Platform
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-600 mb-6 text-sm">
|
||||||
|
Welcome to the ATS. Access your workspace below.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/jobs"
|
||||||
|
className="inline-block w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md shadow-sm transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Go to Dashboard
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
supabase/migrations/00001_initial_schema.sql
Normal file
85
supabase/migrations/00001_initial_schema.sql
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
-- Enable the pgvector extension to work with embeddings
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
|
|
||||||
|
-- Users Table (Recruiters / Managers)
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
role TEXT NOT NULL CHECK (role IN ('recruiter', 'manager')),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Jobs Table
|
||||||
|
CREATE TABLE IF NOT EXISTS jobs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
requirements TEXT,
|
||||||
|
embedding vector(1536),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Candidates Table
|
||||||
|
CREATE TABLE IF NOT EXISTS candidates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
phone TEXT,
|
||||||
|
resume_url TEXT,
|
||||||
|
summary TEXT,
|
||||||
|
seniority TEXT,
|
||||||
|
embedding vector(1536),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Interviews Table
|
||||||
|
CREATE TABLE IF NOT EXISTS interviews (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
job_id UUID REFERENCES jobs(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
status TEXT NOT NULL CHECK (status IN ('scheduled', 'completed', 'cancelled')) DEFAULT 'scheduled',
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Scores Table
|
||||||
|
CREATE TABLE IF NOT EXISTS scores (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
candidate_id UUID REFERENCES candidates(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
job_id UUID REFERENCES jobs(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
interviewer_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
score NUMERIC NOT NULL CHECK (score >= 0 AND score <= 100),
|
||||||
|
evaluation JSONB, -- MUST contain exactly: summary, classification, suggestions, riskLevel
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RPC Function to Match Candidates using Cosine Distance
|
||||||
|
CREATE OR REPLACE FUNCTION match_candidates (
|
||||||
|
query_embedding vector(1536),
|
||||||
|
match_threshold FLOAT,
|
||||||
|
match_count INT
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
id UUID,
|
||||||
|
name TEXT,
|
||||||
|
email TEXT,
|
||||||
|
seniority TEXT,
|
||||||
|
similarity FLOAT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
candidates.id,
|
||||||
|
candidates.name,
|
||||||
|
candidates.email,
|
||||||
|
candidates.seniority,
|
||||||
|
(1 - (candidates.embedding <=> query_embedding))::FLOAT AS similarity
|
||||||
|
FROM candidates
|
||||||
|
WHERE (1 - (candidates.embedding <=> query_embedding)) > match_threshold
|
||||||
|
ORDER BY candidates.embedding <=> query_embedding ASC
|
||||||
|
LIMIT match_count;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
Loading…
Reference in a new issue