Spaces:
Running
Running
Download components/ScoringControlForm.tsx from zimejin/Job-Scorer: direct link, hf CLI and curl.
- Browser
- Download file 4.38 kB
-
https://huggingface.co/spaces/zimejin/Job-Scorer/resolve/main/components/ScoringControlForm.tsx
- Command line
-
hf download hf://spaces/zimejin/Job-Scorer/components/ScoringControlForm.tsx
-
curl -L -o ScoringControlForm.tsx https://huggingface.co/spaces/zimejin/Job-Scorer/resolve/main/components/ScoringControlForm.tsx
4.38 kB
| "use client"; | |
| import { | |
| ALL_DEALBREAKERS, | |
| dealbreakerLabel, | |
| type Dealbreaker, | |
| type ScoringWeights, | |
| } from "@/lib/job-search-preferences"; | |
| import { MATCH_SCORE_WEIGHTS } from "@/lib/score-display"; | |
| export type ScoringControlValues = { | |
| scoringWeights: ScoringWeights; | |
| dealbreakers: Dealbreaker[]; | |
| }; | |
| export function scoringControlValuesFromPrefs(opts?: { | |
| scoringWeights?: ScoringWeights; | |
| dealbreakers?: Dealbreaker[]; | |
| }): ScoringControlValues { | |
| return { | |
| scoringWeights: { | |
| role_fit: opts?.scoringWeights?.role_fit ?? MATCH_SCORE_WEIGHTS.role_fit * 10, | |
| global_remote: opts?.scoringWeights?.global_remote ?? MATCH_SCORE_WEIGHTS.global_remote * 10, | |
| culture_async: opts?.scoringWeights?.culture_async ?? MATCH_SCORE_WEIGHTS.culture_async * 10, | |
| contractor_friendly: | |
| opts?.scoringWeights?.contractor_friendly ?? MATCH_SCORE_WEIGHTS.contractor_friendly * 10, | |
| }, | |
| dealbreakers: opts?.dealbreakers ?? [], | |
| }; | |
| } | |
| const WEIGHT_FIELDS: Array<{ key: keyof ScoringWeights; label: string }> = [ | |
| { key: "role_fit", label: "Role & skill fit" }, | |
| { key: "global_remote", label: "Worldwide remote" }, | |
| { key: "culture_async", label: "Async culture" }, | |
| { key: "contractor_friendly", label: "Contractor-friendly" }, | |
| ]; | |
| type Props = { | |
| values: ScoringControlValues; | |
| onChange: (next: ScoringControlValues) => void; | |
| disabled?: boolean; | |
| }; | |
| export function ScoringControlForm({ values, onChange, disabled = false }: Props) { | |
| const setWeight = (key: keyof ScoringWeights, raw: string) => { | |
| const n = Number(raw); | |
| onChange({ | |
| ...values, | |
| scoringWeights: { | |
| ...values.scoringWeights, | |
| [key]: Number.isFinite(n) ? Math.max(0, Math.min(10, n)) : 0, | |
| }, | |
| }); | |
| }; | |
| const toggleDealbreaker = (d: Dealbreaker) => { | |
| const has = values.dealbreakers.includes(d); | |
| onChange({ | |
| ...values, | |
| dealbreakers: has ? | |
| values.dealbreakers.filter((x) => x !== d) | |
| : [...values.dealbreakers, d], | |
| }); | |
| }; | |
| return ( | |
| <> | |
| <div className="app-shell__section" style={{ marginBottom: 12 }}> | |
| <span className="app-shell__section-label">What should weigh most in the match %</span> | |
| <p className="app-shell__field-hint" style={{ margin: "4px 0 8px", fontSize: 12 }}> | |
| Defaults favour role fit and worldwide remote. Values are relative. They do not have to | |
| add to 10. | |
| </p> | |
| {WEIGHT_FIELDS.map(({ key, label }) => ( | |
| <label | |
| key={key} | |
| style={{ | |
| display: "grid", | |
| gridTemplateColumns: "1fr auto", | |
| gap: 8, | |
| alignItems: "center", | |
| marginBottom: 6, | |
| fontSize: 12, | |
| }} | |
| > | |
| <span> | |
| {label} | |
| <input | |
| type="range" | |
| min={0} | |
| max={10} | |
| step={0.5} | |
| disabled={disabled} | |
| value={values.scoringWeights[key] ?? 0} | |
| onChange={(e) => setWeight(key, e.target.value)} | |
| style={{ width: "100%", marginTop: 4 }} | |
| /> | |
| </span> | |
| <span style={{ fontVariantNumeric: "tabular-nums", minWidth: "3ch" }}> | |
| {Number(values.scoringWeights[key] ?? 0).toFixed(1)} | |
| </span> | |
| </label> | |
| ))} | |
| </div> | |
| <div className="app-shell__section" style={{ marginBottom: 12 }}> | |
| <span className="app-shell__section-label">Dealbreakers</span> | |
| <p className="app-shell__field-hint" style={{ margin: "4px 0 8px", fontSize: 12 }}> | |
| Hard rules. A listing that breaks any of these is scored Skip, with an explicit callout. | |
| </p> | |
| <div style={{ display: "flex", flexDirection: "column", gap: 6 }}> | |
| {ALL_DEALBREAKERS.map((d) => ( | |
| <label | |
| key={d} | |
| style={{ display: "flex", gap: 8, alignItems: "flex-start", fontSize: 12 }} | |
| > | |
| <input | |
| type="checkbox" | |
| disabled={disabled} | |
| checked={values.dealbreakers.includes(d)} | |
| onChange={() => toggleDealbreaker(d)} | |
| style={{ marginTop: 2 }} | |
| /> | |
| <span>{dealbreakerLabel(d)}</span> | |
| </label> | |
| ))} | |
| </div> | |
| </div> | |
| </> | |
| ); | |
| } | |