Spaces:
No application file
No application file
| # π― IQKiller Vercel Setup Script | |
| # Based on Vercel AI SDK PDF Support Example | |
| # https://github.com/vercel-labs/ai-sdk-preview-pdf-support | |
| set -e | |
| echo "π― Setting up IQKiller using Vercel AI SDK PDF Support..." | |
| echo "=========================================================" | |
| # Configuration | |
| PROJECT_NAME="iqkiller-vercel" | |
| VERCEL_EXAMPLE="https://github.com/vercel-labs/ai-sdk-preview-pdf-support" | |
| # Check if directory exists | |
| if [ -d "$PROJECT_NAME" ]; then | |
| echo "β οΈ Directory $PROJECT_NAME already exists. Do you want to remove it? (y/N)" | |
| read -r response | |
| if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then | |
| rm -rf "$PROJECT_NAME" | |
| else | |
| echo "β Aborting setup. Please choose a different directory name." | |
| exit 1 | |
| fi | |
| fi | |
| # Clone Vercel AI SDK PDF example | |
| echo "π¦ Cloning Vercel AI SDK PDF Support example..." | |
| npx create-next-app@latest "$PROJECT_NAME" --example "$VERCEL_EXAMPLE" --use-npm --yes | |
| cd "$PROJECT_NAME" | |
| echo "π Customizing for IQKiller features..." | |
| # Install additional IQKiller dependencies | |
| echo "π¦ Installing IQKiller-specific dependencies..." | |
| npm install \ | |
| firecrawl-py \ | |
| @vercel/kv \ | |
| @vercel/postgres \ | |
| puppeteer \ | |
| cheerio \ | |
| axios \ | |
| date-fns \ | |
| react-dropzone \ | |
| react-hot-toast \ | |
| framer-motion \ | |
| @hookform/resolvers \ | |
| react-hook-form | |
| # Install development dependencies | |
| npm install -D \ | |
| @types/cheerio \ | |
| @types/react-dropzone | |
| # Create IQKiller-specific components | |
| echo "π§© Creating IQKiller components..." | |
| # Create job analysis component | |
| cat > components/job-analysis.tsx << 'EOF' | |
| 'use client' | |
| import { useState } from 'react' | |
| import { Button } from '@/components/ui/button' | |
| import { Input } from '@/components/ui/input' | |
| import { Label } from '@/components/ui/label' | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' | |
| import { Loader2, Link as LinkIcon, FileText } from 'lucide-react' | |
| interface JobAnalysisProps { | |
| onJobData: (data: any) => void | |
| } | |
| export function JobAnalysis({ onJobData }: JobAnalysisProps) { | |
| const [jobUrl, setJobUrl] = useState('') | |
| const [jobText, setJobText] = useState('') | |
| const [isLoading, setIsLoading] = useState(false) | |
| const [mode, setMode] = useState<'url' | 'text'>('url') | |
| const handleUrlScraping = async () => { | |
| if (!jobUrl.trim()) return | |
| setIsLoading(true) | |
| try { | |
| const response = await fetch('/api/scrape', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ url: jobUrl }) | |
| }) | |
| const data = await response.json() | |
| if (data.success) { | |
| onJobData(data.jobData) | |
| } | |
| } catch (error) { | |
| console.error('Error scraping job:', error) | |
| } finally { | |
| setIsLoading(false) | |
| } | |
| } | |
| const handleTextSubmit = () => { | |
| if (!jobText.trim()) return | |
| onJobData({ | |
| description: jobText, | |
| source: 'manual_text' | |
| }) | |
| } | |
| return ( | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Job Information</CardTitle> | |
| <CardDescription> | |
| Add job posting via URL or paste the description directly | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="flex space-x-2"> | |
| <Button | |
| variant={mode === 'url' ? 'default' : 'outline'} | |
| onClick={() => setMode('url')} | |
| className="flex-1" | |
| > | |
| <LinkIcon className="w-4 h-4 mr-2" /> | |
| URL | |
| </Button> | |
| <Button | |
| variant={mode === 'text' ? 'default' : 'outline'} | |
| onClick={() => setMode('text')} | |
| className="flex-1" | |
| > | |
| <FileText className="w-4 h-4 mr-2" /> | |
| Text | |
| </Button> | |
| </div> | |
| {mode === 'url' ? ( | |
| <div className="space-y-3"> | |
| <Label htmlFor="job-url">Job Posting URL</Label> | |
| <Input | |
| id="job-url" | |
| type="url" | |
| placeholder="https://linkedin.com/jobs/view/123456" | |
| value={jobUrl} | |
| onChange={(e) => setJobUrl(e.target.value)} | |
| /> | |
| <Button | |
| onClick={handleUrlScraping} | |
| disabled={isLoading || !jobUrl.trim()} | |
| className="w-full" | |
| > | |
| {isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />} | |
| Scrape Job Details | |
| </Button> | |
| </div> | |
| ) : ( | |
| <div className="space-y-3"> | |
| <Label htmlFor="job-text">Job Description</Label> | |
| <textarea | |
| id="job-text" | |
| className="w-full min-h-[200px] p-3 border rounded-md resize-y" | |
| placeholder="Paste the complete job description here..." | |
| value={jobText} | |
| onChange={(e) => setJobText(e.target.value)} | |
| /> | |
| <Button | |
| onClick={handleTextSubmit} | |
| disabled={!jobText.trim()} | |
| className="w-full" | |
| > | |
| Use This Job Description | |
| </Button> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ) | |
| } | |
| EOF | |
| # Create interview analysis component | |
| cat > components/interview-analysis.tsx << 'EOF' | |
| 'use client' | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' | |
| import { Badge } from '@/components/ui/badge' | |
| import { Progress } from '@/components/ui/progress' | |
| import { Separator } from '@/components/ui/separator' | |
| interface AnalysisResult { | |
| matchScore: number | |
| technicalSkills: string[] | |
| softSkills: string[] | |
| matchingSkills: string[] | |
| interviewQuestions: Array<{ | |
| category: 'technical' | 'behavioral' | 'culture' | |
| question: string | |
| }> | |
| recommendations: string[] | |
| } | |
| interface InterviewAnalysisProps { | |
| analysis: AnalysisResult | |
| } | |
| export function InterviewAnalysis({ analysis }: InterviewAnalysisProps) { | |
| const getScoreColor = (score: number) => { | |
| if (score >= 85) return 'text-green-600' | |
| if (score >= 70) return 'text-yellow-600' | |
| return 'text-red-600' | |
| } | |
| const getScoreLabel = (score: number) => { | |
| if (score >= 85) return 'π’ Excellent Match' | |
| if (score >= 70) return 'π‘ Strong Match' | |
| return 'π΄ Developing Match' | |
| } | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Match Score */} | |
| <Card> | |
| <CardHeader className="text-center"> | |
| <CardTitle className="text-3xl font-bold"> | |
| <span className={getScoreColor(analysis.matchScore)}> | |
| {analysis.matchScore}% | |
| </span> | |
| </CardTitle> | |
| <CardDescription className="text-lg"> | |
| {getScoreLabel(analysis.matchScore)} | |
| </CardDescription> | |
| <Progress value={analysis.matchScore} className="w-full mt-4" /> | |
| </CardHeader> | |
| </Card> | |
| {/* Skills Analysis */} | |
| <div className="grid md:grid-cols-2 gap-6"> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-lg">Technical Skills Match</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-2"> | |
| {analysis.matchingSkills.slice(0, 8).map((skill, index) => ( | |
| <Badge key={index} variant="secondary" className="mr-2 mb-2"> | |
| {skill} | |
| </Badge> | |
| ))} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="text-lg">Recommendations</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <ul className="space-y-2"> | |
| {analysis.recommendations.slice(0, 4).map((rec, index) => ( | |
| <li key={index} className="text-sm text-gray-600 flex items-start"> | |
| <span className="text-blue-500 mr-2">β’</span> | |
| {rec} | |
| </li> | |
| ))} | |
| </ul> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Interview Questions */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Essential Interview Questions</CardTitle> | |
| <CardDescription> | |
| Practice these questions to ace your interview | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-4"> | |
| {['technical', 'behavioral', 'culture'].map((category) => { | |
| const questions = analysis.interviewQuestions.filter(q => q.category === category) | |
| if (questions.length === 0) return null | |
| return ( | |
| <div key={category}> | |
| <h4 className="font-semibold capitalize mb-2 text-sm"> | |
| {category} Questions | |
| </h4> | |
| <div className="space-y-2"> | |
| {questions.slice(0, 2).map((q, index) => ( | |
| <div key={index} className="p-3 bg-gray-50 rounded-md text-sm"> | |
| {q.question} | |
| </div> | |
| ))} | |
| </div> | |
| {category !== 'culture' && <Separator className="mt-4" />} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ) | |
| } | |
| EOF | |
| # Create job scraping API route | |
| echo "π Creating API routes..." | |
| mkdir -p app/api/scrape | |
| cat > app/api/scrape/route.ts << 'EOF' | |
| import { NextRequest, NextResponse } from 'next/server' | |
| export async function POST(req: NextRequest) { | |
| try { | |
| const { url } = await req.json() | |
| if (!url) { | |
| return NextResponse.json({ error: 'URL is required' }, { status: 400 }) | |
| } | |
| // For now, return mock data. In production, integrate with Firecrawl | |
| const mockJobData = { | |
| title: 'Software Engineer', | |
| company: 'Tech Corp', | |
| description: 'We are looking for a skilled software engineer...', | |
| requirements: [ | |
| '3+ years of experience', | |
| 'Proficiency in JavaScript/TypeScript', | |
| 'Experience with React', | |
| 'Knowledge of Node.js' | |
| ], | |
| location: 'San Francisco, CA', | |
| source: url | |
| } | |
| return NextResponse.json({ | |
| success: true, | |
| jobData: mockJobData | |
| }) | |
| } catch (error) { | |
| console.error('Scraping error:', error) | |
| return NextResponse.json( | |
| { error: 'Failed to scrape job posting' }, | |
| { status: 500 } | |
| ) | |
| } | |
| } | |
| EOF | |
| # Create comprehensive analysis API route | |
| mkdir -p app/api/analyze | |
| cat > app/api/analyze/route.ts << 'EOF' | |
| import { openai } from '@ai-sdk/openai' | |
| import { generateObject } from 'ai' | |
| import { NextRequest, NextResponse } from 'next/server' | |
| import { z } from 'zod' | |
| const analysisSchema = z.object({ | |
| matchScore: z.number().min(0).max(100), | |
| technicalSkills: z.array(z.string()), | |
| softSkills: z.array(z.string()), | |
| matchingSkills: z.array(z.string()), | |
| interviewQuestions: z.array(z.object({ | |
| category: z.enum(['technical', 'behavioral', 'culture']), | |
| question: z.string() | |
| })), | |
| recommendations: z.array(z.string()) | |
| }) | |
| export async function POST(req: NextRequest) { | |
| try { | |
| const { resumeText, jobDescription } = await req.json() | |
| if (!resumeText || !jobDescription) { | |
| return NextResponse.json( | |
| { error: 'Resume text and job description are required' }, | |
| { status: 400 } | |
| ) | |
| } | |
| const { object } = await generateObject({ | |
| model: openai('gpt-4o-mini'), | |
| prompt: `Analyze this resume against the job posting and provide a comprehensive interview preparation analysis. | |
| Resume: | |
| ${resumeText} | |
| Job Posting: | |
| ${jobDescription} | |
| Provide: | |
| 1. Match score (0-100) based on skills and experience alignment | |
| 2. Technical skills found in resume | |
| 3. Soft skills identified | |
| 4. Skills that match between resume and job | |
| 5. 6 interview questions (2 technical, 2 behavioral, 2 culture-fit) | |
| 6. 4 recommendations for interview preparation | |
| Be accurate and helpful in your analysis.`, | |
| schema: analysisSchema, | |
| }) | |
| return NextResponse.json({ | |
| success: true, | |
| analysis: object | |
| }) | |
| } catch (error) { | |
| console.error('Analysis error:', error) | |
| return NextResponse.json( | |
| { error: 'Failed to analyze resume and job posting' }, | |
| { status: 500 } | |
| ) | |
| } | |
| } | |
| EOF | |
| # Update the main page to include our IQKiller features | |
| cat > app/page.tsx << 'EOF' | |
| 'use client' | |
| import { useState } from 'react' | |
| import { useObject } from 'ai/react' | |
| import { FileUpload } from '@/components/file-upload' | |
| import { JobAnalysis } from '@/components/job-analysis' | |
| import { InterviewAnalysis } from '@/components/interview-analysis' | |
| import { Button } from '@/components/ui/button' | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' | |
| import { Separator } from '@/components/ui/separator' | |
| import { Loader2 } from 'lucide-react' | |
| export default function Home() { | |
| const [resumeText, setResumeText] = useState('') | |
| const [jobData, setJobData] = useState<any>(null) | |
| const [showAnalysis, setShowAnalysis] = useState(false) | |
| const { object, submit, isLoading } = useObject({ | |
| api: '/api/analyze', | |
| schema: z.object({ | |
| matchScore: z.number(), | |
| technicalSkills: z.array(z.string()), | |
| softSkills: z.array(z.string()), | |
| matchingSkills: z.array(z.string()), | |
| interviewQuestions: z.array(z.object({ | |
| category: z.enum(['technical', 'behavioral', 'culture']), | |
| question: z.string() | |
| })), | |
| recommendations: z.array(z.string()) | |
| }), | |
| }) | |
| const handleAnalyze = () => { | |
| if (!resumeText.trim() || !jobData) return | |
| submit({ | |
| resumeText, | |
| jobDescription: jobData.description || JSON.stringify(jobData) | |
| }) | |
| setShowAnalysis(true) | |
| } | |
| const canAnalyze = resumeText.trim() && jobData | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100"> | |
| <div className="container mx-auto px-4 py-8"> | |
| {/* Header */} | |
| <div className="text-center mb-12"> | |
| <h1 className="text-4xl font-bold text-gray-900 mb-4"> | |
| π― IQKiller | |
| </h1> | |
| <p className="text-xl text-gray-600 max-w-2xl mx-auto"> | |
| AI-powered interview preparation platform. Upload your resume, add a job posting, | |
| and get personalized interview questions and preparation strategies. | |
| </p> | |
| </div> | |
| {!showAnalysis ? ( | |
| /* Input Phase */ | |
| <div className="max-w-4xl mx-auto space-y-8"> | |
| <div className="grid md:grid-cols-2 gap-8"> | |
| {/* Resume Upload */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Upload Your Resume</CardTitle> | |
| <CardDescription> | |
| Upload a PDF resume for AI-powered analysis | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <FileUpload onFileContent={setResumeText} /> | |
| {resumeText && ( | |
| <div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-md"> | |
| <p className="text-sm text-green-700"> | |
| β Resume processed ({resumeText.length} characters) | |
| </p> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| {/* Job Analysis */} | |
| <JobAnalysis onJobData={setJobData} /> | |
| </div> | |
| {/* Analyze Button */} | |
| {canAnalyze && ( | |
| <div className="text-center"> | |
| <Separator className="mb-8" /> | |
| <Button | |
| onClick={handleAnalyze} | |
| disabled={isLoading} | |
| size="lg" | |
| className="px-8 py-3 text-lg" | |
| > | |
| {isLoading && <Loader2 className="w-5 h-5 mr-2 animate-spin" />} | |
| Generate Interview Analysis | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| /* Results Phase */ | |
| <div className="max-w-6xl mx-auto"> | |
| <div className="mb-8 text-center"> | |
| <Button | |
| variant="outline" | |
| onClick={() => setShowAnalysis(false)} | |
| className="mb-4" | |
| > | |
| β Start New Analysis | |
| </Button> | |
| <h2 className="text-2xl font-bold text-gray-900"> | |
| Your Interview Preparation Report | |
| </h2> | |
| </div> | |
| {isLoading ? ( | |
| <div className="text-center py-12"> | |
| <Loader2 className="w-12 h-12 animate-spin mx-auto mb-4" /> | |
| <p className="text-lg text-gray-600">Analyzing your resume and job posting...</p> | |
| </div> | |
| ) : object ? ( | |
| <InterviewAnalysis analysis={object} /> | |
| ) : ( | |
| <div className="text-center py-12"> | |
| <p className="text-lg text-gray-600">Analysis in progress...</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| EOF | |
| # Add missing import to the main page | |
| sed -i '1i import { z } from "zod"' app/page.tsx | |
| # Update environment variables template | |
| cat > .env.example << 'EOF' | |
| # AI Providers | |
| OPENAI_API_KEY=sk-proj-your-openai-key | |
| ANTHROPIC_API_KEY=sk-ant-your-anthropic-key | |
| # IQKiller Services | |
| FIRECRAWL_API_KEY=fc-your-firecrawl-key | |
| SERPAPI_KEY=your-serpapi-key | |
| # Vercel Storage (optional for production) | |
| KV_URL=your-vercel-kv-url | |
| KV_REST_API_URL=your-kv-rest-url | |
| KV_REST_API_TOKEN=your-kv-token | |
| KV_REST_API_READ_ONLY_TOKEN=your-kv-readonly-token | |
| POSTGRES_URL=your-neon-postgres-url | |
| # App Configuration | |
| NODE_ENV=development | |
| NEXT_PUBLIC_APP_URL=http://localhost:3000 | |
| EOF | |
| # Create Vercel deployment configuration | |
| cat > vercel.json << 'EOF' | |
| { | |
| "framework": "nextjs", | |
| "buildCommand": "npm run build", | |
| "devCommand": "npm run dev", | |
| "installCommand": "npm install", | |
| "env": { | |
| "OPENAI_API_KEY": "@openai-api-key", | |
| "ANTHROPIC_API_KEY": "@anthropic-api-key", | |
| "FIRECRAWL_API_KEY": "@firecrawl-api-key", | |
| "SERPAPI_KEY": "@serpapi-key" | |
| }, | |
| "regions": ["iad1", "sfo1", "lhr1"], | |
| "functions": { | |
| "app/api/analyze/route.ts": { | |
| "maxDuration": 30 | |
| }, | |
| "app/api/scrape/route.ts": { | |
| "maxDuration": 15 | |
| } | |
| } | |
| } | |
| EOF | |
| # Update package.json scripts | |
| npm pkg set scripts.deploy="vercel --prod" | |
| npm pkg set scripts.dev:vercel="vercel dev" | |
| # Initialize git repository | |
| git add . | |
| git commit -m "π― IQKiller: Vercel AI SDK foundation with custom features | |
| - PDF resume upload and analysis | |
| - Job posting scraping (URL + manual) | |
| - AI-powered interview question generation | |
| - Comprehensive skill matching | |
| - Modern UI with shadcn/ui | |
| - Ready for Vercel deployment" | |
| echo "" | |
| echo "β IQKiller setup complete!" | |
| echo "============================================" | |
| echo "π Project location: $PWD" | |
| echo "π Next steps:" | |
| echo " 1. cp .env.example .env.local" | |
| echo " 2. Add your API keys to .env.local" | |
| echo " 3. npm run dev" | |
| echo " 4. Open http://localhost:3000" | |
| echo "" | |
| echo "π Ready to deploy to Vercel:" | |
| echo " β’ Install Vercel CLI: npm i -g vercel" | |
| echo " β’ Deploy: npm run deploy" | |
| echo "" | |
| echo "π Features included:" | |
| echo " β PDF resume upload (Vercel AI SDK)" | |
| echo " β Job posting analysis" | |
| echo " β AI interview questions" | |
| echo " β Skills matching" | |
| echo " β Vercel deployment ready" | |
| echo "" | |
| echo "π― IQKiller is ready to revolutionize interview prep!" | |
| EOF |