From da2f1f8244fb1445c5626de8c17809530f524737 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Mon, 17 Aug 2026 22:32:32 +0200 Subject: [PATCH] refactor(applications): autofill from a pasted posting instead of a URL Fetching an arbitrary job URL server side meant owning SSRF defence, redirect and size limits, and per-site scraping quirks. The autofill tool now takes only pasted text, so the URL input, the fetch path and its MCP annotation are gone. The sheet gates the call behind a tested AI provider and a minimum paste length so a stray snippet does not spend an AI call. --- .../components/application-form-sheet.tsx | 125 ++++--- .../api/src/features/applications/ai.test.ts | 151 +------- packages/api/src/features/applications/ai.ts | 353 +----------------- packages/mcp/src/mcp-server-card.test.ts | 11 +- packages/mcp/src/tool-annotations.test.ts | 9 +- packages/mcp/src/tool-meta.ts | 16 +- 6 files changed, 114 insertions(+), 551 deletions(-) diff --git a/apps/web/src/features/applications/components/application-form-sheet.tsx b/apps/web/src/features/applications/components/application-form-sheet.tsx index a8c30b037..6b0fb6ada 100644 --- a/apps/web/src/features/applications/components/application-form-sheet.tsx +++ b/apps/web/src/features/applications/components/application-form-sheet.tsx @@ -6,8 +6,8 @@ import { Trans } from "@lingui/react/macro"; import { SparkleIcon, XIcon } from "@phosphor-icons/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; -import { toast } from "sonner"; import { STAGES } from "@reactive-resume/schema/applications/data"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@reactive-resume/ui/components/accordion"; import { Button } from "@reactive-resume/ui/components/button"; import { Input } from "@reactive-resume/ui/components/input"; import { Label } from "@reactive-resume/ui/components/label"; @@ -20,6 +20,7 @@ import { SheetTitle, } from "@reactive-resume/ui/components/sheet"; import { Textarea } from "@reactive-resume/ui/components/textarea"; +import { toast } from "@reactive-resume/ui/components/toast"; import { Combobox } from "@/components/ui/combobox"; import { orpc } from "@/libs/orpc/client"; import { applicationsListQueryKey } from "../queries"; @@ -27,6 +28,10 @@ import { FileAttachmentField } from "./file-attachment-field"; // Preset source suggestions surfaced via a ; the field itself stays free-text. const SOURCE_OPTIONS = ["LinkedIn", "Indeed", "Company Website", "Referral", "Recruiter", "Other"]; +// Mirrors the server-side cap on `applications.ai.autofill`. +const MAX_JOB_DESCRIPTION_CHARS = 20_000; +// ponytail: a paste shorter than this is a snippet, not a posting — don't burn an AI call on it. +const MIN_AUTOFILL_CHARS = 200; const todayInputValue = () => new Date().toISOString().slice(0, 10); const emptyForm = () => ({ @@ -99,6 +104,10 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props) const { data: allTags } = useQuery(orpc.applications.tags.queryOptions()); + // Same gate as the rest of the AI surfaces: at least one enabled provider that tested green. + const { data: providers } = useQuery(orpc.aiProviders.list.queryOptions()); + const aiEnabled = providers?.some((provider) => provider.enabled && provider.testStatus === "success") ?? false; + const set = (key: K, value: FormState[K]) => setForm((prev) => ({ ...prev, [key]: value })); @@ -117,11 +126,11 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props) orpc.applications.create.mutationOptions({ onSuccess: () => { invalidate(); - toast.success(t`Application added to your pipeline.`); + toast.add({ type: "success", description: t`Application added to your pipeline.` }); setForm(emptyForm()); onOpenChange(false); }, - onError: () => toast.error(t`Couldn't add the application. Please try again.`), + onError: () => toast.add({ type: "error", description: t`Couldn't add the application. Please try again.` }), }), ); @@ -129,10 +138,10 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props) orpc.applications.update.mutationOptions({ onSuccess: () => { invalidate(); - toast.success(t`Application updated.`); + toast.add({ type: "success", description: t`Application updated.` }); onOpenChange(false); }, - onError: () => toast.error(t`Couldn't save your changes. Please try again.`), + onError: () => toast.add({ type: "error", description: t`Couldn't save your changes. Please try again.` }), }), ); @@ -145,14 +154,19 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props) role: result.role || prev.role, location: result.location || prev.location, salary: result.salary || prev.salary, - jobDescription: result.jobDescription || prev.jobDescription, })); - toast.success(t`Filled in what we could from the posting.`); + toast.add({ type: "success", description: t`Filled in what we could from the posting.` }); }, - onError: (error) => toast.error(error.message || t`Auto-fill failed. Paste the description instead.`), + onError: (error) => toast.add({ type: "error", description: error.message || t`Auto-fill failed.` }), }), ); + const runAutofill = (jobDescription: string) => { + const posting = jobDescription.trim(); + if (posting.length < MIN_AUTOFILL_CHARS || autofill.isPending) return; + autofill.mutate({ jobDescription: posting.slice(0, MAX_JOB_DESCRIPTION_CHARS) }); + }; + const pending = create.isPending || update.isPending; const submit = () => { @@ -195,32 +209,57 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props)
- {/* AI job-posting autofill: extracts the fields below from a posting URL. */} - {!isEditing && ( -
- -
- set("sourceUrl", event.target.value)} - /> - -
-

- Let AI read the posting and fill the fields below. -

-
+ {/* Pasted job description: stored with the application and used for every AI action. + Collapsed by default so the form stays short; hidden entirely when AI is off. */} + {aiEnabled && ( + + + + + + Job description + + + +

+ + Copy the entire job description from the posting and paste it below. We'll fill in the fields for + you and keep the text with this application for match scoring and tailoring. + +

+