feat(foundations): init schema frontend components and quality gates
This commit is contained in:
parent
84a8cf52d7
commit
dd7e0ff198
10 changed files with 275 additions and 6 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
// @ts-ignore
|
||||
import pdf from "pdf-parse";
|
||||
import { PDFParse } from "pdf-parse";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -17,9 +16,11 @@ export async function POST(request: NextRequest) {
|
|||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Extract text from PDF
|
||||
const pdfData = await pdf(buffer);
|
||||
// Extract text from PDF using PDFParse v2 API
|
||||
const parser = new PDFParse({ data: buffer });
|
||||
const pdfData = await parser.getText();
|
||||
const text = pdfData.text;
|
||||
await parser.destroy();
|
||||
|
||||
// Extract candidate name from file name (strip extension)
|
||||
const name = file.name.replace(/\.[^/.]+$/, "");
|
||||
|
|
@ -75,8 +76,9 @@ export async function POST(request: NextRequest) {
|
|||
message: "CVs processed and forwarded to n8n successfully",
|
||||
data: responseData,
|
||||
});
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Error in parse-cv route:", error);
|
||||
return NextResponse.json({ error: error.message || "Internal server error" }, { status: 500 });
|
||||
const errorMessage = error instanceof Error ? error.message : "Internal server error";
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
components/DataCard.tsx
Normal file
17
components/DataCard.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
|
||||
interface DataCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DataCard({ title, description, children }: DataCardProps) {
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-slate-50">
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-1">{title}</h3>
|
||||
<p className="text-slate-600 text-sm mb-4">{description}</p>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
components/PrimaryButton.tsx
Normal file
16
components/PrimaryButton.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
|
||||
interface PrimaryButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PrimaryButton({ children, className = "", ...props }: PrimaryButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md shadow-sm transition-colors text-sm ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
20
components/StatusBadge.tsx
Normal file
20
components/StatusBadge.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from "react";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
label: string;
|
||||
variant?: "primary" | "secondary";
|
||||
}
|
||||
|
||||
export function StatusBadge({ label, variant = "primary" }: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
variant === "primary"
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-slate-50 text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
50
docs/discovery-and-scope.md
Normal file
50
docs/discovery-and-scope.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Phase 1: Discovery & Scope
|
||||
|
||||
This document establishes the business boundaries, user personas, backlog of user stories, and Definition of Done (DoD) for the AI Recruitment Platform.
|
||||
|
||||
## User Personas
|
||||
|
||||
* **Technical Recruiter**: Focuses on sourcing, intake, initial AI-assisted screening, seniority verification, and tracking candidates through the hiring pipeline.
|
||||
* **Hiring Manager**: Focuses on reviewing pre-screened and scored candidates, evaluating role suitability, and conducting structured interviews.
|
||||
* **Candidate**: Interacts with the platform to submit applications/CVs and receives automated status updates.
|
||||
|
||||
## Prioritized Backlog of User Stories
|
||||
|
||||
1. **CV Intake**
|
||||
* *User Story*: As a recruiter, I want to upload candidate PDFs via the Web UI so they can be processed and stored.
|
||||
* *Acceptance Criteria*:
|
||||
* Drag-and-drop or file selector for PDF CVs.
|
||||
* Successful text extraction and ingestion.
|
||||
* Parsing and storage of extracted candidate info.
|
||||
2. **Semantic Ranking**
|
||||
* *User Story*: As a recruiter, I want a ranked list of candidates matching an open job description based on contextual relevance.
|
||||
* *Acceptance Criteria*:
|
||||
* Compute cosine distance between job embeddings and candidate embeddings.
|
||||
* Display ranked matches with normalized similarity score between -1 and 1.
|
||||
3. **Profile Summary**
|
||||
* *User Story*: As a recruiter, I want a terse AI-generated candidate summary to speed up screening.
|
||||
* *Acceptance Criteria*:
|
||||
* Display a short summary on candidate profiles.
|
||||
* Synthesized from parsed resume details.
|
||||
4. **Seniority Guard**
|
||||
* *User Story*: As a recruiter, I want automatic seniority detection to route candidates to the correct interview pool.
|
||||
* *Acceptance Criteria*:
|
||||
* AI-based classification (e.g., Junior, Mid, Senior).
|
||||
* Correct pool routing matches candidate seniority level.
|
||||
5. **Unified Scoring**
|
||||
* *User Story*: As a hiring manager, I want to compare candidate suitability using a standardized score.
|
||||
* *Acceptance Criteria*:
|
||||
* Standardized scoring schema with AI evaluation.
|
||||
* JSON structure containing keys: `summary`, `classification`, `suggestions`, and `riskLevel`.
|
||||
6. **Stage Progression**
|
||||
* *User Story*: As a recruiter, I want candidate stages to update dynamically, triggering confirmation emails.
|
||||
* *Acceptance Criteria*:
|
||||
* Visual stage progression pipeline.
|
||||
* Updating stage triggers background event notifications.
|
||||
|
||||
## Definition of Done (DoD)
|
||||
|
||||
* **Type Safety**: Fully typed Next.js App Router with TypeScript (no `any` types where avoidable).
|
||||
* **Database Schema**: Supabase relational database schema complete with verified `pgvector` distance metrics.
|
||||
* **Linting & Quality**: Zero TypeScript compilation or linting warnings/errors.
|
||||
* **Git Quality Gates**: Active Git quality gates (hooks) verifying builds, conventional commit messages, linting, and formatting.
|
||||
|
|
@ -12,6 +12,7 @@ const eslintConfig = defineConfig([
|
|||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
"scripts/**",
|
||||
]),
|
||||
]);
|
||||
|
||||
|
|
|
|||
48
lib/logger/index.ts
Normal file
48
lib/logger/index.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
export interface LogPayload {
|
||||
message: string;
|
||||
error?: Error | unknown;
|
||||
latencyMs?: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private static format(level: "INFO" | "WARN" | "ERROR", payload: LogPayload): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
const parts: string[] = [`[${timestamp}] [${level}] ${payload.message}`];
|
||||
|
||||
if (payload.latencyMs !== undefined) {
|
||||
parts.push(`(Latency: ${payload.latencyMs}ms)`);
|
||||
}
|
||||
|
||||
if (payload.error) {
|
||||
const errorMsg =
|
||||
payload.error instanceof Error
|
||||
? payload.error.stack || payload.error.message
|
||||
: String(payload.error);
|
||||
parts.push(`\nError: ${errorMsg}`);
|
||||
}
|
||||
|
||||
if (payload.metadata && Object.keys(payload.metadata).length > 0) {
|
||||
parts.push(`\nMetadata: ${JSON.stringify(payload.metadata, null, 2)}`);
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
static info(message: string, metadata?: Record<string, unknown>, latencyMs?: number) {
|
||||
console.log(this.format("INFO", { message, metadata, latencyMs }));
|
||||
}
|
||||
|
||||
static warn(message: string, metadata?: Record<string, unknown>, latencyMs?: number) {
|
||||
console.warn(this.format("WARN", { message, metadata, latencyMs }));
|
||||
}
|
||||
|
||||
static error(
|
||||
message: string,
|
||||
error?: Error | unknown,
|
||||
metadata?: Record<string, unknown>,
|
||||
latencyMs?: number
|
||||
) {
|
||||
console.error(this.format("ERROR", { message, error, metadata, latencyMs }));
|
||||
}
|
||||
}
|
||||
86
supabase/migrations/20260608000000_initial_schema.sql
Normal file
86
supabase/migrations/20260608000000_initial_schema.sql
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
-- Enable pgvector extension
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- Recruiters table
|
||||
CREATE TABLE IF NOT EXISTS recruiters (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Jobs table
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
requirements JSONB NOT NULL,
|
||||
embedding vector(1536),
|
||||
recruiter_id UUID REFERENCES recruiters(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Candidates table
|
||||
CREATE TABLE IF NOT EXISTS candidates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
contact_info JSONB NOT NULL,
|
||||
embedding vector(1536),
|
||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Interviews table
|
||||
CREATE TABLE IF NOT EXISTS interviews (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
candidate_id UUID NOT NULL REFERENCES candidates(id) ON DELETE CASCADE,
|
||||
job_id UUID NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
||||
interview_date TIMESTAMPTZ NOT NULL,
|
||||
stage TEXT NOT NULL, -- Technical, Cultural, etc.
|
||||
feedback TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Scores table
|
||||
CREATE TABLE IF NOT EXISTS scores (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
candidate_id UUID NOT NULL REFERENCES candidates(id) ON DELETE CASCADE,
|
||||
interview_id UUID REFERENCES interviews(id) ON DELETE CASCADE,
|
||||
ai_score FLOAT NOT NULL,
|
||||
evaluation JSONB NOT NULL, -- Hold summary, classification, suggestions, riskLevel
|
||||
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
|
||||
CONSTRAINT check_evaluation_schema CHECK (
|
||||
evaluation ? 'summary' AND
|
||||
evaluation ? 'classification' AND
|
||||
evaluation ? 'suggestions' AND
|
||||
evaluation ? 'riskLevel'
|
||||
)
|
||||
);
|
||||
|
||||
-- Cosine distance match function
|
||||
CREATE OR REPLACE FUNCTION match_candidates(
|
||||
query_embedding vector(1536),
|
||||
match_threshold float,
|
||||
match_count int
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id uuid,
|
||||
name text,
|
||||
contact_info jsonb,
|
||||
embedding vector(1536),
|
||||
similarity float
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
candidates.id,
|
||||
candidates.name,
|
||||
candidates.contact_info,
|
||||
candidates.embedding,
|
||||
(1 - (candidates.embedding <=> query_embedding))::float AS similarity
|
||||
FROM candidates
|
||||
WHERE (1 - (candidates.embedding <=> query_embedding)) > match_threshold
|
||||
ORDER BY similarity DESC
|
||||
LIMIT match_count;
|
||||
END;
|
||||
$$;
|
||||
28
tailwind.config.ts
Normal file
28
tailwind.config.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
// Overriding the default theme colors to restrict to the whitelist.
|
||||
colors: {
|
||||
transparent: "transparent",
|
||||
current: "currentColor",
|
||||
white: "#ffffff",
|
||||
slate: {
|
||||
50: "#f8fafc",
|
||||
600: "#475569",
|
||||
900: "#0f172a",
|
||||
},
|
||||
blue: {
|
||||
600: "#2563eb",
|
||||
700: "#1d4ed8",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
types.d.ts
vendored
Normal file
1
types.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
declare module 'pdf-parse';
|
||||
Loading…
Reference in a new issue