+>(({ className, ...props }, ref) => (
+ | [role=checkbox]]:translate-y-0.5",
+ className,
+ )}
+ {...props}
+ />
+));
+TableCell.displayName = "TableCell";
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = "TableCaption";
+
+export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
diff --git a/job-hunting/components/ui/tabs.tsx b/job-hunting/components/ui/tabs.tsx
new file mode 100644
index 0000000..497ba5e
--- /dev/null
+++ b/job-hunting/components/ui/tabs.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+function Tabs({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/job-hunting/components/ui/textarea.tsx b/job-hunting/components/ui/textarea.tsx
new file mode 100644
index 0000000..7f21b5e
--- /dev/null
+++ b/job-hunting/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/job-hunting/eslint.config.mjs b/job-hunting/eslint.config.mjs
new file mode 100644
index 0000000..05e726d
--- /dev/null
+++ b/job-hunting/eslint.config.mjs
@@ -0,0 +1,18 @@
+import { defineConfig, globalIgnores } from "eslint/config";
+import nextVitals from "eslint-config-next/core-web-vitals";
+import nextTs from "eslint-config-next/typescript";
+
+const eslintConfig = defineConfig([
+ ...nextVitals,
+ ...nextTs,
+ // Override default ignores of eslint-config-next.
+ globalIgnores([
+ // Default ignores of eslint-config-next:
+ ".next/**",
+ "out/**",
+ "build/**",
+ "next-env.d.ts",
+ ]),
+]);
+
+export default eslintConfig;
diff --git a/job-hunting/lib/ai/client.ts b/job-hunting/lib/ai/client.ts
new file mode 100644
index 0000000..f2595c4
--- /dev/null
+++ b/job-hunting/lib/ai/client.ts
@@ -0,0 +1,251 @@
+"use client";
+
+/**
+ * Client-side API calls to server-side AI routes
+ * API keys are kept secure on the server
+ */
+
+import type { UserProfile, SearchConfig, Job, GeneratedSearchUrl } from "../types";
+
+export interface ParsedResume {
+ fullName: string;
+ email: string;
+ phone: string | null;
+ location: string;
+ currentTitle: string;
+ yearsExperience: number;
+ skills: string[];
+ industries: string[];
+ education: string;
+ preferredTitles: string[];
+ seniorityLevel: "entry" | "mid" | "senior" | "lead" | "executive";
+ summary: string;
+}
+
+export interface JobMatchResult {
+ matchScore: number;
+ matchExplanation: string;
+ keyStrengths: string[];
+ potentialConcerns: string[];
+ isReach: boolean;
+ isPerfectFit: boolean;
+}
+
+/**
+ * Parse resume text using AI
+ */
+export async function parseResume(resumeText: string): Promise {
+ const response = await fetch("/api/ai/parse-resume", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ resumeText }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to parse resume");
+ }
+
+ const { data } = await response.json();
+ return data;
+}
+
+/**
+ * Generate job board search URLs
+ */
+export async function generateSearchUrls(
+ profile: UserProfile,
+ searchConfig: SearchConfig
+): Promise {
+ const response = await fetch("/api/ai/generate-urls", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ profile, searchConfig }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to generate search URLs");
+ }
+
+ const { data } = await response.json();
+ return data;
+}
+
+/**
+ * Analyze job match with AI
+ */
+export async function analyzeJobMatch(
+ job: Partial,
+ profile: UserProfile
+): Promise {
+ const response = await fetch("/api/ai/match-jobs", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ job, profile }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to analyze job match");
+ }
+
+ const { data } = await response.json();
+ return data;
+}
+
+/**
+ * Generate cover letter
+ */
+export async function generateCoverLetter(
+ job: Job,
+ profile: UserProfile
+): Promise {
+ const response = await fetch("/api/ai/cover-letter", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ job, profile }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to generate cover letter");
+ }
+
+ const { data } = await response.json();
+ return data;
+}
+
+/**
+ * Batch analyze multiple jobs
+ */
+export async function batchAnalyzeJobs(
+ jobs: Partial[],
+ profile: UserProfile,
+ onProgress?: (current: number, total: number) => void
+): Promise |