Compare commits

..

No commits in common. "411421f2b6498ca37d960bd593b218878b6d8a0f" and "6cd0e0ba0f80edbc0586f7c1344dc6ce69843e02" have entirely different histories.

13 changed files with 5 additions and 270 deletions

View file

@ -1,3 +0,0 @@
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
View file

@ -1,42 +0,0 @@
# 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

View file

@ -1,14 +1,7 @@
# AI Recruitment Platform (ATS) # Semillero 2 - AI ATS
## Project Description This repository contains the AI ATS project.
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.
## Core User Stories ## Remote Sync Details
- **Recruiter - CV Upload:** As a recruiter, I want to upload CVs in PDF format so they can be evaluated automatically. - **Fetch:** Private Git server (`git.gaboggamer.online`)
- **Recruiter - Vacancy Ranking:** As a recruiter, I want a ranking of candidates per job vacancy. - **Push:** Private Git server and GitHub (`github.com`)
- **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.

View file

@ -1,8 +0,0 @@
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>
);
}

View file

@ -1,8 +0,0 @@
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>
);
}

View file

@ -1,8 +0,0 @@
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>
);
}

View file

@ -1,36 +0,0 @@
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>
);
}

View file

@ -1,8 +0,0 @@
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>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,5 +0,0 @@
@import "tailwindcss";
body {
@apply bg-slate-50 text-slate-600 antialiased;
}

View file

@ -1,33 +0,0 @@
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>
);
}

View file

@ -1,22 +0,0 @@
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>
);
}

View file

@ -1,85 +0,0 @@
-- 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;
$$;