Abhisingh-18 commited on
Commit
cc6cee7
·
verified ·
1 Parent(s): c792ad3

Mirror of github.com/Abhisingh18/HunterAI

Browse files
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ venv/
2
+ node_modules/
3
+ __pycache__/
4
+ .env
5
+ .DS_Store
6
+ uploads/
7
+ *.pdf
8
+ *.xlsx
9
+ backend/uploads/
README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #Deployed link: https://hunterai-1.onrender.com
2
+ # HunterAI
3
+ An intelligent AI system that automates job and freelancing cold email outreach by leveraging resume parsing, company data analysis, and a multi-model LLM architecture. The system uses local LLMs (Ollama + Mistral) to generate highly personalized, professional emails with zero API dependency.
backend/hunter_backend.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ # Trigger reload
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import os
5
+ import shutil
6
+ import time
7
+ from dotenv import load_dotenv
8
+
9
+ # Load env vars *before* importing services that might need them
10
+ load_dotenv()
11
+
12
+ from services.resume_parser import parse_resume
13
+ from services.excel_reader import read_company_excel
14
+ from services.ai_engine import generate_cold_email
15
+ from services.email_sender import send_email
16
+ from pydantic import BaseModel
17
+ from typing import List, Dict, Any, Optional
18
+
19
+
20
+ app = FastAPI(title="Hunter AI Backend")
21
+
22
+ origins = [
23
+ "http://localhost:5173",
24
+ "http://localhost:3000",
25
+ os.getenv("FRONTEND_URL", "http://localhost:5173"), # Allow Render frontend
26
+ ]
27
+
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=origins,
31
+ allow_credentials=True,
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+ UPLOAD_DIR = "uploads"
37
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
38
+
39
+ @app.get("/")
40
+ def read_root():
41
+ return "AI Outreach Backend is running 🚀"
42
+
43
+ @app.post("/upload")
44
+ async def upload_files(resume: UploadFile = File(...), company_excel: UploadFile = File(...)):
45
+ resume_path = os.path.join(UPLOAD_DIR, resume.filename)
46
+ excel_path = os.path.join(UPLOAD_DIR, company_excel.filename)
47
+
48
+ with open(resume_path, "wb") as buffer:
49
+ shutil.copyfileobj(resume.file, buffer)
50
+
51
+ with open(excel_path, "wb") as buffer:
52
+ shutil.copyfileobj(company_excel.file, buffer)
53
+
54
+ resume_text = parse_resume(resume_path)
55
+ companies = read_company_excel(excel_path)
56
+
57
+ return {
58
+ "status": "success",
59
+ "resume_filename": resume.filename,
60
+ "excel_filename": company_excel.filename,
61
+ "resume_preview": resume_text[:500] if resume_text else "No text extracted",
62
+ "companies_count": len(companies),
63
+ "first_company_example": companies[0] if companies else None
64
+ }
65
+
66
+ class GenerateRequest(BaseModel):
67
+ resume_filename: str
68
+ excel_filename: str
69
+
70
+ @app.post("/generate-emails")
71
+ async def generate_emails_endpoint(request: GenerateRequest):
72
+ resume_path = os.path.join(UPLOAD_DIR, request.resume_filename)
73
+ excel_path = os.path.join(UPLOAD_DIR, request.excel_filename)
74
+
75
+ if not os.path.exists(resume_path) or not os.path.exists(excel_path):
76
+ raise HTTPException(status_code=404, detail="Files not found")
77
+
78
+ resume_text = parse_resume(resume_path)
79
+ companies = read_company_excel(excel_path)
80
+
81
+ results = []
82
+ # Limit to first 5 for safety in MVP/Demo to avoid burning API quota or time
83
+ for company in companies[:5]:
84
+ email = generate_cold_email(resume_text, company)
85
+ results.append({
86
+ "company": company.get("Company Name", "Unknown"),
87
+ "email": email,
88
+ "hr_email": company.get("Email", "") # Ensure we have the target email
89
+ })
90
+ return {"emails": results}
91
+
92
+ class SendEmailRequest(BaseModel):
93
+ emails: List[Dict[str, str]] # List of { "to": "...", "subject": "...", "body": "..." }
94
+ smtp_email: str
95
+ smtp_password: str
96
+ resume_filename: Optional[str] = None
97
+
98
+ @app.post("/send-bulk-emails")
99
+ async def send_bulk_emails_endpoint(request: SendEmailRequest):
100
+ results = []
101
+ smtp_config = {"email": request.smtp_email, "password": request.smtp_password}
102
+
103
+ # Construct attachment path if provided
104
+ attachment_path = None
105
+ if request.resume_filename:
106
+ attachment_path = os.path.join(UPLOAD_DIR, request.resume_filename)
107
+
108
+ print(f"🚀 Starting bulk email send for {len(request.emails)} recipients...")
109
+ if attachment_path:
110
+ print(f"📎 Including attachment: {request.resume_filename}")
111
+
112
+ for index, item in enumerate(request.emails):
113
+ to_email = item.get("to")
114
+ subject = item.get("subject")
115
+ body = item.get("body")
116
+
117
+ if to_email and subject and body:
118
+ print(f"[{index+1}/{len(request.emails)}] Sending to {to_email}...")
119
+
120
+ # Pass attachment_path to send_email
121
+ success, message = send_email(to_email, subject, body, smtp_config, attachment_path)
122
+
123
+ results.append({"to": to_email, "status": "sent" if success else "failed", "error": message})
124
+
125
+ # Rate limiting: Sleep 2 seconds between emails to avoid spam filters
126
+ if index < len(request.emails) - 1:
127
+ time.sleep(2)
128
+ else:
129
+ results.append({"to": to_email, "status": "failed", "error": "Invalid data"})
130
+
131
+ print("✅ Bulk sending complete.")
132
+ return {"results": results}
133
+
134
+ if __name__ == "__main__":
135
+ import uvicorn
136
+ uvicorn.run(app, host="0.0.0.0", port=10000)
backend/requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ pandas
5
+ openpyxl
6
+ huggingface_hub
7
+ pypdf
8
+ python-dotenv
9
+ email-validator
10
+ requests
11
+ openai
backend/services/__init__.py ADDED
File without changes
backend/services/ai_engine.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ import os
3
+ import json
4
+
5
+ def generate_cold_email(resume_text: str, company_info: dict, tone="Confident, polite, result-oriented"):
6
+ """
7
+ Generates a cold email using OpenRouter API.
8
+ """
9
+
10
+ # Fetch key dynamically to ensure it's loaded
11
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
12
+
13
+ # 1. Map Data
14
+ candidate_profile = resume_text[:4000] # Increased limit slightly
15
+ company_name = company_info.get('Company Name', 'Target Company')
16
+ role = company_info.get('Role', 'Employee')
17
+ tech_stack = company_info.get('Tech Stack', 'Industry standard technologies')
18
+ hr_name = company_info.get('HR Name', 'Hiring Manager')
19
+
20
+ # 2. Construct Master Prompt
21
+ # Enhanced prompt for better personalization
22
+ prompt = f"""
23
+ You are an expert Copywriter and AI Outreach Assistant.
24
+
25
+ OBJECTIVE:
26
+ Write a high-converting, hyper-personalized cold email to {hr_name} at {company_name} for the role of {role}.
27
+
28
+ CANDIDATE PROFILE (RESUME):
29
+ {candidate_profile}
30
+
31
+ TARGET COMPANY DETAILS:
32
+ Company: {company_name}
33
+ Role: {role}
34
+ Tech Stack: {tech_stack}
35
+
36
+ INSTRUCTIONS:
37
+ 1. **Analyze the Match**: First, silently identify the *strongest* project or skill from the candidate's resume that DIRECTLY pertains to the company's tech stack ({tech_stack}).
38
+ 2. **Hook**: Open with a strong, non-generic hook. Mention why you are interested in {company_name} or a specific achievement of theirs if known (or just general enthusiasm for their mission).
39
+ 3. **The "Why Me" (Crucial)**: You MUST include 1-2 sentences explicitly connecting a specific project/skill from the resume to the {role} requirements. "For example, in my project [Project Name], I used [Tech] to achieve [Result], which aligns with your work in [Domain]."
40
+ 4. **Tone**: Confident, professional, yet human. Avoid stiff corporate jargon.
41
+ 5. **Structure**:
42
+ * **Subject**: Catchy & Relevant (e.g., "{role} Application - [Candidate Name] - [Key Skill] expert")
43
+ * **Salutation**: Dear {hr_name},
44
+ * **Body**: Hook -> "Why Me" (Specific Proof) -> Value Proposition.
45
+ * **Call to Action**: Clear request for a brief chat or interview. Mention attached resume.
46
+ * **Sign-off**: Best regards, [Candidate Name]
47
+
48
+ CONSTRAINTS:
49
+ * Keep it under 150 words.
50
+ * NO spelling errors.
51
+ * NO placeholders like [Insert Here] - use the data provided. If data is missing, generalize intelligently.
52
+ * OUTPUT ONLY the email content.
53
+ """
54
+
55
+ return generate_email_via_ai(prompt, OPENROUTER_API_KEY)
56
+
57
+ def generate_email_via_ai(prompt: str, api_key: str) -> str:
58
+ if not api_key:
59
+ return "Error: OPENROUTER_API_KEY not found in environment variables. Please check your .env file."
60
+
61
+ try:
62
+ client = OpenAI(
63
+ api_key=api_key,
64
+ base_url="https://openrouter.ai/api/v1"
65
+ )
66
+
67
+ response = client.chat.completions.create(
68
+ model="openai/gpt-oss-20b",
69
+ messages=[
70
+ {"role": "system", "content": "You are a professional AI assistant."},
71
+ {"role": "user", "content": prompt}
72
+ ],
73
+ temperature=0.5,
74
+ max_tokens=1000 # Increased slightly to ensure full email + reasoning found in some models
75
+ )
76
+
77
+ return response.choices[0].message.content
78
+ except Exception as e:
79
+ return f"Error connecting to OpenRouter: {str(e)}"
backend/services/email_sender.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import smtplib
2
+ from email.mime.text import MIMEText
3
+ from email.mime.multipart import MIMEMultipart
4
+ from email.mime.application import MIMEApplication
5
+ import os
6
+
7
+ def send_email(to_email, subject, body, smtp_config=None, attachment_path=None):
8
+ # If smtp_config is not provided, try to use env vars (though user provided creds are better)
9
+ # If smtp_config is not provided, try to use env vars (though user provided creds are better)
10
+ # Fix: Ensure we don't use empty strings from config if they are empty
11
+ sender_email = (smtp_config and smtp_config.get("email")) or os.getenv("SMTP_EMAIL")
12
+ password = (smtp_config and smtp_config.get("password")) or os.getenv("SMTP_PASSWORD")
13
+
14
+ if not sender_email or not password:
15
+ return False, "Missing SMTP credentials"
16
+
17
+ msg = MIMEMultipart()
18
+ msg['From'] = sender_email
19
+ msg['To'] = to_email
20
+ msg['Subject'] = subject
21
+
22
+ # Attach body as HTML
23
+ # Converting newlines to <br> if it looks like plain text, or assuming incoming body has basic formatting
24
+ html_body = f"""
25
+ <html>
26
+ <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
27
+ <div style="padding: 20px; border: 1px solid #eee; border-radius: 8px;">
28
+ {body.replace(chr(10), '<br>')}
29
+ </div>
30
+ <p style="font-size: 12px; color: #888; margin-top: 20px;">
31
+ Sent via Hunter AI Agent
32
+ </p>
33
+ </body>
34
+ </html>
35
+ """
36
+ msg.attach(MIMEText(html_body, 'html'))
37
+
38
+ # Attach resume if provided
39
+ if attachment_path and os.path.exists(attachment_path):
40
+ try:
41
+ with open(attachment_path, "rb") as f:
42
+ part = MIMEApplication(
43
+ f.read(),
44
+ Name=os.path.basename(attachment_path)
45
+ )
46
+ # After the file is closed
47
+ part['Content-Disposition'] = f'attachment; filename="{os.path.basename(attachment_path)}"'
48
+ msg.attach(part)
49
+ print(f"📎 Attached file: {attachment_path}")
50
+ except Exception as e:
51
+ print(f"⚠️ Failed to attach file: {e}")
52
+
53
+ try:
54
+ # Connect to Gmail SMTP server
55
+ server = smtplib.SMTP('smtp.gmail.com', 587)
56
+ server.starttls()
57
+ server.login(sender_email, password)
58
+ text = msg.as_string()
59
+ server.sendmail(sender_email, to_email, text)
60
+ server.quit()
61
+ print(f"✅ Email sent to {to_email}")
62
+ return True, "Email sent successfully"
63
+ except Exception as e:
64
+ print(f"❌ Failed to send to {to_email}: {e}")
65
+ return False, f"Failed to send email: {str(e)}"
backend/services/excel_reader.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ def read_company_excel(file_path: str):
4
+ try:
5
+ if file_path.endswith('.csv'):
6
+ df = pd.read_csv(file_path)
7
+ else:
8
+ df = pd.read_excel(file_path)
9
+
10
+ # Normalize headers to lowercase? Or expected exact match?
11
+ # User prompt: "Company Name | HR Name | Email | Role | Tech Stack | Type"
12
+ # Let's clean up nan values first
13
+ df = df.where(pd.notnull(df), None)
14
+ companies = df.to_dict(orient="records")
15
+ return companies
16
+ except Exception as e:
17
+ print(f"Error reading Excel/CSV: {e}")
18
+ return []
backend/services/resume_parser.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pypdf
2
+ import re
3
+
4
+ def parse_resume(file_path: str):
5
+ text = ""
6
+ try:
7
+ reader = pypdf.PdfReader(file_path)
8
+ for page in reader.pages:
9
+ text += page.extract_text() + "\n"
10
+
11
+ # Basic cleaning
12
+ text = re.sub(r'\s+', ' ', text).strip()
13
+
14
+ except Exception as e:
15
+ print(f"Error reading PDF: {e}")
16
+ return str(e)
17
+ return text
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@tailwindcss/postcss": "^4.1.18",
14
+ "axios": "^1.13.2",
15
+ "clsx": "^2.1.1",
16
+ "framer-motion": "^12.23.26",
17
+ "lucide-react": "^0.561.0",
18
+ "react": "^19.2.0",
19
+ "react-dom": "^19.2.0",
20
+ "recharts": "^3.5.1",
21
+ "tailwind-merge": "^3.4.0"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^9.39.1",
25
+ "@types/react": "^19.2.5",
26
+ "@types/react-dom": "^19.2.3",
27
+ "@vitejs/plugin-react": "^5.1.1",
28
+ "autoprefixer": "^10.4.22",
29
+ "eslint": "^9.39.1",
30
+ "eslint-plugin-react-hooks": "^7.0.1",
31
+ "eslint-plugin-react-refresh": "^0.4.24",
32
+ "globals": "^16.5.0",
33
+ "postcss": "^8.5.6",
34
+ "tailwindcss": "^4.1.18",
35
+ "vite": "^7.2.4"
36
+ }
37
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import Layout from './components/Layout';
3
+ import DashboardView from './views/DashboardView';
4
+ import UploadView from './views/UploadView';
5
+ import GeneratorView from './views/GeneratorView';
6
+
7
+ function App() {
8
+ const [activeTab, setActiveTab] = useState('dashboard');
9
+ const [uploadData, setUploadData] = useState(null);
10
+
11
+ const handleUploadComplete = (data) => {
12
+ setUploadData(data);
13
+ setActiveTab('generator');
14
+ };
15
+
16
+ const renderView = () => {
17
+ switch (activeTab) {
18
+ case 'dashboard':
19
+ return <DashboardView setView={setActiveTab} />;
20
+ case 'upload':
21
+ return <UploadView onUploadComplete={handleUploadComplete} />;
22
+ case 'generator':
23
+ return <GeneratorView uploadData={uploadData} />;
24
+ case 'campaigns':
25
+ return <div className="text-slate-500 text-center py-20">Campaign Management Module Coming Soon</div>;
26
+ case 'analytics':
27
+ return <div className="text-slate-500 text-center py-20">Advanced Analytics Module Coming Soon</div>;
28
+ case 'settings':
29
+ return <div className="text-slate-500 text-center py-20">Settings Module Coming Soon</div>;
30
+ default:
31
+ return <DashboardView setView={setActiveTab} />;
32
+ }
33
+ };
34
+
35
+ return (
36
+ <Layout activeTab={activeTab} setActiveTab={setActiveTab}>
37
+ {renderView()}
38
+ </Layout>
39
+ );
40
+ }
41
+
42
+ export default App;
frontend/src/Dashboard.jsx ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import UploadForm from './UploadForm';
3
+ import UploadForm from './UploadForm';
4
+ import axios from 'axios';
5
+ import { API_BASE_URL } from './config';
6
+
7
+ const Dashboard = () => {
8
+ const [step, setStep] = useState(1);
9
+ const [uploadData, setUploadData] = useState(null);
10
+ const [generatedEmails, setGeneratedEmails] = useState([]);
11
+ const [smtpEmail, setSmtpEmail] = useState("");
12
+ const [smtpPassword, setSmtpPassword] = useState("");
13
+ const [sendingResults, setSendingResults] = useState([]);
14
+
15
+ // Background Orbs for "Alive" feel
16
+ const BackgroundOrbs = () => (
17
+ <div className="fixed inset-0 overflow-hidden pointer-events-none -z-10">
18
+ <div className="absolute top-[-10%] left-[-10%] w-96 h-96 bg-purple-600/30 rounded-full blur-3xl float"></div>
19
+ <div className="absolute top-[20%] right-[-5%] w-72 h-72 bg-cyan-600/30 rounded-full blur-3xl float-delayed"></div>
20
+ <div className="absolute bottom-[-10%] left-[20%] w-80 h-80 bg-blue-600/30 rounded-full blur-3xl float"></div>
21
+ </div>
22
+ );
23
+
24
+ const handleUploadSuccess = (data) => {
25
+ setUploadData(data);
26
+ setStep(2);
27
+ generateEmails(data);
28
+ };
29
+
30
+ const generateEmails = async (data) => {
31
+ try {
32
+ const response = await axios.post(`${API_BASE_URL}/generate-emails`, {
33
+ resume_filename: data.resume_filename,
34
+ excel_filename: data.excel_filename
35
+ });
36
+ setGeneratedEmails(response.data.emails);
37
+ setStep(3);
38
+ } catch (error) {
39
+ console.error(error);
40
+ alert("Error generating emails. Ensure Backend is active.");
41
+ setStep(1);
42
+ }
43
+ };
44
+
45
+ const handleSendEmails = async () => {
46
+ if (!smtpEmail || !smtpPassword) {
47
+ alert("Please enter SMTP Credentials");
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const payload = {
53
+ emails: generatedEmails.map(item => ({
54
+ to: item.hr_email,
55
+ subject: item.email.includes("Subject:") ? item.email.split('\n').find(l => l.includes("Subject:")).replace("Subject:", "").trim() : "Application Inquiry",
56
+ body: item.email
57
+ })),
58
+ smtp_email: smtpEmail,
59
+ smtp_password: smtpPassword
60
+ };
61
+
62
+ const response = await axios.post(`${API_BASE_URL}/send-bulk-emails`, payload);
63
+ setSendingResults(response.data.results);
64
+ setStep(4);
65
+ } catch (error) {
66
+ console.error(error);
67
+ alert("Error sending emails");
68
+ }
69
+ };
70
+
71
+ return (
72
+ <div className="relative min-h-screen w-full flex flex-col items-center justify-center p-6">
73
+ <BackgroundOrbs />
74
+
75
+ {/* Header */}
76
+ <header className="mb-10 text-center z-10 fade-in-up" style={{ animationDelay: '0.1s' }}>
77
+ <div className="inline-block p-1 rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 mb-4">
78
+ <div className="bg-slate-900 rounded-full px-4 py-1">
79
+ <span className="text-sm font-bold bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-400">
80
+ AI-POWERED RECRUITMENT AGENT
81
+ </span>
82
+ </div>
83
+ </div>
84
+ <h1 className="text-6xl font-extrabold text-white glow-text tracking-tight mb-2">
85
+ Hunter<span className="text-cyan-400">.ai</span>
86
+ </h1>
87
+ <p className="text-blue-200 text-lg max-w-lg mx-auto leading-relaxed">
88
+ Automate your job outreach with personalized, AI-crafted emails.
89
+ </p>
90
+ </header>
91
+
92
+ <main className="w-full max-w-6xl z-10">
93
+ {step === 1 && (
94
+ <div className="fade-in-up" style={{ animationDelay: '0.2s' }}>
95
+ <UploadForm onUploadSuccess={handleUploadSuccess} />
96
+ </div>
97
+ )}
98
+
99
+ {step === 2 && (
100
+ <div className="max-w-xl mx-auto glass-panel p-12 text-center fade-in-up">
101
+ <div className="relative w-24 h-24 mx-auto mb-8">
102
+ <div className="absolute inset-0 border-4 border-cyan-500/30 rounded-full animate-ping"></div>
103
+ <div className="absolute inset-0 border-4 border-t-cyan-400 rounded-full animate-spin"></div>
104
+ <div className="absolute inset-4 bg-cyan-500/20 rounded-full backdrop-blur-sm"></div>
105
+ </div>
106
+ <h2 className="text-3xl font-bold text-white mb-4">AI Brain at Work</h2>
107
+ <p className="text-blue-200 mb-8">Parsing resume, analyzing tech stack, and crafting perfect pitches...</p>
108
+
109
+ <div className="bg-black/20 rounded-full h-1.5 w-full overflow-hidden">
110
+ <div className="h-full bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 animate-[shimmer_1.5s_infinite] w-2/3"></div>
111
+ </div>
112
+ </div>
113
+ )}
114
+
115
+ {step === 3 && (
116
+ <div className="fade-in-up">
117
+ <div className="flex justify-between items-end mb-8">
118
+ <div>
119
+ <h2 className="text-3xl font-bold text-white">Draft Reviews</h2>
120
+ <p className="text-blue-300">We've prepared {generatedEmails.length} personalized emails for you.</p>
121
+ </div>
122
+ <button className="text-sm text-cyan-400 hover:text-cyan-300 font-semibold underline underline-offset-4">
123
+ Regenerate All
124
+ </button>
125
+ </div>
126
+
127
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
128
+ {generatedEmails.map((item, index) => (
129
+ <div key={index} className="glass-panel p-6 group hover:bg-white/15 transition-all duration-300 hover:-translate-y-2">
130
+ <div className="flex justify-between items-start mb-4">
131
+ <div className="p-2 bg-gradient-to-br from-cyan-500/20 to-blue-500/20 rounded-lg">
132
+ <span className="text-2xl">🏢</span>
133
+ </div>
134
+ <span className="text-xs font-bold px-2 py-1 rounded bg-white/10 text-cyan-300 border border-cyan-500/20">
135
+ MATCH: 92%
136
+ </span>
137
+ </div>
138
+ <h3 className="font-bold text-xl text-white mb-1 group-hover:text-cyan-300 transition-colors">{item.company}</h3>
139
+ <p className="text-sm text-gray-400 mb-4 font-mono truncate">{item.hr_email}</p>
140
+
141
+ <div className="bg-black/40 p-3 rounded-lg border border-white/5 h-40 overflow-hidden relative group-hover:border-cyan-500/30 transition-colors">
142
+ <div className="absolute bottom-0 left-0 w-full h-12 bg-gradient-to-t from-black/80 to-transparent pointer-events-none"></div>
143
+ <p className="text-xs text-gray-300 whitespace-pre-wrap leading-relaxed">{item.email}</p>
144
+ </div>
145
+ <button className="w-full mt-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 text-sm font-semibold text-white border border-white/10 transition-colors">
146
+ Edit / Preview
147
+ </button>
148
+ </div>
149
+ ))}
150
+ </div>
151
+
152
+ <div className="glass-panel p-8 max-w-2xl mx-auto border-t-4 border-t-cyan-500">
153
+ <h3 className="text-2xl font-bold text-white mb-6 text-center">Ready to Launch Campaign? 🚀</h3>
154
+
155
+ <div className="grid grid-cols-1 gap-4 mb-6">
156
+ <input
157
+ type="email"
158
+ placeholder="Your Gmail Address"
159
+ value={smtpEmail}
160
+ onChange={(e) => setSmtpEmail(e.target.value)}
161
+ className="glass-input w-full"
162
+ />
163
+ <input
164
+ type="password"
165
+ placeholder="Gmail App Password"
166
+ value={smtpPassword}
167
+ onChange={(e) => setSmtpPassword(e.target.value)}
168
+ className="glass-input w-full"
169
+ />
170
+ </div>
171
+ <button
172
+ onClick={handleSendEmails}
173
+ className="w-full glass-btn text-lg"
174
+ >
175
+ Send {generatedEmails.length} Emails Now
176
+ </button>
177
+ </div>
178
+ </div>
179
+ )}
180
+
181
+ {step === 4 && (
182
+ <div className="max-w-4xl mx-auto glass-panel p-10 fade-in-up">
183
+ <div className="text-center mb-10">
184
+ <div className="inline-block p-4 rounded-full bg-green-500/20 mb-4">
185
+ <span className="text-4xl">🎉</span>
186
+ </div>
187
+ <h2 className="text-4xl font-bold text-white mb-2">Campaign Completed!</h2>
188
+ <p className="text-gray-300">Here's how your outreach went.</p>
189
+ </div>
190
+
191
+ <div className="overflow-hidden rounded-xl border border-white/10 bg-black/20">
192
+ <table className="w-full text-left">
193
+ <thead className="bg-white/5">
194
+ <tr>
195
+ <th className="p-4 text-sm font-semibold text-gray-400 uppercase tracking-wider">Recipient</th>
196
+ <th className="p-4 text-sm font-semibold text-gray-400 uppercase tracking-wider">Status</th>
197
+ <th className="p-4 text-sm font-semibold text-gray-400 uppercase tracking-wider">Details</th>
198
+ </tr>
199
+ </thead>
200
+ <tbody className="divide-y divide-white/10">
201
+ {sendingResults.map((res, index) => (
202
+ <tr key={index} className="hover:bg-white/5 transition-colors">
203
+ <td className="p-4 text-white font-medium">{res.to}</td>
204
+ <td className="p-4">
205
+ {res.status === 'sent' ? (
206
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-500/30">
207
+ SENT
208
+ </span>
209
+ ) : (
210
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-500/20 text-red-400 border border-red-500/30">
211
+ FAILED
212
+ </span>
213
+ )}
214
+ </td>
215
+ <td className="p-4 text-sm text-gray-500">{res.error || "Delivered"}</td>
216
+ </tr>
217
+ ))}
218
+ </tbody>
219
+ </table>
220
+ </div>
221
+ <div className="mt-10 text-center">
222
+ <button
223
+ onClick={() => window.location.reload()}
224
+ className="px-8 py-3 rounded-xl bg-white/5 hover:bg-white/10 text-white font-semibold transition-all border border-white/10 hover:border-cyan-500/50"
225
+ >
226
+ Start New Campaign
227
+ </button>
228
+ </div>
229
+ </div>
230
+ )}
231
+ </main>
232
+ </div>
233
+ );
234
+ };
235
+
236
+ export default Dashboard;
frontend/src/UploadForm.jsx ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import axios from 'axios';
3
+ import { API_BASE_URL } from './config';
4
+
5
+ const UploadForm = ({ onUploadSuccess }) => {
6
+ const [resume, setResume] = useState(null);
7
+ const [excel, setExcel] = useState(null);
8
+ const [loading, setLoading] = useState(false);
9
+ const [status, setStatus] = useState("");
10
+
11
+ const handleUpload = async (e) => {
12
+ e.preventDefault();
13
+ if (!resume || !excel) {
14
+ alert("Please select both Resume and Excel file.");
15
+ return;
16
+ }
17
+
18
+ setStatus("🚀 Initiating Launch Sequence...");
19
+ setLoading(true);
20
+
21
+ const formData = new FormData();
22
+ formData.append("resume", resume);
23
+ formData.append("company_excel", excel);
24
+
25
+ try {
26
+ const response = await axios.post(`${API_BASE_URL}/upload`, formData, {
27
+ headers: {
28
+ 'Content-Type': 'multipart/form-data'
29
+ }
30
+ });
31
+ console.log(response.data);
32
+ setStatus("✨ Files Secure. Engaging AI Core...");
33
+ setTimeout(() => {
34
+ onUploadSuccess(response.data);
35
+ }, 1500);
36
+ } catch (error) {
37
+ console.error(error);
38
+ setStatus("❌ System Failure during upload.");
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <div className="glass-panel p-10 max-w-lg mx-auto transform transition-all hover:scale-[1.01] hover:shadow-[0_0_40px_rgba(6,182,212,0.15)] relative overflow-hidden group">
46
+ {/* Glossy shine effect */}
47
+ <div className="absolute top-0 left-[-100%] w-1/2 h-full bg-gradient-to-r from-transparent via-white/5 to-transparent skew-x-12 group-hover:animate-[shine_1.5s_ease-in-out]"></div>
48
+
49
+ <h2 className="text-2xl font-bold mb-6 text-center text-white">Initialize Outreach</h2>
50
+
51
+ <form onSubmit={handleUpload} className="space-y-6">
52
+ <div>
53
+ <label className="block text-sm font-semibold text-cyan-300 mb-2 uppercase tracking-wide">Resume (PDF)</label>
54
+ <div className={`relative border-2 border-dashed border-white/20 rounded-xl p-6 transition-all ${resume ? 'bg-cyan-500/10 border-cyan-500/50' : 'hover:bg-white/5 hover:border-cyan-400/50'}`}>
55
+ <input
56
+ type="file" accept=".pdf"
57
+ onChange={(e) => setResume(e.target.files[0])}
58
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
59
+ />
60
+ <div className="text-center pointer-events-none">
61
+ <span className="text-2xl mb-2 block">{resume ? '📄' : '📤'}</span>
62
+ <p className="text-sm font-medium text-gray-300">{resume ? resume.name : "Drop PDF or Click to Browse"}</p>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <div>
68
+ <label className="block text-sm font-semibold text-purple-300 mb-2 uppercase tracking-wide">Company List (Excel)</label>
69
+ <div className={`relative border-2 border-dashed border-white/20 rounded-xl p-6 transition-all ${excel ? 'bg-purple-500/10 border-purple-500/50' : 'hover:bg-white/5 hover:border-purple-400/50'}`}>
70
+ <input
71
+ type="file" accept=".xlsx, .xls"
72
+ onChange={(e) => setExcel(e.target.files[0])}
73
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
74
+ />
75
+ <div className="text-center pointer-events-none">
76
+ <span className="text-2xl mb-2 block">{excel ? '📊' : '📥'}</span>
77
+ <p className="text-sm font-medium text-gray-300">{excel ? excel.name : "Drop Excel or Click to Browse"}</p>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <button
83
+ type="submit"
84
+ disabled={loading}
85
+ className="w-full glass-btn mt-4 group"
86
+ >
87
+ {loading ? (
88
+ <span className="flex items-center justify-center gap-2">
89
+ <svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
90
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
91
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
92
+ </svg>
93
+ Processing...
94
+ </span>
95
+ ) : (
96
+ "🚀 Start Processing"
97
+ )}
98
+ </button>
99
+ </form>
100
+ {status && <div className="mt-4 p-3 rounded-lg bg-black/30 text-center text-sm font-mono text-cyan-200 border border-cyan-500/20">{status}</div>}
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default UploadForm;
frontend/src/assets/react.svg ADDED
frontend/src/components/Layout.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import Sidebar from './Sidebar';
3
+
4
+ const Layout = ({ children, activeTab, setActiveTab }) => {
5
+ return (
6
+ <div className="min-h-screen bg-slate-50 text-slate-900 font-sans selection:bg-indigo-500/30">
7
+ <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
8
+
9
+ <main className="pl-64 min-h-screen">
10
+ {/* Top Bar Place holder if needed, or integrated into pages */}
11
+ <div className="p-8 max-w-7xl mx-auto animate-in fade-in zoom-in duration-300">
12
+ {children}
13
+ </div>
14
+ </main>
15
+ </div>
16
+ );
17
+ };
18
+
19
+ export default Layout;
frontend/src/components/Sidebar.jsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import {
3
+ LayoutDashboard,
4
+ UploadCloud,
5
+ Sparkles,
6
+ Send,
7
+ BarChart2,
8
+ Settings,
9
+ LogOut
10
+ } from 'lucide-react';
11
+ import { motion } from 'framer-motion';
12
+
13
+ const Sidebar = ({ activeTab, setActiveTab }) => {
14
+ const menuItems = [
15
+ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
16
+ { id: 'upload', label: 'Upload Data', icon: UploadCloud },
17
+ { id: 'generator', label: 'AI Generator', icon: Sparkles },
18
+ { id: 'campaigns', label: 'Campaigns', icon: Send },
19
+ { id: 'analytics', label: 'Analytics', icon: BarChart2 },
20
+ { id: 'settings', label: 'Settings', icon: Settings },
21
+ ];
22
+
23
+ return (
24
+ <div className="w-64 h-screen bg-white border-r border-slate-200 flex flex-col fixed left-0 top-0 z-50">
25
+ {/* Brand */}
26
+ <div className="p-6">
27
+ <div className="flex items-center gap-2">
28
+ <div className="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center">
29
+ <Sparkles className="w-5 h-5 text-white" />
30
+ </div>
31
+ <span className="text-xl font-bold text-slate-900 tracking-tight">HunterAI</span>
32
+ </div>
33
+ </div>
34
+
35
+ {/* Navigation */}
36
+ <nav className="flex-1 px-4 space-y-1 mt-6">
37
+ {menuItems.map((item) => {
38
+ const Icon = item.icon;
39
+ const isActive = activeTab === item.id;
40
+
41
+ return (
42
+ <motion.div
43
+ key={item.id}
44
+ onClick={() => setActiveTab(item.id)}
45
+ className={`nav-item group ${isActive ? 'active' : ''}`}
46
+ whileHover={{ x: 4 }}
47
+ whileTap={{ scale: 0.98 }}
48
+ >
49
+ <Icon className={`sidebar-icon ${isActive ? 'text-indigo-400' : ''}`} />
50
+ <span>{item.label}</span>
51
+ {isActive && (
52
+ <motion.div
53
+ layoutId="active-pill"
54
+ className="absolute left-0 w-1 h-8 bg-indigo-500 rounded-r-full"
55
+ initial={{ opacity: 0 }}
56
+ animate={{ opacity: 1 }}
57
+ exit={{ opacity: 0 }}
58
+ />
59
+ )}
60
+ </motion.div>
61
+ );
62
+ })}
63
+ </nav>
64
+
65
+ {/* User Profile */}
66
+ <div className="p-4 border-t border-slate-200">
67
+ <div className="flex items-center gap-3 px-2 py-2 rounded-xl hover:bg-slate-100 cursor-pointer transition-colors">
68
+ <div className="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center text-xs font-semibold text-slate-600">
69
+ AS
70
+ </div>
71
+ <div className="flex-1">
72
+ <p className="text-sm font-medium text-slate-900">Abhishek Singh</p>
73
+ <p className="text-xs text-slate-500">Pro Plan</p>
74
+ </div>
75
+ <LogOut className="w-4 h-4 text-slate-400 hover:text-slate-600" />
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ export default Sidebar;
frontend/src/config.js ADDED
@@ -0,0 +1 @@
 
 
1
+ export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:10000";
frontend/src/index.css ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
2
+
3
+ @import "tailwindcss";
4
+
5
+ @layer theme {
6
+ :root {
7
+ --font-sans: 'Inter', sans-serif;
8
+ }
9
+ }
10
+
11
+ @layer base {
12
+ body {
13
+ @apply bg-slate-50 text-slate-900 font-sans antialiased;
14
+ }
15
+ }
16
+
17
+ .glass-panel {
18
+ @apply bg-white/60 backdrop-blur-xl border border-slate-200 shadow-xl rounded-2xl;
19
+ }
20
+
21
+ .gradient-text {
22
+ @apply bg-clip-text text-transparent bg-gradient-to-r from-indigo-600 via-violet-600 to-blue-600;
23
+ }
24
+
25
+ .sidebar-icon {
26
+ @apply w-5 h-5 text-slate-400 group-hover:text-indigo-600 transition-colors;
27
+ }
28
+
29
+ .nav-item {
30
+ @apply flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition-all cursor-pointer;
31
+ }
32
+
33
+ .nav-item.active {
34
+ @apply bg-indigo-50 text-indigo-600 border border-indigo-200;
35
+ }
36
+
37
+ /* Animations */
38
+ .fade-in {
39
+ animation: fadeIn 0.4s ease-out forwards;
40
+ }
41
+
42
+ @keyframes fadeIn {
43
+ from {
44
+ opacity: 0;
45
+ transform: translateY(10px);
46
+ }
47
+
48
+ to {
49
+ opacity: 1;
50
+ transform: translateY(0);
51
+ }
52
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/src/views/DashboardView.jsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import {
3
+ BarChart,
4
+ Bar,
5
+ XAxis,
6
+ YAxis,
7
+ CartesianGrid,
8
+ Tooltip,
9
+ ResponsiveContainer,
10
+ LineChart,
11
+ Line
12
+ } from 'recharts';
13
+ import { Mail, MousePointer, Users, Activity, Plus } from 'lucide-react';
14
+ import { motion } from 'framer-motion';
15
+
16
+ const data = [
17
+ { name: 'Mon', emails: 400, replies: 240 },
18
+ { name: 'Tue', emails: 300, replies: 139 },
19
+ { name: 'Wed', emails: 200, replies: 980 },
20
+ { name: 'Thu', emails: 278, replies: 390 },
21
+ { name: 'Fri', emails: 189, replies: 480 },
22
+ { name: 'Sat', emails: 239, replies: 380 },
23
+ { name: 'Sun', emails: 349, replies: 430 },
24
+ ];
25
+
26
+ const StatCard = ({ title, value, change, icon: Icon, color }) => (
27
+ <div className="bg-white p-6 flex flex-col justify-between h-40 rounded-2xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
28
+ <div className="flex justify-between items-start">
29
+ <div className={`p-3 rounded-xl ${color} text-white shadow-md shadow-${color.replace('bg-', '')}/20`}>
30
+ <Icon className="w-6 h-6" />
31
+ </div>
32
+ <span className="text-xs font-semibold text-emerald-600 bg-emerald-50 px-2 py-1 rounded-full">{change}</span>
33
+ </div>
34
+ <div>
35
+ <h3 className="text-3xl font-bold text-slate-900 tracking-tight">{value}</h3>
36
+ <p className="text-sm text-slate-500 font-medium mt-1">{title}</p>
37
+ </div>
38
+ </div>
39
+ );
40
+
41
+ const DashboardView = ({ setView }) => {
42
+ return (
43
+ <div className="space-y-8">
44
+ {/* Hero Section */}
45
+ <div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-indigo-50 to-violet-50 border border-indigo-100 p-10 shadow-lg">
46
+ <div className="absolute top-0 right-0 w-96 h-96 bg-white/40 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none" />
47
+ <div className="relative z-10 max-w-2xl">
48
+ <motion.div
49
+ initial={{ opacity: 0, y: 10 }}
50
+ animate={{ opacity: 1, y: 0 }}
51
+ transition={{ duration: 0.5 }}
52
+ >
53
+ <h1 className="text-4xl font-bold text-slate-900 mb-4">
54
+ Automate your <span className="gradient-text">Outreach</span>
55
+ </h1>
56
+ <p className="text-slate-600 text-lg mb-8 leading-relaxed">
57
+ HunterAI creates hyper-personalized emails using your resume and target company data to 10x your response rate.
58
+ </p>
59
+ <button
60
+ onClick={() => setView('upload')}
61
+ className="bg-indigo-600 text-white px-6 py-3 rounded-xl font-semibold hover:bg-indigo-700 transition-colors flex items-center gap-2 shadow-lg shadow-indigo-200"
62
+ >
63
+ <Plus className="w-5 h-5" />
64
+ Create New Campaign
65
+ </button>
66
+ </motion.div>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Stats Grid */}
71
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
72
+ <StatCard title="Total Emails Sent" value="12,450" change="+12%" icon={Mail} color="bg-indigo-500" />
73
+ <StatCard title="Response Rate" value="24.8%" change="+4.2%" icon={MousePointer} color="bg-violet-500" />
74
+ <StatCard title="Active Campaigns" value="3" change="Active" icon={Activity} color="bg-blue-500" />
75
+ <StatCard title="Jobs Matched" value="85" change="+18" icon={Users} color="bg-emerald-500" />
76
+ </div>
77
+
78
+ {/* Analytics Chart */}
79
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-96">
80
+ <div className="lg:col-span-2 glass-panel p-6 bg-white">
81
+ <h3 className="text-lg font-semibold text-slate-900 mb-6">Campaign Performance</h3>
82
+ <ResponsiveContainer width="100%" height="85%">
83
+ <BarChart data={data}>
84
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" vertical={false} />
85
+ <XAxis dataKey="name" stroke="#64748b" fontSize={12} tickLine={false} axisLine={false} />
86
+ <YAxis stroke="#64748b" fontSize={12} tickLine={false} axisLine={false} />
87
+ <Tooltip
88
+ contentStyle={{ backgroundColor: '#ffffff', border: '1px solid #e2e8f0', borderRadius: '8px', color: '#0f172a' }}
89
+ itemStyle={{ color: '#0f172a' }}
90
+ />
91
+ <Bar dataKey="emails" fill="#6366f1" radius={[4, 4, 0, 0]} />
92
+ <Bar dataKey="replies" fill="#8b5cf6" radius={[4, 4, 0, 0]} />
93
+ </BarChart>
94
+ </ResponsiveContainer>
95
+ </div>
96
+
97
+ <div className="glass-panel p-6 bg-white">
98
+ <h3 className="text-lg font-semibold text-slate-900 mb-4">Recent Activity</h3>
99
+ <div className="space-y-4">
100
+ {[1, 2, 3, 4].map((_, i) => (
101
+ <div key={i} className="flex items-center gap-3 p-3 rounded-lg hover:bg-slate-50 transition-colors cursor-pointer border border-transparent hover:border-slate-100">
102
+ <div className="w-2 h-2 rounded-full bg-indigo-500"></div>
103
+ <div className="flex-1">
104
+ <p className="text-sm text-slate-700">Campaign "Frontend Roles" sent</p>
105
+ <p className="text-xs text-slate-500">2 hours ago</p>
106
+ </div>
107
+ </div>
108
+ ))}
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ );
114
+ };
115
+
116
+ export default DashboardView;
frontend/src/views/GeneratorView.jsx ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { RefreshCw, Check, X, Send, Edit, Copy } from 'lucide-react';
3
+ import axios from 'axios';
4
+ import { API_BASE_URL } from '../config';
5
+
6
+ const GeneratorView = ({ uploadData }) => {
7
+ const [generatedEmails, setGeneratedEmails] = useState([]);
8
+ const [selectedEmailIndex, setSelectedEmailIndex] = useState(0);
9
+ const [loading, setLoading] = useState(false);
10
+ const [sending, setSending] = useState(false);
11
+
12
+ useEffect(() => {
13
+ if (uploadData && generatedEmails.length === 0) {
14
+ generateEmails();
15
+ }
16
+ }, [uploadData]);
17
+
18
+ const generateEmails = async () => {
19
+ setLoading(true);
20
+ try {
21
+ const res = await axios.post(`${API_BASE_URL}/generate-emails`, {
22
+ resume_filename: uploadData.resume_filename,
23
+ excel_filename: uploadData.excel_filename
24
+ });
25
+ setGeneratedEmails(res.data.emails);
26
+ } catch (e) {
27
+ console.error(e);
28
+ alert("Error generating emails");
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ };
33
+
34
+ const handleSendCurrent = async () => {
35
+ // Logic to send single email or push to queue
36
+ if (!uploadData.smtpEmail || !uploadData.smtpPassword) {
37
+ alert("Missing SMTP Credentials. Go back to Upload.");
38
+ return;
39
+ }
40
+
41
+ const current = generatedEmails[selectedEmailIndex];
42
+ setSending(true);
43
+ try {
44
+ const payload = {
45
+ emails: [{
46
+ to: current.hr_email,
47
+ subject: current.email.split('\n')[0].replace("Subject: ", ""), // Simple extraction
48
+ body: current.email
49
+ }],
50
+ smtp_email: uploadData.smtpEmail,
51
+ smtp_password: uploadData.smtpPassword,
52
+ resume_filename: uploadData.resume_filename // Pass filename for attachment
53
+ };
54
+ await axios.post(`${API_BASE_URL}/send-bulk-emails`, payload);
55
+ alert(`Email sent to ${current.company}`);
56
+ } catch (e) {
57
+ console.error(e);
58
+ alert("Sending failed.");
59
+ } finally {
60
+ setSending(false);
61
+ }
62
+ };
63
+
64
+ if (!uploadData) return (
65
+ <div className="flex items-center justify-center h-full text-slate-500">
66
+ No data loaded. Please start from Upload page.
67
+ </div>
68
+ );
69
+
70
+ return (
71
+ <div className="h-[calc(100vh-100px)] flex gap-6">
72
+ {/* Left: List */}
73
+ <div className="w-1/3 glass-panel overflow-hidden flex flex-col bg-white">
74
+ <div className="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
75
+ <h3 className="font-semibold text-slate-900">Candidates ({generatedEmails.length})</h3>
76
+ <button
77
+ onClick={generateEmails}
78
+ className="p-2 hover:bg-white/10 rounded-lg transition-colors"
79
+ title="Regenerate All"
80
+ >
81
+ <RefreshCw className={`w-4 h-4 text-slate-400 ${loading ? 'animate-spin' : ''}`} />
82
+ </button>
83
+ </div>
84
+
85
+ <div className="overflow-y-auto flex-1 p-2 space-y-2">
86
+ {loading ? (
87
+ <div className="text-center p-8 text-slate-500">AI is brainstorming...</div>
88
+ ) : generatedEmails.map((item, idx) => (
89
+ <div
90
+ key={idx}
91
+ onClick={() => setSelectedEmailIndex(idx)}
92
+ className={`p-4 rounded-xl cursor-pointer border transition-all ${selectedEmailIndex === idx ? 'bg-indigo-50 border-indigo-200' : 'bg-transparent border-transparent hover:bg-slate-50'}`}
93
+ >
94
+ <div className="flex justify-between items-center mb-1">
95
+ <h4 className={`font-medium ${selectedEmailIndex === idx ? 'text-indigo-700' : 'text-slate-700'}`}>{item.company}</h4>
96
+ <span className="text-xs text-slate-500 px-2 py-0.5 rounded-full bg-slate-100">95%</span>
97
+ </div>
98
+ <p className="text-xs text-slate-500 truncate">{item.hr_email}</p>
99
+ </div>
100
+ ))}
101
+ </div>
102
+ </div>
103
+
104
+ {/* Right: Preview */}
105
+ <div className="flex-1 glass-panel flex flex-col overflow-hidden relative bg-white">
106
+ {generatedEmails.length > 0 && (
107
+ <>
108
+ <div className="p-6 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
109
+ <div>
110
+ <p className="text-sm text-slate-500 mb-1">Subject</p>
111
+ <h2 className="text-lg font-medium text-slate-900 truncate max-w-lg">
112
+ {generatedEmails[selectedEmailIndex].email.split('\n')[0].replace("Subject: ", "")}
113
+ </h2>
114
+ </div>
115
+ <div className="flex gap-2">
116
+ <button className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 text-sm font-medium transition-colors">
117
+ <Edit className="w-4 h-4" /> Edit
118
+ </button>
119
+ <button
120
+ onClick={handleSendCurrent}
121
+ disabled={sending}
122
+ className="flex items-center gap-2 px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium transition-colors shadow-lg shadow-indigo-500/20"
123
+ >
124
+ <Send className="w-4 h-4" /> {sending ? 'Sending...' : 'Send Now'}
125
+ </button>
126
+ </div>
127
+ </div>
128
+
129
+ <div className="flex-1 p-8 overflow-y-auto bg-slate-100/50">
130
+ <div className="max-w-3xl mx-auto bg-white text-slate-900 p-8 rounded-lg shadow-sm border border-slate-200 min-h-full font-serif loading-relaxed leading-7 whitespace-pre-wrap">
131
+ {generatedEmails[selectedEmailIndex].email}
132
+ </div>
133
+ </div>
134
+ </>
135
+ )}
136
+ </div>
137
+ </div>
138
+ );
139
+ };
140
+
141
+ export default GeneratorView;
frontend/src/views/UploadView.jsx ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { UploadCloud, FileText, Settings, ArrowRight, CheckCircle2 } from 'lucide-react';
3
+ import { motion } from 'framer-motion';
4
+ import axios from 'axios';
5
+ import { API_BASE_URL } from '../config';
6
+
7
+ const UploadView = ({ onUploadComplete }) => {
8
+ const [resume, setResume] = useState(null);
9
+ const [excel, setExcel] = useState(null);
10
+ const [smtpEmail, setSmtpEmail] = useState("");
11
+ const [smtpPassword, setSmtpPassword] = useState("");
12
+ const [loading, setLoading] = useState(false);
13
+
14
+ const handleUpload = async () => {
15
+ if (!resume || !excel) {
16
+ alert("Please provide both Resume and Excel files.");
17
+ return;
18
+ }
19
+ setLoading(true);
20
+ const formData = new FormData();
21
+ formData.append("resume", resume);
22
+ formData.append("company_excel", excel);
23
+
24
+ try {
25
+ const res = await axios.post(`${API_BASE_URL}/upload`, formData);
26
+ // Pass minimal data needed for generator + generic SMTP creds storage (in state)
27
+ onUploadComplete({
28
+ ...res.data,
29
+ smtpEmail,
30
+ smtpPassword
31
+ });
32
+ } catch (err) {
33
+ console.error(err);
34
+ alert("Upload failed.");
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <div className="max-w-5xl mx-auto py-8">
42
+ <div className="mb-12">
43
+ <h2 className="text-3xl font-bold text-slate-900 mb-2">Configure Campaign</h2>
44
+ <p className="text-slate-500">Upload your data sources to initialize the AI agent.</p>
45
+ </div>
46
+
47
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
48
+ {/* Card 1: Company List */}
49
+ <div className="glass-panel p-6 hover:bg-white transition-colors group bg-white">
50
+ <div className="flex items-center gap-3 mb-6">
51
+ <div className="p-2 bg-emerald-50 rounded-lg text-emerald-600 group-hover:bg-emerald-100 transition-colors">
52
+ <UploadCloud className="w-6 h-6" />
53
+ </div>
54
+ <h3 className="font-semibold text-slate-900">Company Data</h3>
55
+ </div>
56
+
57
+ <div className={`border-2 border-dashed rounded-xl p-8 text-center transition-all cursor-pointer ${excel ? 'border-emerald-500 bg-emerald-50' : 'border-slate-200 hover:border-slate-400'}`}>
58
+ <input
59
+ type="file"
60
+ accept=".xlsx, .xls, .csv"
61
+ onChange={(e) => setExcel(e.target.files[0])}
62
+ className="hidden"
63
+ id="excel-upload"
64
+ />
65
+ <label htmlFor="excel-upload" className="cursor-pointer block h-full">
66
+ {excel ? (
67
+ <div className="text-emerald-400 flex flex-col items-center">
68
+ <CheckCircle2 className="w-8 h-8 mb-2" />
69
+ <span className="text-sm font-medium truncate max-w-full">{excel.name}</span>
70
+ </div>
71
+ ) : (
72
+ <span className="text-sm text-slate-500">
73
+ Drag & drop Excel or CSV<br /><span className="text-xs opacity-70">or click to browse</span>
74
+ </span>
75
+ )}
76
+ </label>
77
+ </div>
78
+ </div>
79
+
80
+ {/* Card 2: Resume */}
81
+ <div className="glass-panel p-6 hover:bg-white transition-colors group bg-white">
82
+ <div className="flex items-center gap-3 mb-6">
83
+ <div className="p-2 bg-blue-50 rounded-lg text-blue-600 group-hover:bg-blue-100 transition-colors">
84
+ <FileText className="w-6 h-6" />
85
+ </div>
86
+ <h3 className="font-semibold text-slate-900">Your Resume</h3>
87
+ </div>
88
+
89
+ <div className={`border-2 border-dashed rounded-xl p-8 text-center transition-all cursor-pointer ${resume ? 'border-blue-500 bg-blue-50' : 'border-slate-200 hover:border-slate-400'}`}>
90
+ <input
91
+ type="file"
92
+ accept=".pdf"
93
+ onChange={(e) => setResume(e.target.files[0])}
94
+ className="hidden"
95
+ id="resume-upload"
96
+ />
97
+ <label htmlFor="resume-upload" className="cursor-pointer block h-full">
98
+ {resume ? (
99
+ <div className="text-blue-400 flex flex-col items-center">
100
+ <CheckCircle2 className="w-8 h-8 mb-2" />
101
+ <span className="text-sm font-medium truncate max-w-full">{resume.name}</span>
102
+ </div>
103
+ ) : (
104
+ <span className="text-sm text-slate-500">
105
+ Drag & drop PDF<br /><span className="text-xs opacity-70">or click to browse</span>
106
+ </span>
107
+ )}
108
+ </label>
109
+ </div>
110
+ </div>
111
+
112
+ {/* Card 3: Email Config */}
113
+ <div className="glass-panel p-6 hover:bg-white transition-colors group bg-white">
114
+ <div className="flex items-center gap-3 mb-6">
115
+ <div className="p-2 bg-purple-50 rounded-lg text-purple-600 group-hover:bg-purple-100 transition-colors">
116
+ <Settings className="w-6 h-6" />
117
+ </div>
118
+ <h3 className="font-semibold text-slate-900">Sender Config</h3>
119
+ </div>
120
+
121
+ <div className="space-y-4">
122
+ <input
123
+ type="email"
124
+ placeholder="Gmail Address"
125
+ value={smtpEmail}
126
+ onChange={(e) => setSmtpEmail(e.target.value)}
127
+ className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-900 focus:outline-none focus:border-indigo-500"
128
+ />
129
+ <input
130
+ type="password"
131
+ placeholder="App Password"
132
+ value={smtpPassword}
133
+ onChange={(e) => setSmtpPassword(e.target.value)}
134
+ className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-900 focus:outline-none focus:border-indigo-500"
135
+ />
136
+ <div className="flex items-center gap-2">
137
+ <div className="w-2 h-2 rounded-full bg-slate-600"></div>
138
+ <span className="text-xs text-slate-500">SMTP Disconnected</span>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <div className="flex justify-end">
145
+ <button
146
+ onClick={handleUpload}
147
+ disabled={loading}
148
+ className="group relative inline-flex items-center gap-2 px-8 py-4 bg-indigo-600 text-white rounded-xl font-bold text-lg hover:bg-indigo-700 transition-all shadow-lg hover:shadow-xl disabled:opacity-50"
149
+ >
150
+ {loading ? 'Processing...' : 'Analyze & Generate'}
151
+ <ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
152
+ </button>
153
+ </div>
154
+ </div>
155
+ );
156
+ };
157
+
158
+ export default UploadView;
frontend/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
main.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import uvicorn
4
+
5
+ # Fix path to allow imports from backend folder
6
+ sys.path.append(os.path.join(os.path.dirname(__file__), "backend"))
7
+
8
+ from hunter_backend import app
9
+
10
+ if __name__ == "__main__":
11
+ uvicorn.run(app, host="0.0.0.0", port=10000)
render.yaml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ # Backend Service
3
+ - type: web
4
+ name: hunter-ai-backend
5
+ env: python
6
+ region: singapore
7
+ plan: free
8
+ buildCommand: pip install -r requirements.txt
9
+ startCommand: python main.py
10
+ rootDirectory: .
11
+ envVars:
12
+ - key: PYTHON_VERSION
13
+ value: 3.10.0
14
+ - key: FRONTEND_URL
15
+ fromService:
16
+ type: web
17
+ name: hunter-ai-frontend
18
+ property: url
19
+ - key: OLLAMA_URL
20
+ value: http://localhost:11434 # Placeholder, user must update this in Render dashboard
21
+ - key: PORT
22
+ value: 10000
23
+ - key: GROQ_API_KEY
24
+ sync: false
25
+
26
+ # Frontend Service
27
+ - type: web
28
+ name: hunter-ai-frontend
29
+ env: static
30
+ region: singapore
31
+ plan: free
32
+ buildCommand: npm install && npm run build
33
+ staticPublishPath: ./dist
34
+ rootDirectory: frontend
35
+ envVars:
36
+ - key: VITE_API_URL
37
+ fromService:
38
+ type: web
39
+ name: hunter-ai-backend
40
+ property: url
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ -r backend/requirements.txt
test_companies.csv ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Company Name,HR Name,Email,Role,Tech Stack,Type
2
+ Google,Jane Doe,jane@google.com,Software Engineer,Python,Product
3
+ Facebook,John Start,john@facebook.com,Product Manager,React,Product
4
+ Amazon,Alice Stone,alice@amazon.com,Data Scientist,SQL,Service