diff --git a/orchestrator/src/client/pages/SettingsPage.tsx b/orchestrator/src/client/pages/SettingsPage.tsx index a993ffe..dc983f2 100644 --- a/orchestrator/src/client/pages/SettingsPage.tsx +++ b/orchestrator/src/client/pages/SettingsPage.tsx @@ -3,70 +3,24 @@ */ import React, { useEffect, useMemo, useState } from "react" -import { AlertTriangle, Settings, Trash2 } from "lucide-react" +import { Settings } from "lucide-react" import { toast } from "sonner" import { PageHeader } from "../components/layout" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" +import { Accordion } from "@/components/ui/accordion" import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Checkbox } from "@/components/ui/checkbox" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import type { AppSettings, JobStatus, ResumeProjectsSettings } from "../../shared/types" import * as api from "../api" - -/** All available job statuses for clearing */ -const ALL_JOB_STATUSES: JobStatus[] = ['discovered', 'processing', 'ready', 'applied', 'skipped', 'expired'] - -/** Status descriptions for UI */ -const STATUS_DESCRIPTIONS: Record = { - discovered: 'Crawled but not processed', - processing: 'Currently generating resume', - ready: 'PDF generated, waiting for user to apply', - applied: 'User marked as applied', - skipped: 'User skipped this job', - expired: 'Deadline passed', -} - -function arraysEqual(a: string[], b: string[]) { - if (a.length !== b.length) return false - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false - } - return true -} - -function resumeProjectsEqual(a: ResumeProjectsSettings, b: ResumeProjectsSettings) { - return ( - a.maxProjects === b.maxProjects && - arraysEqual(a.lockedProjectIds, b.lockedProjectIds) && - arraysEqual(a.aiSelectableProjectIds, b.aiSelectableProjectIds) - ) -} - -function clampInt(value: number, min: number, max: number) { - const int = Math.floor(value) - if (Number.isNaN(int)) return min - return Math.min(max, Math.max(min, int)) -} +import { arraysEqual, resumeProjectsEqual } from "./settings/utils" +import { DangerZoneSection } from "./settings/components/DangerZoneSection" +import { GradcrackerSection } from "./settings/components/GradcrackerSection" +import { JobCompleteWebhookSection } from "./settings/components/JobCompleteWebhookSection" +import { JobspySection } from "./settings/components/JobspySection" +import { ModelSettingsSection } from "./settings/components/ModelSettingsSection" +import { PipelineWebhookSection } from "./settings/components/PipelineWebhookSection" +import { ResumeProjectsSection } from "./settings/components/ResumeProjectsSection" +import { SearchTermsSection } from "./settings/components/SearchTermsSection" +import { UkvisajobsSection } from "./settings/components/UkvisajobsSection" export const SettingsPage: React.FC = () => { const [settings, setSettings] = useState(null) @@ -332,7 +286,7 @@ export const SettingsPage: React.FC = () => { setIsSaving(true) let totalDeleted = 0 const results: string[] = [] - + for (const status of statusesToClear) { const result = await api.deleteJobsByStatus(status) totalDeleted += result.count @@ -340,14 +294,14 @@ export const SettingsPage: React.FC = () => { results.push(`${result.count} ${status}`) } } - + if (totalDeleted > 0) { - toast.success("Jobs cleared", { - description: `Deleted ${totalDeleted} jobs: ${results.join(', ')}` + toast.success("Jobs cleared", { + description: `Deleted ${totalDeleted} jobs: ${results.join(', ')}`, }) } else { - toast.info("No jobs found", { - description: `No jobs with selected statuses found` + toast.info("No jobs found", { + description: `No jobs with selected statuses found`, }) } } catch (error) { @@ -359,8 +313,8 @@ export const SettingsPage: React.FC = () => { } const toggleStatusToClear = (status: JobStatus) => { - setStatusesToClear(prev => - prev.includes(status) + setStatusesToClear(prev => + prev.includes(status) ? prev.filter(s => s !== status) : [...prev, status] ) @@ -422,707 +376,119 @@ export const SettingsPage: React.FC = () => {
- - - Model - - -
-
-
Override model
- setModelDraft(event.target.value)} - placeholder={defaultModel || "openai/gpt-4o-mini"} - disabled={isLoading || isSaving} - /> -
- Leave blank to use the default from server env (`MODEL`). -
-
+ + + + + + + + + + - - -
-
Task-Specific Overrides
- -
-
-
Scoring Model
- setModelScorerDraft(event.target.value)} - placeholder={effectiveModel || "inherit"} - disabled={isLoading || isSaving} - /> -
- Effective: {effectiveModelScorer || effectiveModel} -
-
- -
-
Tailoring Model
- setModelTailoringDraft(event.target.value)} - placeholder={effectiveModel || "inherit"} - disabled={isLoading || isSaving} - /> -
- Effective: {effectiveModelTailoring || effectiveModel} -
-
- -
-
Project Selection Model
- setModelProjectSelectionDraft(event.target.value)} - placeholder={effectiveModel || "inherit"} - disabled={isLoading || isSaving} - /> -
- Effective: {effectiveModelProjectSelection || effectiveModel} -
-
-
-
- - - -
-
-
Global Effective
-
{effectiveModel || "—"}
-
-
-
Default (env)
-
{defaultModel || "—"}
-
-
-
-
-
- - - - Pipeline Webhook - - -
-
-
Pipeline status webhook URL
- setPipelineWebhookUrlDraft(event.target.value)} - placeholder={defaultPipelineWebhookUrl || "https://..."} - disabled={isLoading || isSaving} - /> -
- When set, the server sends a POST on pipeline completion/failure. Leave blank to disable. -
-
- - - -
-
-
Effective
-
{effectivePipelineWebhookUrl || "—"}
-
-
-
Default (env)
-
{defaultPipelineWebhookUrl || "—"}
-
-
-
-
-
- - - - Job Complete Webhook - - -
-
-
Job completion webhook URL
- setJobCompleteWebhookUrlDraft(event.target.value)} - placeholder={defaultJobCompleteWebhookUrl || "https://..."} - disabled={isLoading || isSaving} - /> -
- When set, the server sends a POST when you mark a job as applied (includes the job description). -
-
- - - -
-
-
Effective
-
{effectiveJobCompleteWebhookUrl || "—"}
-
-
-
Default (env)
-
{defaultJobCompleteWebhookUrl || "—"}
-
-
-
-
-
- - - - UKVisaJobs Extractor - - -
-
-
Max jobs to fetch
- { - const value = parseInt(event.target.value, 10) - if (Number.isNaN(value)) { - setUkvisajobsMaxJobsDraft(null) - } else { - setUkvisajobsMaxJobsDraft(Math.min(1000, Math.max(1, value))) - } - }} - disabled={isLoading || isSaving} - /> -
- Maximum number of jobs to fetch from UKVisaJobs per pipeline run. Range: 1-1000. -
-
- - - -
-
-
Effective
-
{effectiveUkvisajobsMaxJobs}
-
-
-
Default
-
{defaultUkvisajobsMaxJobs}
-
-
-
-
-
- - - - Gradcracker Extractor - - -
-
-
Max jobs per search term
- { - const value = parseInt(event.target.value, 10) - if (Number.isNaN(value)) { - setGradcrackerMaxJobsPerTermDraft(null) - } else { - setGradcrackerMaxJobsPerTermDraft(Math.min(1000, Math.max(1, value))) - } - }} - disabled={isLoading || isSaving} - /> -
- Maximum number of jobs to fetch for EACH search term from Gradcracker. Range: 1-1000. -
-
- - - -
-
-
Effective
-
{effectiveGradcrackerMaxJobsPerTerm}
-
-
-
Default
-
{defaultGradcrackerMaxJobsPerTerm}
-
-
-
-
-
- - - - Search Terms - - -
-
-
Global search terms
-