api implemented
This commit is contained in:
@@ -186,6 +186,7 @@ export async function updateSettings(update: {
|
||||
jobspyCountryIndeed?: string | null
|
||||
jobspySites?: string[] | null
|
||||
jobspyLinkedinFetchDescription?: boolean | null
|
||||
rxResumeBaseResumeId?: string | null
|
||||
}): Promise<AppSettings> {
|
||||
return fetchApi<AppSettings>('/settings', {
|
||||
method: 'PATCH',
|
||||
@@ -193,6 +194,12 @@ export async function updateSettings(update: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRxResumes(): Promise<{ id: string; name: string }[]> {
|
||||
const data = await fetchApi<{ resumes: { id: string; name: string }[] }>('/settings/rx-resumes');
|
||||
return data.resumes;
|
||||
}
|
||||
|
||||
|
||||
// Database API
|
||||
export async function clearDatabase(): Promise<{
|
||||
message: string;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { PipelineWebhookSection } from "./settings/components/PipelineWebhookSec
|
||||
import { ResumeProjectsSection } from "./settings/components/ResumeProjectsSection"
|
||||
import { SearchTermsSection } from "./settings/components/SearchTermsSection"
|
||||
import { UkvisajobsSection } from "./settings/components/UkvisajobsSection"
|
||||
import { ReactiveResumeSection } from "./settings/components/ReactiveResumeSection"
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const [settings, setSettings] = useState<AppSettings | null>(null)
|
||||
@@ -41,6 +42,7 @@ export const SettingsPage: React.FC = () => {
|
||||
const [jobspyCountryIndeedDraft, setJobspyCountryIndeedDraft] = useState<string | null>(null)
|
||||
const [jobspySitesDraft, setJobspySitesDraft] = useState<string[] | null>(null)
|
||||
const [jobspyLinkedinFetchDescriptionDraft, setJobspyLinkedinFetchDescriptionDraft] = useState<boolean | null>(null)
|
||||
const [rxResumeBaseResumeIdDraft, setRxResumeBaseResumeIdDraft] = useState<string | null>(null)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [statusesToClear, setStatusesToClear] = useState<JobStatus[]>(['discovered'])
|
||||
@@ -69,6 +71,7 @@ export const SettingsPage: React.FC = () => {
|
||||
setJobspyCountryIndeedDraft(data.overrideJobspyCountryIndeed)
|
||||
setJobspySitesDraft(data.overrideJobspySites)
|
||||
setJobspyLinkedinFetchDescriptionDraft(data.overrideJobspyLinkedinFetchDescription)
|
||||
setRxResumeBaseResumeIdDraft(data.rxResumeBaseResumeId)
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : "Failed to load settings"
|
||||
@@ -163,7 +166,8 @@ export const SettingsPage: React.FC = () => {
|
||||
jobspyHoursOldDraft !== (overrideJobspyHoursOld ?? null) ||
|
||||
jobspyCountryIndeedDraft !== (overrideJobspyCountryIndeed ?? null) ||
|
||||
JSON.stringify((jobspySitesDraft ?? []).slice().sort()) !== JSON.stringify((overrideJobspySites ?? []).slice().sort()) ||
|
||||
jobspyLinkedinFetchDescriptionDraft !== (overrideJobspyLinkedinFetchDescription ?? null)
|
||||
jobspyLinkedinFetchDescriptionDraft !== (overrideJobspyLinkedinFetchDescription ?? null) ||
|
||||
rxResumeBaseResumeIdDraft !== (settings.rxResumeBaseResumeId ?? null)
|
||||
)
|
||||
}, [
|
||||
settings,
|
||||
@@ -198,6 +202,7 @@ export const SettingsPage: React.FC = () => {
|
||||
overrideJobspyCountryIndeed,
|
||||
overrideJobspySites,
|
||||
overrideJobspyLinkedinFetchDescription,
|
||||
rxResumeBaseResumeIdDraft,
|
||||
])
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -222,6 +227,7 @@ export const SettingsPage: React.FC = () => {
|
||||
const jobspyCountryIndeedOverride = jobspyCountryIndeedDraft === defaultJobspyCountryIndeed ? null : jobspyCountryIndeedDraft
|
||||
const jobspySitesOverride = arraysEqual((jobspySitesDraft ?? []).slice().sort(), (defaultJobspySites ?? []).slice().sort()) ? null : jobspySitesDraft
|
||||
const jobspyLinkedinFetchDescriptionOverride = jobspyLinkedinFetchDescriptionDraft === defaultJobspyLinkedinFetchDescription ? null : jobspyLinkedinFetchDescriptionDraft
|
||||
const rxResumeBaseResumeIdOverride = rxResumeBaseResumeIdDraft
|
||||
const updated = await api.updateSettings({
|
||||
model: trimmed.length > 0 ? trimmed : null,
|
||||
modelScorer: trimmedScorer.length > 0 ? trimmedScorer : null,
|
||||
@@ -239,6 +245,7 @@ export const SettingsPage: React.FC = () => {
|
||||
jobspyCountryIndeed: jobspyCountryIndeedOverride,
|
||||
jobspySites: jobspySitesOverride,
|
||||
jobspyLinkedinFetchDescription: jobspyLinkedinFetchDescriptionOverride,
|
||||
rxResumeBaseResumeId: rxResumeBaseResumeIdOverride,
|
||||
})
|
||||
setSettings(updated)
|
||||
setModelDraft(updated.overrideModel ?? "")
|
||||
@@ -257,6 +264,7 @@ export const SettingsPage: React.FC = () => {
|
||||
setJobspyCountryIndeedDraft(updated.overrideJobspyCountryIndeed)
|
||||
setJobspySitesDraft(updated.overrideJobspySites)
|
||||
setJobspyLinkedinFetchDescriptionDraft(updated.overrideJobspyLinkedinFetchDescription)
|
||||
setRxResumeBaseResumeIdDraft(updated.rxResumeBaseResumeId)
|
||||
toast.success("Settings saved")
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to save settings"
|
||||
@@ -340,6 +348,7 @@ export const SettingsPage: React.FC = () => {
|
||||
jobspyCountryIndeed: null,
|
||||
jobspySites: null,
|
||||
jobspyLinkedinFetchDescription: null,
|
||||
rxResumeBaseResumeId: null,
|
||||
})
|
||||
setSettings(updated)
|
||||
setModelDraft("")
|
||||
@@ -358,6 +367,7 @@ export const SettingsPage: React.FC = () => {
|
||||
setJobspyCountryIndeedDraft(null)
|
||||
setJobspySitesDraft(null)
|
||||
setJobspyLinkedinFetchDescriptionDraft(null)
|
||||
setRxResumeBaseResumeIdDraft(null)
|
||||
toast.success("Reset to default")
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to reset settings"
|
||||
@@ -471,6 +481,13 @@ export const SettingsPage: React.FC = () => {
|
||||
isLoading={isLoading}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
<ReactiveResumeSection
|
||||
rxResumeBaseResumeIdDraft={rxResumeBaseResumeIdDraft}
|
||||
setRxResumeBaseResumeIdDraft={setRxResumeBaseResumeIdDraft}
|
||||
hasRxResumeApiKey={settings?.hasRxResumeApiKey ?? false}
|
||||
isLoading={isLoading}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
<DangerZoneSection
|
||||
statusesToClear={statusesToClear}
|
||||
toggleStatusToClear={toggleStatusToClear}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { AlertCircle, CheckCircle2, RefreshCw } from "lucide-react"
|
||||
|
||||
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import * as api from "../../../api"
|
||||
|
||||
type ReactiveResumeSectionProps = {
|
||||
rxResumeBaseResumeIdDraft: string | null
|
||||
setRxResumeBaseResumeIdDraft: (value: string | null) => void
|
||||
hasRxResumeApiKey: boolean
|
||||
isLoading: boolean
|
||||
isSaving: boolean
|
||||
}
|
||||
|
||||
export const ReactiveResumeSection: React.FC<ReactiveResumeSectionProps> = ({
|
||||
rxResumeBaseResumeIdDraft,
|
||||
setRxResumeBaseResumeIdDraft,
|
||||
hasRxResumeApiKey,
|
||||
isLoading,
|
||||
isSaving,
|
||||
}) => {
|
||||
const [resumes, setResumes] = useState<{ id: string; name: string }[]>([])
|
||||
const [isFetchingResumes, setIsFetchingResumes] = useState(false)
|
||||
const [fetchError, setFetchError] = useState<string | null>(null)
|
||||
|
||||
const fetchResumes = async () => {
|
||||
if (!hasRxResumeApiKey) return
|
||||
|
||||
setIsFetchingResumes(true)
|
||||
setFetchError(null)
|
||||
try {
|
||||
const data = await api.getRxResumes()
|
||||
setResumes(data)
|
||||
} catch (error) {
|
||||
setFetchError(error instanceof Error ? error.message : "Failed to fetch resumes")
|
||||
} finally {
|
||||
setIsFetchingResumes(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (hasRxResumeApiKey) {
|
||||
fetchResumes()
|
||||
}
|
||||
}, [hasRxResumeApiKey])
|
||||
|
||||
return (
|
||||
<AccordionItem value="reactive-resume" className="border rounded-lg px-4">
|
||||
<AccordionTrigger className="hover:no-underline py-4">
|
||||
<span className="text-base font-semibold">Reactive Resume</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4">
|
||||
<div className="space-y-4">
|
||||
{!hasRxResumeApiKey ? (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>API Key Missing</AlertTitle>
|
||||
<AlertDescription>
|
||||
<code>RXRESUME_API_KEY</code> is not configured in the server environment. Please add it to your <code>.env</code> file.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Alert className="bg-green-50 border-green-200 dark:bg-green-900/10 dark:border-green-900/20">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<AlertTitle className="text-green-800 dark:text-green-300">API Key Configured</AlertTitle>
|
||||
<AlertDescription className="text-green-700 dark:text-green-400">
|
||||
Reactive Resume API integration is active.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-medium">Base Resume</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={fetchResumes}
|
||||
disabled={isFetchingResumes || isLoading || isSaving}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 mr-1 ${isFetchingResumes ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={rxResumeBaseResumeIdDraft || "none"}
|
||||
onValueChange={(value: string) => setRxResumeBaseResumeIdDraft(value === "none" ? null : value)}
|
||||
disabled={isLoading || isSaving || isFetchingResumes}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a base resume..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None (Fallback to local base.json)</SelectItem>
|
||||
{resumes.map((resume) => (
|
||||
<SelectItem key={resume.id} value={resume.id}>
|
||||
{resume.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{fetchError && (
|
||||
<div className="text-xs text-destructive mt-1">
|
||||
{fetchError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
The selected resume will be used as a template for tailoring. A temporary copy will be created during generation and deleted afterwards.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user