mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 09:24:54 +10:00
feat(jobs): add job listings with AI-powered resume tailoring (#2788)
* feat: add job listings feature with JSearch API integration, resume tailoring, and per-user rate limiting * feat(jobs): add search filters UI, filter helper functions with tests, and job_search_quota DB migration * feat(jobs): add pagination with 30 results per page and prev/next navigation * refactor(job-detail): Adjust sheet width and scroll area height * feat(ai): Add resume tailoring feature and prompt * refactor(ai): Revise tailoring prompts and schema for full skill rewrite * feat(ai): Add reference tailoring and output sanitization * feat(testing): Add Vitest testing framework * fix: address PR review - atomic rate limiting, calendar-month quota, skill sync warning, gitignore routeTree.gen.ts * feat(jobs): Add location filter to job listings * feat(job-listings): Add DOCX document generation * feat(job-listings): Enable search by location and on Enter key * feat(job-listings): Split location filter into city, state, and country * feat(jobs): Implement job search adapter and JSearch * Update 'locale/' directory * feat(resume): Simplify filename generation and add tests * fix(JSearch): reduce JSearch API usage to 1 request per search to prevent quota exhaustion * fix(JSearch): Displayed quota amounts on Job Search functionality and settings fixed to pull from RapidAPI/JSearch response * fix(internal rate limit): Removed internal rate limit and .env.example addition, cloud based implementation handles. * style(job-filters): Adjust layout of switch filters * fix(typecheck): Fixed typecheck issues introduced to sync with origin * feat(jobs): Enhance tailor dialog with apply link and tags * feat(locale files): updated locale files with the latest build * feat(jobs): Add job search provider and integrate testing functionality - Introduced `createJobSearchProvider` function to instantiate a JSearchProvider. - Enhanced job search provider with methods for searching jobs, retrieving job details, and testing connection. - Updated `vite.config.ts` to include new testing configurations and plugins. - Added new dependencies in `package.json` for testing and document generation. - Removed obsolete `vitest.config.ts` file. - Improved job search provider tests for better coverage and reliability. * refactor: Update job search routes and remove obsolete test configurations - Removed the test configuration from `vite.config.ts`. - Updated localization files to reflect changes in job search routes, renaming references from `jobs` to `job-search` across multiple languages. - Adjusted autofix workflow to run formatting without the `--fix` flag for better control over code style adjustments. * chore: Update dependencies and improve animation performance - Added `jsdom` as a new dependency in `package.json`. - Updated `vite-plus` and `vitest` to the latest versions for better compatibility. - Enhanced animation components with `willChange` styles to optimize rendering performance. - Adjusted various UI components to improve responsiveness and visual effects. - Removed obsolete job details functionality from the job search provider and related tests. * chore(locales): Update localization files for job search improvements - Modified job search related strings to remove references to "this month" for a more concise format. - Updated file references in localization entries to reflect changes in the job search component structure. - Added new strings for API usage, quota remaining, and job fetching error messages across multiple languages. - Removed obsolete "Monthly Usage" string from localization files. * chore(dependencies): Update @typescript/native-preview to version 7.0.0-dev.20260319.1 --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import { AISDKError, type UIMessage } from "ai";
|
||||
import { OllamaError } from "ai-sdk-ollama";
|
||||
import z, { flattenError, ZodError } from "zod";
|
||||
|
||||
import type { ResumeData } from "@/schema/resume/data";
|
||||
import { jobResultSchema } from "@/schema/jobs";
|
||||
import { type ResumeData, resumeDataSchema } from "@/schema/resume/data";
|
||||
import { tailorOutputSchema } from "@/schema/tailor";
|
||||
|
||||
import { protectedProcedure } from "../context";
|
||||
import { aiCredentialsSchema, aiProviderSchema, aiService, fileInputSchema } from "../services/ai";
|
||||
@@ -172,6 +174,54 @@ export const aiRouter = {
|
||||
throw new ORPCError("BAD_GATEWAY", { message: error.message });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
|
||||
tailorResume: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/ai/tailor-resume",
|
||||
tags: ["AI"],
|
||||
operationId: "tailorResume",
|
||||
summary: "Auto-tailor resume for a job posting",
|
||||
description:
|
||||
"Uses AI to automatically tailor a resume for a specific job posting. Rewrites the summary, adjusts experience descriptions, and curates skills for ATS optimization. Returns structured modifications as a simplified output object. Requires authentication and AI credentials.",
|
||||
successDescription: "Structured tailoring output returned successfully.",
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
...aiCredentialsSchema.shape,
|
||||
resumeData: resumeDataSchema,
|
||||
job: jobResultSchema,
|
||||
}),
|
||||
)
|
||||
.output(tailorOutputSchema)
|
||||
.errors({
|
||||
BAD_GATEWAY: {
|
||||
message: "The AI provider returned an error or is unreachable.",
|
||||
status: 502,
|
||||
},
|
||||
BAD_REQUEST: {
|
||||
message: "The AI returned an improperly formatted structure.",
|
||||
status: 400,
|
||||
},
|
||||
})
|
||||
.handler(async ({ input }) => {
|
||||
try {
|
||||
return await aiService.tailorResume(input);
|
||||
} catch (error) {
|
||||
if (error instanceof AISDKError || error instanceof OllamaError) {
|
||||
throw new ORPCError("BAD_GATEWAY", { message: error.message });
|
||||
}
|
||||
|
||||
if (error instanceof ZodError) {
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: "Invalid resume data structure",
|
||||
cause: flattenError(error),
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { aiRouter } from "./ai";
|
||||
import { authRouter } from "./auth";
|
||||
import { flagsRouter } from "./flags";
|
||||
import { jobsRouter } from "./jobs";
|
||||
import { printerRouter } from "./printer";
|
||||
import { resumeRouter } from "./resume";
|
||||
import { statisticsRouter } from "./statistics";
|
||||
@@ -10,8 +11,9 @@ export default {
|
||||
ai: aiRouter,
|
||||
auth: authRouter,
|
||||
flags: flagsRouter,
|
||||
resume: resumeRouter,
|
||||
storage: storageRouter,
|
||||
jobs: jobsRouter,
|
||||
printer: printerRouter,
|
||||
resume: resumeRouter,
|
||||
statistics: statisticsRouter,
|
||||
storage: storageRouter,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ORPCError } from "@orpc/client";
|
||||
import z from "zod";
|
||||
|
||||
import { postFilterOptionsSchema, searchParamsSchema } from "@/schema/jobs";
|
||||
|
||||
import { protectedProcedure } from "../context";
|
||||
import { jobsService } from "../services/jobs";
|
||||
|
||||
export const jobsRouter = {
|
||||
testConnection: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/jobs/test-connection",
|
||||
tags: ["Jobs"],
|
||||
operationId: "testJobsConnection",
|
||||
summary: "Test RapidAPI JSearch connection",
|
||||
description:
|
||||
"Validates the RapidAPI key by performing a minimal test search against the JSearch API. Requires authentication.",
|
||||
successDescription: "The RapidAPI key is valid and JSearch is reachable.",
|
||||
})
|
||||
.input(z.object({ apiKey: z.string().min(1) }))
|
||||
.errors({
|
||||
BAD_GATEWAY: {
|
||||
message: "The JSearch API returned an error or is unreachable.",
|
||||
status: 502,
|
||||
},
|
||||
})
|
||||
.handler(async ({ input }) => {
|
||||
try {
|
||||
return await jobsService.testConnection(input.apiKey);
|
||||
} catch (error) {
|
||||
throw new ORPCError("BAD_GATEWAY", {
|
||||
message: error instanceof Error ? error.message : "Connection test failed",
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.route({
|
||||
method: "POST",
|
||||
path: "/jobs/search",
|
||||
tags: ["Jobs"],
|
||||
operationId: "searchJobs",
|
||||
summary: "Search for job listings",
|
||||
description:
|
||||
"Searches the JSearch API for job listings matching the given parameters. Results are deduplicated and optionally filtered. Requires authentication.",
|
||||
successDescription: "Job search results returned successfully.",
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
apiKey: z.string().min(1),
|
||||
params: searchParamsSchema,
|
||||
filters: postFilterOptionsSchema.optional(),
|
||||
}),
|
||||
)
|
||||
.errors({
|
||||
BAD_GATEWAY: {
|
||||
message: "The JSearch API returned an error or is unreachable.",
|
||||
status: 502,
|
||||
},
|
||||
})
|
||||
.handler(async ({ input }) => {
|
||||
try {
|
||||
const response = await jobsService.search(input.apiKey, input.params);
|
||||
|
||||
let jobs = jobsService.deduplicateJobs(response.data);
|
||||
|
||||
if (input.filters) {
|
||||
jobs = jobsService.applyPostFilters(jobs, input.filters);
|
||||
}
|
||||
|
||||
return { data: jobs, rapidApiQuota: response.rapidApiQuota };
|
||||
} catch (error) {
|
||||
throw new ORPCError("BAD_GATEWAY", {
|
||||
message: error instanceof Error ? error.message : "Search failed",
|
||||
});
|
||||
}
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user