From 4f4f00e42cb6bcc94673f1a93fcc31ec73c8f68d Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Mon, 19 Jan 2026 18:39:14 +0000 Subject: [PATCH 01/12] manual import initial comit --- orchestrator/src/client/api/client.ts | 21 + .../src/client/components/DiscoveredPanel.tsx | 1 + orchestrator/src/client/components/Header.tsx | 1 + .../client/components/ManualImportSheet.tsx | 424 ++++++++++++++++++ orchestrator/src/client/components/index.ts | 1 + .../src/client/pages/OrchestratorPage.tsx | 29 +- orchestrator/src/server/api/routes.ts | 116 ++++- orchestrator/src/server/db/schema.ts | 2 +- orchestrator/src/server/services/manualJob.ts | 164 +++++++ orchestrator/src/shared/types.ts | 25 +- 10 files changed, 779 insertions(+), 5 deletions(-) create mode 100644 orchestrator/src/client/components/ManualImportSheet.tsx create mode 100644 orchestrator/src/server/services/manualJob.ts diff --git a/orchestrator/src/client/api/client.ts b/orchestrator/src/client/api/client.ts index 56111d7..0b7c38c 100644 --- a/orchestrator/src/client/api/client.ts +++ b/orchestrator/src/client/api/client.ts @@ -15,6 +15,8 @@ import type { UkVisaJobsSearchResponse, UkVisaJobsImportResponse, CreateJobInput, + ManualJobDraft, + ManualJobInferenceResponse, VisaSponsorSearchResponse, VisaSponsorStatusResponse, VisaSponsor, @@ -138,6 +140,25 @@ export async function importUkVisaJobs(input: { }); } +// Manual Job Import API +export async function inferManualJob(input: { + jobDescription: string; +}): Promise { + return fetchApi('/manual-jobs/infer', { + method: 'POST', + body: JSON.stringify(input), + }); +} + +export async function importManualJob(input: { + job: ManualJobDraft; +}): Promise { + return fetchApi('/manual-jobs/import', { + method: 'POST', + body: JSON.stringify(input), + }); +} + // Settings & Profile API export async function getSettings(): Promise { return fetchApi('/settings'); diff --git a/orchestrator/src/client/components/DiscoveredPanel.tsx b/orchestrator/src/client/components/DiscoveredPanel.tsx index d04e284..cf1f3c0 100644 --- a/orchestrator/src/client/components/DiscoveredPanel.tsx +++ b/orchestrator/src/client/components/DiscoveredPanel.tsx @@ -73,6 +73,7 @@ const sourceLabel: Record = { indeed: "Indeed", linkedin: "LinkedIn", ukvisajobs: "UK Visa Jobs", + manual: "Manual", }; // ───────────────────────────────────────────────────────────────────────────── diff --git a/orchestrator/src/client/components/Header.tsx b/orchestrator/src/client/components/Header.tsx index 7fd9c57..ec014e7 100644 --- a/orchestrator/src/client/components/Header.tsx +++ b/orchestrator/src/client/components/Header.tsx @@ -60,6 +60,7 @@ export const Header: React.FC = ({ indeed: "Indeed", linkedin: "LinkedIn", ukvisajobs: "UK Visa Jobs", + manual: "Manual", }; const orderedSources: JobSource[] = ["gradcracker", "indeed", "linkedin", "ukvisajobs"]; diff --git a/orchestrator/src/client/components/ManualImportSheet.tsx b/orchestrator/src/client/components/ManualImportSheet.tsx new file mode 100644 index 0000000..a582f5a --- /dev/null +++ b/orchestrator/src/client/components/ManualImportSheet.tsx @@ -0,0 +1,424 @@ +/** + * Manual job import flow (paste JD -> infer -> review -> import). + */ + +import React, { useEffect, useMemo, useState } from "react"; +import { ArrowLeft, FileText, Loader2, Sparkles } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils"; +import * as api from "../api"; +import type { ManualJobDraft } from "../../shared/types"; + +type ManualImportStep = "paste" | "loading" | "review"; + +type ManualJobDraftState = { + title: string; + employer: string; + jobUrl: string; + applicationLink: string; + location: string; + salary: string; + deadline: string; + jobDescription: string; + jobType: string; + jobLevel: string; + jobFunction: string; + disciplines: string; + degreeRequired: string; + starting: string; +}; + +const emptyDraft: ManualJobDraftState = { + title: "", + employer: "", + jobUrl: "", + applicationLink: "", + location: "", + salary: "", + deadline: "", + jobDescription: "", + jobType: "", + jobLevel: "", + jobFunction: "", + disciplines: "", + degreeRequired: "", + starting: "", +}; + +const normalizeDraft = (draft?: ManualJobDraft | null, jd?: string): ManualJobDraftState => ({ + ...emptyDraft, + title: draft?.title ?? "", + employer: draft?.employer ?? "", + jobUrl: draft?.jobUrl ?? "", + applicationLink: draft?.applicationLink ?? "", + location: draft?.location ?? "", + salary: draft?.salary ?? "", + deadline: draft?.deadline ?? "", + jobDescription: jd ?? draft?.jobDescription ?? "", + jobType: draft?.jobType ?? "", + jobLevel: draft?.jobLevel ?? "", + jobFunction: draft?.jobFunction ?? "", + disciplines: draft?.disciplines ?? "", + degreeRequired: draft?.degreeRequired ?? "", + starting: draft?.starting ?? "", +}); + +const toPayload = (draft: ManualJobDraftState): ManualJobDraft => { + const clean = (value: string) => { + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; + }; + + return { + title: clean(draft.title), + employer: clean(draft.employer), + jobUrl: clean(draft.jobUrl), + applicationLink: clean(draft.applicationLink), + location: clean(draft.location), + salary: clean(draft.salary), + deadline: clean(draft.deadline), + jobDescription: clean(draft.jobDescription), + jobType: clean(draft.jobType), + jobLevel: clean(draft.jobLevel), + jobFunction: clean(draft.jobFunction), + disciplines: clean(draft.disciplines), + degreeRequired: clean(draft.degreeRequired), + starting: clean(draft.starting), + }; +}; + +interface ManualImportSheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onImported: (jobId: string) => void | Promise; +} + +export const ManualImportSheet: React.FC = ({ + open, + onOpenChange, + onImported, +}) => { + const [step, setStep] = useState("paste"); + const [rawDescription, setRawDescription] = useState(""); + const [draft, setDraft] = useState(emptyDraft); + const [warning, setWarning] = useState(null); + const [error, setError] = useState(null); + const [isImporting, setIsImporting] = useState(false); + + useEffect(() => { + if (!open) { + setStep("paste"); + setRawDescription(""); + setDraft(emptyDraft); + setWarning(null); + setError(null); + setIsImporting(false); + } + }, [open]); + + const stepIndex = step === "paste" ? 0 : step === "loading" ? 1 : 2; + const stepLabel = ["Paste JD", "Infer details", "Review & import"][stepIndex]; + + const canAnalyze = rawDescription.trim().length > 0 && step !== "loading"; + const canImport = useMemo(() => { + if (step !== "review") return false; + return ( + draft.title.trim().length > 0 && + draft.employer.trim().length > 0 && + draft.jobDescription.trim().length > 0 + ); + }, [draft, step]); + + const handleAnalyze = async () => { + if (!rawDescription.trim()) { + setError("Paste a job description to continue."); + return; + } + + try { + setError(null); + setWarning(null); + setStep("loading"); + const response = await api.inferManualJob({ jobDescription: rawDescription }); + setDraft(normalizeDraft(response.job, rawDescription.trim())); + setWarning(response.warning ?? null); + setStep("review"); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to analyze job description"; + setError(message); + setStep("paste"); + } + }; + + const handleImport = async () => { + if (!canImport) return; + + try { + setIsImporting(true); + const payload = toPayload(draft); + const created = await api.importManualJob({ job: payload }); + toast.success("Job imported", { + description: "The job is now in the discovered column.", + }); + await onImported(created.id); + onOpenChange(false); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to import job"; + toast.error(message); + } finally { + setIsImporting(false); + } + }; + + return ( + + +
+ + + + + + Manual Import + + + Paste a job description, review the AI draft, then import the role. + + + +
+
+
+ Step {stepIndex + 1} of 3 + {stepLabel} +
+
+
+
+
+ +
+ +
+ {step === "paste" && ( +
+
+ +