use person name from base.json for file downloads
This commit is contained in:
parent
e5c99d54bf
commit
6a3a25578a
@ -171,7 +171,15 @@ export async function getSettings(): Promise<AppSettings> {
|
||||
}
|
||||
|
||||
export async function getProfileProjects(): Promise<ResumeProjectCatalogItem[]> {
|
||||
return fetchApi<ResumeProjectCatalogItem[]>('/profile/projects');
|
||||
return fetchApi<ResumeProjectCatalogItem[]>('/profile/projects', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProfile(): Promise<any> {
|
||||
return fetchApi<any>('/profile', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -45,6 +45,7 @@ import { cn, copyTextToClipboard, formatJobForWebhook } from "@/lib/utils";
|
||||
import * as api from "../api";
|
||||
import { FitAssessment, JobHeader, TailoredSummary } from ".";
|
||||
import { TailorMode } from "./discovered-panel/TailorMode";
|
||||
import { useProfile } from "../hooks/useProfile";
|
||||
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
|
||||
|
||||
type PanelMode = "ready" | "tailor";
|
||||
@ -74,6 +75,8 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
|
||||
timeoutId: ReturnType<typeof setTimeout>;
|
||||
} | null>(null);
|
||||
|
||||
const { personName } = useProfile();
|
||||
|
||||
// Load project catalog once
|
||||
useEffect(() => {
|
||||
api.getProfileProjects().then(setCatalog).catch(console.error);
|
||||
@ -279,7 +282,7 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
|
||||
<Button asChild variant="outline" className="h-9 w-full gap-1 px-2 text-xs">
|
||||
<a
|
||||
href={pdfHref}
|
||||
download={`Shaheer_Sarfaraz_${safeFilenamePart(job.employer)}.pdf`}
|
||||
download={`${personName.replace(/\s+/g, '_')}_${safeFilenamePart(job.employer)}.pdf`}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">Download PDF</span>
|
||||
|
||||
90
orchestrator/src/client/hooks/useProfile.ts
Normal file
90
orchestrator/src/client/hooks/useProfile.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as api from '../api';
|
||||
|
||||
let profileCache: any = null;
|
||||
let profileError: Error | null = null;
|
||||
let subscribers: Set<(profile: any | null, error: Error | null) => void> = new Set();
|
||||
let isFetching = false;
|
||||
|
||||
/**
|
||||
* Hook to get the full profile data from base.json.
|
||||
* Caches the result to avoid re-fetching.
|
||||
*/
|
||||
export function useProfile() {
|
||||
const [profile, setProfile] = useState<any | null>(profileCache);
|
||||
const [error, setError] = useState<Error | null>(profileError);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileCache) {
|
||||
setProfile(profileCache);
|
||||
}
|
||||
if (profileError) {
|
||||
setError(profileError);
|
||||
}
|
||||
|
||||
const handleUpdate = (newProfile: any | null, newError: Error | null) => {
|
||||
setProfile(newProfile);
|
||||
setError(newError);
|
||||
};
|
||||
|
||||
subscribers.add(handleUpdate);
|
||||
|
||||
if (!profileCache && !isFetching) {
|
||||
isFetching = true;
|
||||
profileError = null;
|
||||
api.getProfile()
|
||||
.then((data) => {
|
||||
profileCache = data;
|
||||
profileError = null;
|
||||
subscribers.forEach(sub => sub(data, null));
|
||||
})
|
||||
.catch((err) => {
|
||||
profileError = err instanceof Error ? err : new Error(String(err));
|
||||
subscribers.forEach(sub => sub(profileCache, profileError));
|
||||
})
|
||||
.finally(() => {
|
||||
isFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscribers.delete(handleUpdate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshProfile = async () => {
|
||||
isFetching = true;
|
||||
profileError = null;
|
||||
subscribers.forEach(sub => sub(profileCache, null));
|
||||
|
||||
try {
|
||||
const data = await api.getProfile();
|
||||
profileCache = data;
|
||||
profileError = null;
|
||||
subscribers.forEach(sub => sub(data, null));
|
||||
return data;
|
||||
} catch (err) {
|
||||
profileError = err instanceof Error ? err : new Error(String(err));
|
||||
subscribers.forEach(sub => sub(profileCache, profileError));
|
||||
throw profileError;
|
||||
} finally {
|
||||
isFetching = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
profile,
|
||||
error,
|
||||
isLoading: !profile && isFetching && !error,
|
||||
personName: profile?.basics?.name || 'Resume',
|
||||
refreshProfile,
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal For testing only */
|
||||
export function _resetProfileCache() {
|
||||
profileCache = null;
|
||||
profileError = null;
|
||||
isFetching = false;
|
||||
subscribers.clear();
|
||||
}
|
||||
@ -30,6 +30,7 @@ import { copyTextToClipboard, formatJobForWebhook, safeFilenamePart, stripHtml }
|
||||
import { DiscoveredPanel, FitAssessment, JobHeader, TailoredSummary } from "../../components";
|
||||
import { ReadyPanel } from "../../components/ReadyPanel";
|
||||
import { TailoringEditor } from "../../components/TailoringEditor";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
import * as api from "../../api";
|
||||
import type { Job } from "../../../shared/types";
|
||||
import type { FilterTab } from "./constants";
|
||||
@ -59,6 +60,8 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
||||
const [processingJobId, setProcessingJobId] = useState<string | null>(null);
|
||||
const saveTailoringRef = useRef<null | (() => Promise<void>)>(null);
|
||||
|
||||
const { personName } = useProfile();
|
||||
|
||||
useEffect(() => {
|
||||
setHasUnsavedTailoring(false);
|
||||
saveTailoringRef.current = null;
|
||||
@ -363,7 +366,7 @@ export const JobDetailPanel: React.FC<JobDetailPanelProps> = ({
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href={selectedPdfHref}
|
||||
download={`Shaheer_Sarfaraz_${safeFilenamePart(selectedJob.employer)}.pdf`}
|
||||
download={`${personName.replace(/\s+/g, '_')}_${safeFilenamePart(selectedJob.employer)}.pdf`}
|
||||
>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Download PDF
|
||||
|
||||
@ -4,9 +4,9 @@ import { extractProjectsFromProfile, loadResumeProfile } from '../../services/re
|
||||
export const profileRouter = Router();
|
||||
|
||||
/**
|
||||
* GET /api/profile/projects - Get all projects available in the base resume
|
||||
* POST /api/profile/projects - Get all projects available in the base resume
|
||||
*/
|
||||
profileRouter.get('/projects', async (req: Request, res: Response) => {
|
||||
profileRouter.post('/projects', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const profile = await loadResumeProfile();
|
||||
const { catalog } = extractProjectsFromProfile(profile);
|
||||
@ -16,3 +16,16 @@ profileRouter.get('/projects', async (req: Request, res: Response) => {
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/profile - Get the full base resume profile
|
||||
*/
|
||||
profileRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const profile = await loadResumeProfile();
|
||||
res.json({ success: true, data: profile });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
@ -16,6 +16,7 @@ import { runUkVisaJobs } from '../services/ukvisajobs.js';
|
||||
import { scoreJobSuitability } from '../services/scorer.js';
|
||||
import { generateTailoring } from '../services/summary.js';
|
||||
import { generatePdf } from '../services/pdf.js';
|
||||
import { getProfile } from '../services/profile.js';
|
||||
import { getSetting } from '../repositories/settings.js';
|
||||
import { pickProjectIdsForJob } from '../services/projectSelection.js';
|
||||
import { extractProjectsFromProfile, resolveResumeProjectsSettings } from '../services/resumeProjects.js';
|
||||
@ -112,7 +113,7 @@ export async function runPipeline(config: Partial<PipelineConfig> = {}): Promise
|
||||
try {
|
||||
// Step 1: Load profile
|
||||
console.log('\n📋 Loading profile...');
|
||||
const profile = await loadProfile(mergedConfig.profilePath);
|
||||
const profile = await getProfile();
|
||||
|
||||
// Step 2: Run crawler
|
||||
console.log('\n🕷️ Running crawler...');
|
||||
@ -429,7 +430,7 @@ export async function summarizeJob(
|
||||
const job = await jobsRepo.getJobById(jobId);
|
||||
if (!job) return { success: false, error: 'Job not found' };
|
||||
|
||||
const profile = await loadProfile(DEFAULT_PROFILE_PATH);
|
||||
const profile = await getProfile();
|
||||
|
||||
// 1. Generate Summary & Tailoring
|
||||
let tailoredSummary = job.tailoredSummary;
|
||||
@ -566,15 +567,3 @@ export function getPipelineStatus(): { isRunning: boolean } {
|
||||
return { isRunning: isPipelineRunning };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the user profile from JSON file.
|
||||
*/
|
||||
async function loadProfile(profilePath: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const content = await readFile(profilePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load profile, using empty object');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,3 +4,4 @@ export * from './scorer.js';
|
||||
export * from './summary.js';
|
||||
export * from './pdf.js';
|
||||
export * from './notion.js';
|
||||
export * from './profile.js';
|
||||
|
||||
@ -13,6 +13,7 @@ import { getSetting } from '../repositories/settings.js';
|
||||
import { pickProjectIdsForJob } from './projectSelection.js';
|
||||
import { extractProjectsFromProfile, resolveResumeProjectsSettings } from './resumeProjects.js';
|
||||
import { getDataDir } from '../config/dataDir.js';
|
||||
import { getProfile } from './profile.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@ -59,7 +60,9 @@ export async function generatePdf(
|
||||
}
|
||||
|
||||
// Read base resume
|
||||
const baseResume = JSON.parse(await readFile(resumeJsonPath, 'utf-8'));
|
||||
const baseResume = baseResumePath
|
||||
? JSON.parse(await readFile(baseResumePath, 'utf-8'))
|
||||
: JSON.parse(JSON.stringify(await getProfile())); // Deep copy from cache
|
||||
|
||||
// Inject tailored summary
|
||||
if (tailoredContent.summary) {
|
||||
|
||||
42
orchestrator/src/server/services/profile.ts
Normal file
42
orchestrator/src/server/services/profile.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_PROFILE_PATH = join(__dirname, '../../../../resume-generator/base.json');
|
||||
|
||||
let cachedProfile: any = null;
|
||||
|
||||
/**
|
||||
* Get the base resume profile from base.json.
|
||||
* Caches the result since it doesn't change often.
|
||||
*/
|
||||
export async function getProfile(forceRefresh = false): Promise<any> {
|
||||
if (cachedProfile && !forceRefresh) {
|
||||
return cachedProfile;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(DEFAULT_PROFILE_PATH, 'utf-8');
|
||||
cachedProfile = JSON.parse(content);
|
||||
return cachedProfile;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load profile from base.json:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the person's name from the profile.
|
||||
*/
|
||||
export async function getPersonName(): Promise<string> {
|
||||
const profile = await getProfile();
|
||||
return profile?.basics?.name || 'Resume';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the profile cache.
|
||||
*/
|
||||
export function clearProfileCache(): void {
|
||||
cachedProfile = null;
|
||||
}
|
||||
@ -4,6 +4,8 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
import type { ResumeProjectCatalogItem, ResumeProjectsSettings } from '../../shared/types.js';
|
||||
|
||||
import { getProfile } from './profile.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const DEFAULT_RESUME_PROFILE_PATH =
|
||||
@ -12,6 +14,9 @@ export const DEFAULT_RESUME_PROFILE_PATH =
|
||||
type ResumeProjectSelectionItem = ResumeProjectCatalogItem & { summaryText: string };
|
||||
|
||||
export async function loadResumeProfile(profilePath: string = DEFAULT_RESUME_PROFILE_PATH): Promise<unknown> {
|
||||
if (profilePath === DEFAULT_RESUME_PROFILE_PATH) {
|
||||
return getProfile();
|
||||
}
|
||||
const content = await readFile(profilePath, 'utf-8');
|
||||
return JSON.parse(content) as unknown;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user