onboarding UI ensures that we have a resume base id when we're in the app
This commit is contained in:
parent
9dfb862649
commit
a268bfdd59
@ -247,6 +247,7 @@ export async function updateSettings(update: {
|
|||||||
ukvisajobsEmail?: string | null
|
ukvisajobsEmail?: string | null
|
||||||
ukvisajobsPassword?: string | null
|
ukvisajobsPassword?: string | null
|
||||||
webhookSecret?: string | null
|
webhookSecret?: string | null
|
||||||
|
rxresumeBaseResumeId?: string | null
|
||||||
}): Promise<AppSettings> {
|
}): Promise<AppSettings> {
|
||||||
return fetchApi<AppSettings>('/settings', {
|
return fetchApi<AppSettings>('/settings', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import * as api from "@client/api"
|
|||||||
import { useSettings } from "@client/hooks/useSettings"
|
import { useSettings } from "@client/hooks/useSettings"
|
||||||
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"
|
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"
|
||||||
import { formatSecretHint } from "@client/pages/settings/utils"
|
import { formatSecretHint } from "@client/pages/settings/utils"
|
||||||
|
import { BaseResumeSelection } from "@client/pages/settings/components/BaseResumeSelection"
|
||||||
import type { ValidationResult } from "@shared/types"
|
import type { ValidationResult } from "@shared/types"
|
||||||
|
|
||||||
type ValidationState = ValidationResult & { checked: boolean }
|
type ValidationState = ValidationResult & { checked: boolean }
|
||||||
@ -21,6 +22,7 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
const [isSavingEnv, setIsSavingEnv] = useState(false)
|
const [isSavingEnv, setIsSavingEnv] = useState(false)
|
||||||
const [isValidatingOpenrouter, setIsValidatingOpenrouter] = useState(false)
|
const [isValidatingOpenrouter, setIsValidatingOpenrouter] = useState(false)
|
||||||
const [isValidatingRxresume, setIsValidatingRxresume] = useState(false)
|
const [isValidatingRxresume, setIsValidatingRxresume] = useState(false)
|
||||||
|
const [isValidatingBaseResume, setIsValidatingBaseResume] = useState(false)
|
||||||
const [openrouterValidation, setOpenrouterValidation] = useState<ValidationState>({
|
const [openrouterValidation, setOpenrouterValidation] = useState<ValidationState>({
|
||||||
valid: false,
|
valid: false,
|
||||||
message: null,
|
message: null,
|
||||||
@ -31,11 +33,17 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
message: null,
|
message: null,
|
||||||
checked: false,
|
checked: false,
|
||||||
})
|
})
|
||||||
|
const [baseResumeValidation, setBaseResumeValidation] = useState<ValidationState>({
|
||||||
|
valid: false,
|
||||||
|
message: null,
|
||||||
|
checked: false,
|
||||||
|
})
|
||||||
const [currentStep, setCurrentStep] = useState<string | null>(null)
|
const [currentStep, setCurrentStep] = useState<string | null>(null)
|
||||||
|
|
||||||
const [openrouterApiKey, setOpenrouterApiKey] = useState("")
|
const [openrouterApiKey, setOpenrouterApiKey] = useState("")
|
||||||
const [rxresumeEmail, setRxresumeEmail] = useState("")
|
const [rxresumeEmail, setRxresumeEmail] = useState("")
|
||||||
const [rxresumePassword, setRxresumePassword] = useState("")
|
const [rxresumePassword, setRxresumePassword] = useState("")
|
||||||
|
const [rxresumeBaseResumeId, setRxresumeBaseResumeId] = useState<string | null>(null)
|
||||||
|
|
||||||
const validateOpenrouter = useCallback(async (apiKey?: string) => {
|
const validateOpenrouter = useCallback(async (apiKey?: string) => {
|
||||||
setIsValidatingOpenrouter(true)
|
setIsValidatingOpenrouter(true)
|
||||||
@ -69,11 +77,27 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const validateBaseResume = useCallback(async () => {
|
||||||
|
setIsValidatingBaseResume(true)
|
||||||
|
try {
|
||||||
|
const result = await api.validateResumeConfig()
|
||||||
|
setBaseResumeValidation({ ...result, checked: true })
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Base resume validation failed"
|
||||||
|
const result = { valid: false, message }
|
||||||
|
setBaseResumeValidation({ ...result, checked: true })
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
setIsValidatingBaseResume(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const hasOpenrouterKey = Boolean(settings?.openrouterApiKeyHint)
|
const hasOpenrouterKey = Boolean(settings?.openrouterApiKeyHint)
|
||||||
const hasRxresumeEmail = Boolean(settings?.rxresumeEmail?.trim())
|
const hasRxresumeEmail = Boolean(settings?.rxresumeEmail?.trim())
|
||||||
const hasRxresumePassword = Boolean(settings?.rxresumePasswordHint)
|
const hasRxresumePassword = Boolean(settings?.rxresumePasswordHint)
|
||||||
const shouldOpen = Boolean(settings && !settingsLoading)
|
const shouldOpen = Boolean(settings && !settingsLoading)
|
||||||
&& !(openrouterValidation.valid && rxresumeValidation.valid)
|
&& !(openrouterValidation.valid && rxresumeValidation.valid && baseResumeValidation.valid)
|
||||||
|
|
||||||
const openrouterCurrent = settings?.openrouterApiKeyHint
|
const openrouterCurrent = settings?.openrouterApiKeyHint
|
||||||
? formatSecretHint(settings.openrouterApiKeyHint)
|
? formatSecretHint(settings.openrouterApiKeyHint)
|
||||||
@ -85,6 +109,12 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
? formatSecretHint(settings.rxresumePasswordHint)
|
? formatSecretHint(settings.rxresumePasswordHint)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings) {
|
||||||
|
setRxresumeBaseResumeId(settings.rxresumeBaseResumeId || null)
|
||||||
|
}
|
||||||
|
}, [settings])
|
||||||
|
|
||||||
const steps = useMemo(
|
const steps = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -92,15 +122,24 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
label: "Connect AI",
|
label: "Connect AI",
|
||||||
subtitle: "OpenRouter key",
|
subtitle: "OpenRouter key",
|
||||||
complete: openrouterValidation.valid,
|
complete: openrouterValidation.valid,
|
||||||
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "rxresume",
|
id: "rxresume",
|
||||||
label: "PDF Export",
|
label: "Connect Reactive Resume",
|
||||||
subtitle: "RxResume login",
|
subtitle: "Reactive Resume login",
|
||||||
complete: rxresumeValidation.valid,
|
complete: rxresumeValidation.valid,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "baseresume",
|
||||||
|
label: "Select Template Resume",
|
||||||
|
subtitle: "Template selection",
|
||||||
|
complete: baseResumeValidation.valid,
|
||||||
|
disabled: !rxresumeValidation.valid,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[openrouterValidation.valid, rxresumeValidation.valid]
|
[openrouterValidation.valid, rxresumeValidation.valid, baseResumeValidation.valid]
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultStep = steps.find((step) => !step.complete)?.id ?? steps[0]?.id
|
const defaultStep = steps.find((step) => !step.complete)?.id ?? steps[0]?.id
|
||||||
@ -117,6 +156,7 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
const results = await Promise.allSettled([
|
const results = await Promise.allSettled([
|
||||||
validateOpenrouter(),
|
validateOpenrouter(),
|
||||||
validateRxresume(),
|
validateRxresume(),
|
||||||
|
validateBaseResume(),
|
||||||
])
|
])
|
||||||
|
|
||||||
const failed = results.find((result) => result.status === "rejected")
|
const failed = results.find((result) => result.status === "rejected")
|
||||||
@ -219,16 +259,45 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSaveBaseResume = async (): Promise<boolean> => {
|
||||||
|
if (!rxresumeBaseResumeId) {
|
||||||
|
toast.info("Select a base resume to continue")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSavingEnv(true)
|
||||||
|
await api.updateSettings({ rxresumeBaseResumeId: rxresumeBaseResumeId })
|
||||||
|
const validation = await validateBaseResume()
|
||||||
|
if (!validation.valid) {
|
||||||
|
toast.error(validation.message || "Base resume validation failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshSettings()
|
||||||
|
toast.success("Base resume set")
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Failed to save base resume"
|
||||||
|
toast.error(message)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
setIsSavingEnv(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedStepIndex = currentStep ? steps.findIndex((step) => step.id === currentStep) : 0
|
const resolvedStepIndex = currentStep ? steps.findIndex((step) => step.id === currentStep) : 0
|
||||||
const stepIndex = resolvedStepIndex >= 0 ? resolvedStepIndex : 0
|
const stepIndex = resolvedStepIndex >= 0 ? resolvedStepIndex : 0
|
||||||
const completedSteps = steps.filter((step) => step.complete).length
|
const completedSteps = steps.filter((step) => step.complete).length
|
||||||
const progressValue = steps.length > 0 ? Math.round((completedSteps / steps.length) * 100) : 0
|
const progressValue = steps.length > 0 ? Math.round((completedSteps / steps.length) * 100) : 0
|
||||||
const isBusy = isSavingEnv || settingsLoading || isValidatingOpenrouter || isValidatingRxresume
|
const isBusy = isSavingEnv || settingsLoading || isValidatingOpenrouter || isValidatingRxresume || isValidatingBaseResume
|
||||||
const canGoBack = stepIndex > 0
|
const canGoBack = stepIndex > 0
|
||||||
const primaryLabel = currentStep === "openrouter"
|
const primaryLabel = currentStep === "openrouter"
|
||||||
? (openrouterValidation.valid ? "Revalidate" : "Validate")
|
? (openrouterValidation.valid ? "Revalidate" : "Validate")
|
||||||
: currentStep === "rxresume"
|
: currentStep === "rxresume"
|
||||||
? (rxresumeValidation.valid ? "Revalidate" : "Validate")
|
? (rxresumeValidation.valid ? "Revalidate" : "Validate")
|
||||||
|
: currentStep === "baseresume"
|
||||||
|
? (baseResumeValidation.valid ? "Revalidate" : "Validate")
|
||||||
: "Validate"
|
: "Validate"
|
||||||
|
|
||||||
const handlePrimaryAction = async () => {
|
const handlePrimaryAction = async () => {
|
||||||
@ -241,6 +310,10 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
await handleSaveRxresume()
|
await handleSaveRxresume()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (currentStep === "baseresume") {
|
||||||
|
await handleSaveBaseResume()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
@ -265,7 +338,7 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<Tabs value={currentStep} onValueChange={setCurrentStep}>
|
<Tabs value={currentStep} onValueChange={setCurrentStep}>
|
||||||
<TabsList className="grid h-auto w-full grid-cols-1 gap-2 border-b border-border/60 bg-transparent p-0 text-left sm:grid-cols-2">
|
<TabsList className="grid h-auto w-full grid-cols-1 gap-2 border-b border-border/60 bg-transparent p-0 text-left sm:grid-cols-3">
|
||||||
{steps.map((step, index) => {
|
{steps.map((step, index) => {
|
||||||
const isActive = step.id === currentStep
|
const isActive = step.id === currentStep
|
||||||
const isComplete = step.complete
|
const isComplete = step.complete
|
||||||
@ -273,13 +346,17 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
key={step.id}
|
key={step.id}
|
||||||
className="w-full [&>[data-slot=field]]:border-0 [&>[data-slot=field]]:p-0 [&>[data-slot=field]]:rounded-none"
|
className={cn(
|
||||||
|
"w-full [&>[data-slot=field]]:border-0 [&>[data-slot=field]]:p-0 [&>[data-slot=field]]:rounded-none",
|
||||||
|
step.disabled && "opacity-50 cursor-not-allowed"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value={step.id}
|
value={step.id}
|
||||||
|
disabled={step.disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full rounded-none border-b-2 border-transparent px-3 py-4 text-left shadow-none",
|
"w-full rounded-md hover:bg-muted/60 border-b-2 border-transparent px-3 py-4 text-left shadow-none",
|
||||||
isActive ? "border-primary bg-muted/60 text-foreground" : "text-muted-foreground"
|
isActive ? "border-primary !bg-muted/60 text-foreground" : "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Field orientation="horizontal" className="items-start">
|
<Field orientation="horizontal" className="items-start">
|
||||||
@ -356,6 +433,21 @@ export const OnboardingGate: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="baseresume" className="space-y-4 pt-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold">Select your template resume</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Choose the resume you want to use as a template.
|
||||||
|
The selected resume will be used as a template for tailoring.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseResumeSelection
|
||||||
|
value={rxresumeBaseResumeId}
|
||||||
|
onValueChange={setRxresumeBaseResumeId}
|
||||||
|
hasRxResumeAccess={rxresumeValidation.valid}
|
||||||
|
disabled={isSavingEnv}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
import React, { useEffect, useState } from "react"
|
||||||
|
import { RefreshCw } from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
import * as api from "@client/api"
|
||||||
|
|
||||||
|
type BaseResumeSelectionProps = {
|
||||||
|
value: string | null
|
||||||
|
onValueChange: (value: string | null) => void
|
||||||
|
hasRxResumeAccess: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseResumeSelection: React.FC<BaseResumeSelectionProps> = ({
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
hasRxResumeAccess,
|
||||||
|
disabled = false,
|
||||||
|
isLoading = false,
|
||||||
|
}) => {
|
||||||
|
const [resumes, setResumes] = useState<{ id: string; name: string }[]>([])
|
||||||
|
const [isFetchingResumes, setIsFetchingResumes] = useState(false)
|
||||||
|
const [fetchError, setFetchError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const fetchResumes = async () => {
|
||||||
|
if (!hasRxResumeAccess) return
|
||||||
|
|
||||||
|
setIsFetchingResumes(true)
|
||||||
|
setFetchError(null)
|
||||||
|
try {
|
||||||
|
const data = await api.getRxResumes()
|
||||||
|
setResumes(data)
|
||||||
|
|
||||||
|
// Preselect if only one option is available and no value is currently set
|
||||||
|
if (data.length === 1 && !value) {
|
||||||
|
onValueChange(data[0].id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setFetchError(error instanceof Error ? error.message : "Failed to fetch resumes")
|
||||||
|
} finally {
|
||||||
|
setIsFetchingResumes(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasRxResumeAccess) {
|
||||||
|
fetchResumes()
|
||||||
|
}
|
||||||
|
}, [hasRxResumeAccess])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">Template Resume</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={fetchResumes}
|
||||||
|
disabled={isFetchingResumes || isLoading || disabled}
|
||||||
|
className="h-8 px-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-3 w-3 mr-1 ${isFetchingResumes ? 'animate-spin' : ''}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={value || ""}
|
||||||
|
onValueChange={(val: string) => onValueChange(val || null)}
|
||||||
|
disabled={disabled || isLoading || isFetchingResumes}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={resumes.length > 0 ? "Select a template resume..." : "No resumes found"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{resumes.map((resume) => (
|
||||||
|
<SelectItem key={resume.id} value={resume.id}>
|
||||||
|
{resume.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{resumes.length === 0 && !isFetchingResumes && !fetchError && (
|
||||||
|
<div className="text-xs text-amber-600 dark:text-amber-400 mt-2">
|
||||||
|
No resumes found in your account. Please create a resume on the{" "}
|
||||||
|
<a
|
||||||
|
href="https://rxresu.me"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="font-semibold underline underline-offset-2"
|
||||||
|
>
|
||||||
|
Reactive Resume website
|
||||||
|
</a>{" "}
|
||||||
|
first.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fetchError && (
|
||||||
|
<div className="text-xs text-destructive mt-1">
|
||||||
|
{fetchError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,19 +1,17 @@
|
|||||||
import React, { useEffect, useState } from "react"
|
import React, { useEffect, useState } from "react"
|
||||||
import { Controller, useFormContext } from "react-hook-form"
|
import { Controller, useFormContext } from "react-hook-form"
|
||||||
import { AlertCircle, CheckCircle2, RefreshCw } from "lucide-react"
|
import { AlertCircle, CheckCircle2 } from "lucide-react"
|
||||||
|
|
||||||
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { clampInt } from "@/lib/utils"
|
import { clampInt } from "@/lib/utils"
|
||||||
import type { ResumeProjectCatalogItem } from "@shared/types"
|
import type { ResumeProjectCatalogItem } from "@shared/types"
|
||||||
import { UpdateSettingsInput } from "@shared/settings-schema"
|
import { UpdateSettingsInput } from "@shared/settings-schema"
|
||||||
import * as api from "../../../api"
|
import { BaseResumeSelection } from "./BaseResumeSelection"
|
||||||
|
|
||||||
type ReactiveResumeSectionProps = {
|
type ReactiveResumeSectionProps = {
|
||||||
rxResumeBaseResumeIdDraft: string | null
|
rxResumeBaseResumeIdDraft: string | null
|
||||||
@ -40,30 +38,6 @@ export const ReactiveResumeSection: React.FC<ReactiveResumeSectionProps> = ({
|
|||||||
isSaving,
|
isSaving,
|
||||||
}) => {
|
}) => {
|
||||||
const { control, formState: { errors } } = useFormContext<UpdateSettingsInput>()
|
const { control, formState: { errors } } = useFormContext<UpdateSettingsInput>()
|
||||||
const [resumes, setResumes] = useState<{ id: string; name: string }[]>([])
|
|
||||||
const [isFetchingResumes, setIsFetchingResumes] = useState(false)
|
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const fetchResumes = async () => {
|
|
||||||
if (!hasRxResumeAccess) 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 (hasRxResumeAccess) {
|
|
||||||
fetchResumes()
|
|
||||||
}
|
|
||||||
}, [hasRxResumeAccess])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccordionItem value="reactive-resume" className="border rounded-lg px-4">
|
<AccordionItem value="reactive-resume" className="border rounded-lg px-4">
|
||||||
@ -90,49 +64,12 @@ export const ReactiveResumeSection: React.FC<ReactiveResumeSectionProps> = ({
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<BaseResumeSelection
|
||||||
<div className="flex items-center justify-between">
|
value={rxResumeBaseResumeIdDraft}
|
||||||
<div className="text-sm font-medium">Base Resume</div>
|
onValueChange={setRxResumeBaseResumeIdDraft}
|
||||||
<Button
|
hasRxResumeAccess={hasRxResumeAccess}
|
||||||
variant="ghost"
|
disabled={isLoading || isSaving}
|
||||||
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 (No profile data will be loaded)</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.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user