@jsonresume/jobs 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -1
- package/package.json +1 -1
- package/src/tui/AIPanel.js +7 -2
- package/src/tui/App.js +32 -3
- package/src/tui/useAI.js +50 -11
- package/src/tui/useJobs.js +18 -2
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ npx @jsonresume/jobs
|
|
|
102
102
|
|
|
103
103
|
The TUI has three main regions:
|
|
104
104
|
|
|
105
|
-
- **Header** — shows the app title, active search profile name, tab bar (All / Interested / Applied / Maybe / Passed) with live counts, and any active filter pills
|
|
105
|
+
- **Header** — shows the app title, active search profile name, tab bar (All / New / Reviewed / Interested / Applied / Maybe / Passed) with live counts, and any active filter pills
|
|
106
106
|
- **Content area** — job list in list view, or a split-pane layout (40% compact list + 60% job detail) in detail view
|
|
107
107
|
- **Status bar** — context-sensitive keyboard hints, loading/reranking indicators, and toast notifications
|
|
108
108
|
|
|
@@ -206,6 +206,8 @@ In split-pane detail view, the left pane shows a compact list with just score, t
|
|
|
206
206
|
| Tab | Shows |
|
|
207
207
|
|-----|-------|
|
|
208
208
|
| All | All jobs from the current search (excludes passed/dismissed) |
|
|
209
|
+
| New | Jobs with no state and no dossier — completely untouched |
|
|
210
|
+
| Reviewed | Jobs with a dossier but no decision yet |
|
|
209
211
|
| Interested | Jobs you marked with `i` |
|
|
210
212
|
| Applied | Jobs you marked with `x` |
|
|
211
213
|
| Maybe | Jobs you marked with `m` |
|
|
@@ -269,6 +271,228 @@ npx @jsonresume/jobs help # All options
|
|
|
269
271
|
|
|
270
272
|

|
|
271
273
|
|
|
274
|
+
## API Reference
|
|
275
|
+
|
|
276
|
+
All endpoints live at `https://registry.jsonresume.org/api/v1`. Authenticated endpoints require a `Bearer` token in the `Authorization` header.
|
|
277
|
+
|
|
278
|
+
### Authentication
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# Generate an API key (no auth required)
|
|
282
|
+
curl -s -X POST https://registry.jsonresume.org/api/v1/keys \
|
|
283
|
+
-H 'Content-Type: application/json' \
|
|
284
|
+
-d '{"username":"YOUR_GITHUB_USERNAME"}'
|
|
285
|
+
# → { "key": "jr_username_xxxxx", "username": "username" }
|
|
286
|
+
|
|
287
|
+
# Use in all subsequent requests
|
|
288
|
+
export JSONRESUME_API_KEY="jr_username_xxxxx"
|
|
289
|
+
AUTH="Authorization: Bearer $JSONRESUME_API_KEY"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### GET /api/v1/me
|
|
293
|
+
|
|
294
|
+
Returns your resume and profile.
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/me
|
|
298
|
+
# → { "username": "...", "resume": { ... } }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### GET /api/v1/jobs
|
|
302
|
+
|
|
303
|
+
Get jobs matched to your resume via vector similarity.
|
|
304
|
+
|
|
305
|
+
| Param | Type | Default | Description |
|
|
306
|
+
|-------|------|---------|-------------|
|
|
307
|
+
| `top` | int | 20 | Number of results (max 100) |
|
|
308
|
+
| `days` | int | 30 | How far back to search |
|
|
309
|
+
| `remote` | bool | false | Remote jobs only |
|
|
310
|
+
| `min_salary` | int | 0 | Minimum salary in thousands (e.g. `150` = $150k) |
|
|
311
|
+
| `search` | string | | Keyword filter across all fields |
|
|
312
|
+
| `search_id` | uuid | | Use a saved search profile's embedding |
|
|
313
|
+
| `rerank` | bool | auto | LLM reranking (defaults to true when `search_id` is set) |
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Basic search
|
|
317
|
+
curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=50&days=30"
|
|
318
|
+
|
|
319
|
+
# Remote jobs, $150k+, keyword filter
|
|
320
|
+
curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=50&remote=true&min_salary=150&search=react"
|
|
321
|
+
|
|
322
|
+
# Using a search profile with reranking
|
|
323
|
+
curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=50&search_id=UUID&rerank=true"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Response:**
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"jobs": [
|
|
330
|
+
{
|
|
331
|
+
"id": 237247,
|
|
332
|
+
"uuid": "...",
|
|
333
|
+
"title": "Full-Stack Engineer",
|
|
334
|
+
"company": "Acme Corp",
|
|
335
|
+
"location": { "city": "San Francisco", "countryCode": "US" },
|
|
336
|
+
"remote": "Full",
|
|
337
|
+
"salary": "$180k - $220k",
|
|
338
|
+
"salary_usd": 180000,
|
|
339
|
+
"experience": "Senior",
|
|
340
|
+
"type": "Full-time",
|
|
341
|
+
"description": "...",
|
|
342
|
+
"skills": [{ "name": "React" }, { "name": "Node.js" }],
|
|
343
|
+
"url": "https://news.ycombinator.com/item?id=...",
|
|
344
|
+
"posted_at": "2026-03-01T...",
|
|
345
|
+
"similarity": 0.654,
|
|
346
|
+
"rerank_score": 8,
|
|
347
|
+
"combined_score": 0.74,
|
|
348
|
+
"state": "interested",
|
|
349
|
+
"has_dossier": true
|
|
350
|
+
}
|
|
351
|
+
],
|
|
352
|
+
"total": 50
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### POST /api/v1/jobs
|
|
357
|
+
|
|
358
|
+
Match jobs against a provided resume — **no auth required**. Useful for local mode or one-off queries.
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
curl -s -X POST https://registry.jsonresume.org/api/v1/jobs \
|
|
362
|
+
-H 'Content-Type: application/json' \
|
|
363
|
+
-d '{
|
|
364
|
+
"resume": { "basics": { "name": "...", "label": "..." }, "skills": [...] },
|
|
365
|
+
"top": 20,
|
|
366
|
+
"days": 30,
|
|
367
|
+
"remote": true
|
|
368
|
+
}'
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### GET /api/v1/jobs/:id
|
|
372
|
+
|
|
373
|
+
Full details for a single job including raw posting content.
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/jobs/237247
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### PUT /api/v1/jobs/:id
|
|
380
|
+
|
|
381
|
+
Mark a job's state.
|
|
382
|
+
|
|
383
|
+
| Body Param | Type | Description |
|
|
384
|
+
|------------|------|-------------|
|
|
385
|
+
| `state` | string | `interested`, `applied`, `maybe`, `not_interested`, or `dismissed` |
|
|
386
|
+
| `feedback` | string | Optional reason/notes |
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
# Mark as interested
|
|
390
|
+
curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
|
|
391
|
+
https://registry.jsonresume.org/api/v1/jobs/237247 \
|
|
392
|
+
-d '{"state":"interested","feedback":"great remote role, strong tech stack"}'
|
|
393
|
+
|
|
394
|
+
# Mark as applied
|
|
395
|
+
curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
|
|
396
|
+
https://registry.jsonresume.org/api/v1/jobs/237247 \
|
|
397
|
+
-d '{"state":"applied","feedback":"applied via email 2026-03-13"}'
|
|
398
|
+
|
|
399
|
+
# Pass on a job
|
|
400
|
+
curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
|
|
401
|
+
https://registry.jsonresume.org/api/v1/jobs/237247 \
|
|
402
|
+
-d '{"state":"not_interested","feedback":"salary too low"}'
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### GET /api/v1/jobs/:id/dossier
|
|
406
|
+
|
|
407
|
+
Fetch saved research dossier for a job.
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/jobs/237247/dossier
|
|
411
|
+
# → { "content": "# Dossier\n\n## Compensation...", "created_at": "..." }
|
|
412
|
+
# → { "content": null } if no dossier exists
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### PUT /api/v1/jobs/:id/dossier
|
|
416
|
+
|
|
417
|
+
Save or update a research dossier.
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
|
|
421
|
+
https://registry.jsonresume.org/api/v1/jobs/237247/dossier \
|
|
422
|
+
-d '{"content":"# Company Research\n\n..."}'
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### PUT /api/v1/resume
|
|
426
|
+
|
|
427
|
+
Update your resume on the registry.
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
|
|
431
|
+
https://registry.jsonresume.org/api/v1/resume \
|
|
432
|
+
-d @resume.json
|
|
433
|
+
# → { "username": "...", "message": "Resume updated" }
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### GET /api/v1/searches
|
|
437
|
+
|
|
438
|
+
List your saved search profiles.
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
curl -s -H "$AUTH" https://registry.jsonresume.org/api/v1/searches
|
|
442
|
+
# → { "searches": [{ "id": "uuid", "name": "Remote React", "prompt": "...", "filters": [...] }] }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### POST /api/v1/searches
|
|
446
|
+
|
|
447
|
+
Create a new search profile. The server generates a HyDE embedding from your prompt + resume.
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
curl -s -X POST -H "$AUTH" -H 'Content-Type: application/json' \
|
|
451
|
+
https://registry.jsonresume.org/api/v1/searches \
|
|
452
|
+
-d '{"name":"Remote React","prompt":"remote React roles at climate tech startups"}'
|
|
453
|
+
# → { "search": { "id": "uuid", "name": "Remote React", "prompt": "..." } }
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### PUT /api/v1/searches/:id
|
|
457
|
+
|
|
458
|
+
Update a search profile's name or filters.
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
|
|
462
|
+
https://registry.jsonresume.org/api/v1/searches/UUID \
|
|
463
|
+
-d '{"name":"Updated Name","filters":[{"type":"remote"},{"type":"minSalary","value":"150"}]}'
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### DELETE /api/v1/searches/:id
|
|
467
|
+
|
|
468
|
+
Delete a search profile.
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
curl -s -X DELETE -H "$AUTH" https://registry.jsonresume.org/api/v1/searches/UUID
|
|
472
|
+
# → { "ok": true }
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Example: Automated Job Application Workflow
|
|
476
|
+
|
|
477
|
+
Use the API to build your own automation — e.g., have Claude in Chrome review your "maybe" jobs and apply:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# 1. Get all jobs, filter to "maybe" state
|
|
481
|
+
JOBS=$(curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs?top=100" \
|
|
482
|
+
| jq '[.jobs[] | select(.state == "maybe")]')
|
|
483
|
+
|
|
484
|
+
# 2. For each, get the full detail + dossier
|
|
485
|
+
for ID in $(echo $JOBS | jq -r '.[].id'); do
|
|
486
|
+
DETAIL=$(curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs/$ID")
|
|
487
|
+
DOSSIER=$(curl -s -H "$AUTH" "https://registry.jsonresume.org/api/v1/jobs/$ID/dossier")
|
|
488
|
+
echo "$DETAIL" | jq '{title: .title, company: .company, url: .url}'
|
|
489
|
+
# ... use the detail + dossier to apply, then mark as applied
|
|
490
|
+
curl -s -X PUT -H "$AUTH" -H 'Content-Type: application/json' \
|
|
491
|
+
"https://registry.jsonresume.org/api/v1/jobs/$ID" \
|
|
492
|
+
-d '{"state":"applied","feedback":"auto-applied via script"}'
|
|
493
|
+
done
|
|
494
|
+
```
|
|
495
|
+
|
|
272
496
|
## How Ranking Works
|
|
273
497
|
|
|
274
498
|
The system uses a five-stage pipeline to match and rank jobs against your resume.
|
package/package.json
CHANGED
package/src/tui/AIPanel.js
CHANGED
|
@@ -9,6 +9,7 @@ export default function AIPanel({
|
|
|
9
9
|
error,
|
|
10
10
|
onDismiss,
|
|
11
11
|
onExport,
|
|
12
|
+
onRegenerate,
|
|
12
13
|
onMark,
|
|
13
14
|
job,
|
|
14
15
|
isActive,
|
|
@@ -54,6 +55,9 @@ export default function AIPanel({
|
|
|
54
55
|
}, 3000);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
58
|
+
if (input === 'r' && job && onRegenerate && !loading) {
|
|
59
|
+
onRegenerate(job);
|
|
60
|
+
}
|
|
57
61
|
// Mark keys
|
|
58
62
|
if (job && onMark) {
|
|
59
63
|
if (input === 'i') onMark(job.id, 'interested');
|
|
@@ -106,14 +110,15 @@ export default function AIPanel({
|
|
|
106
110
|
: '';
|
|
107
111
|
|
|
108
112
|
const exportHint = text ? ' · e export' : '';
|
|
113
|
+
const regenHint = isCover && !loading && job ? ' · r regenerate' : '';
|
|
109
114
|
const statusLine =
|
|
110
115
|
loading && text
|
|
111
116
|
? 'Streaming… ESC to cancel'
|
|
112
117
|
: loading
|
|
113
118
|
? 'ESC to cancel'
|
|
114
119
|
: scrollHint
|
|
115
|
-
? `${scrollHint}${exportHint} · ESC to dismiss`
|
|
116
|
-
: `ESC to dismiss${exportHint}`;
|
|
120
|
+
? `${scrollHint}${exportHint}${regenHint} · ESC to dismiss`
|
|
121
|
+
: `ESC to dismiss${exportHint}${regenHint}`;
|
|
117
122
|
|
|
118
123
|
return h(
|
|
119
124
|
Box,
|
package/src/tui/App.js
CHANGED
|
@@ -24,9 +24,19 @@ import StatusBar from './StatusBar.js';
|
|
|
24
24
|
import AIPanel from './AIPanel.js';
|
|
25
25
|
import HelpModal from './HelpModal.js';
|
|
26
26
|
|
|
27
|
-
const TABS = [
|
|
27
|
+
const TABS = [
|
|
28
|
+
'all',
|
|
29
|
+
'new',
|
|
30
|
+
'reviewed',
|
|
31
|
+
'interested',
|
|
32
|
+
'applied',
|
|
33
|
+
'maybe',
|
|
34
|
+
'passed',
|
|
35
|
+
];
|
|
28
36
|
const TAB_LABELS = {
|
|
29
37
|
all: 'All',
|
|
38
|
+
new: 'New',
|
|
39
|
+
reviewed: 'Reviewed',
|
|
30
40
|
interested: 'Interested',
|
|
31
41
|
applied: 'Applied',
|
|
32
42
|
maybe: 'Maybe',
|
|
@@ -111,6 +121,7 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
111
121
|
});
|
|
112
122
|
}, [searchesHook.searches]);
|
|
113
123
|
|
|
124
|
+
const ai = useAI(resume);
|
|
114
125
|
const {
|
|
115
126
|
jobs: rawJobs,
|
|
116
127
|
allJobs,
|
|
@@ -119,11 +130,15 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
119
130
|
error,
|
|
120
131
|
markJob,
|
|
121
132
|
forceRefresh,
|
|
122
|
-
} = useJobs(api, activeFilters, tab, activeSearchId);
|
|
123
|
-
const ai = useAI(resume);
|
|
133
|
+
} = useJobs(api, activeFilters, tab, activeSearchId, ai.getDossierStatus);
|
|
124
134
|
const { toast, show: showToast } = useToast();
|
|
125
135
|
const [confirmExit, setConfirmExit] = useState(false);
|
|
126
136
|
|
|
137
|
+
// Seed dossier icons from server-side flags when jobs load
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (allJobs.length) ai.seedDossierFlags(allJobs);
|
|
140
|
+
}, [allJobs, ai]);
|
|
141
|
+
|
|
127
142
|
// Kill claude processes on exit (Ctrl+C)
|
|
128
143
|
useEffect(() => {
|
|
129
144
|
const cleanup = () => ai.cancel();
|
|
@@ -304,6 +319,16 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
304
319
|
|
|
305
320
|
const counts = {
|
|
306
321
|
all: allJobs.length,
|
|
322
|
+
new: allJobs.filter(
|
|
323
|
+
(j) =>
|
|
324
|
+
!j.state &&
|
|
325
|
+
!j.has_dossier &&
|
|
326
|
+
ai.getDossierStatus(j.id) !== 'done' &&
|
|
327
|
+
ai.getDossierStatus(j.id) !== 'generating'
|
|
328
|
+
).length,
|
|
329
|
+
reviewed: allJobs.filter(
|
|
330
|
+
(j) => (j.has_dossier || ai.getDossierStatus(j.id) === 'done') && !j.state
|
|
331
|
+
).length,
|
|
307
332
|
interested: allJobs.filter((j) => j.state === 'interested').length,
|
|
308
333
|
applied: allJobs.filter((j) => j.state === 'applied').length,
|
|
309
334
|
maybe: allJobs.filter((j) => j.state === 'maybe').length,
|
|
@@ -449,6 +474,10 @@ function App({ baseUrl, apiKey, apiClient }) {
|
|
|
449
474
|
if (f) showToast(`Saved ./${f}`, 'export');
|
|
450
475
|
return f;
|
|
451
476
|
},
|
|
477
|
+
onRegenerate: (job) => {
|
|
478
|
+
ai.regenerateDossier(job, api);
|
|
479
|
+
showToast('Regenerating dossier…', 'info');
|
|
480
|
+
},
|
|
452
481
|
isActive: true,
|
|
453
482
|
})
|
|
454
483
|
)
|
package/src/tui/useAI.js
CHANGED
|
@@ -214,7 +214,20 @@ ${jobText}
|
|
|
214
214
|
|
|
215
215
|
Research everything you can and produce a complete dossier covering:
|
|
216
216
|
|
|
217
|
-
### 1.
|
|
217
|
+
### 1. Compensation Context
|
|
218
|
+
- Market rate for this role/level/location
|
|
219
|
+
- How the listed salary compares
|
|
220
|
+
- Negotiation leverage points
|
|
221
|
+
|
|
222
|
+
### 2. AI Engineering Culture
|
|
223
|
+
- Does the company use or encourage AI coding tools (Copilot, Claude Code, Cursor, agentic workflows)?
|
|
224
|
+
- Any public statements, blog posts, or job descriptions mentioning AI-assisted development?
|
|
225
|
+
- Is the engineering culture likely to embrace engineers who ship faster using AI tools, or is there resistance?
|
|
226
|
+
- Look for signals: do they mention "AI-native", "agentic", "LLM-augmented" workflows, or similar?
|
|
227
|
+
- Are there concerns about AI tool usage (IP policies, code review friction, etc.)?
|
|
228
|
+
- Rating: AI-Forward / AI-Friendly / Neutral / Unknown / AI-Resistant
|
|
229
|
+
|
|
230
|
+
### 3. Company Deep Dive
|
|
218
231
|
- What the company does, their products/services
|
|
219
232
|
- Funding stage, size, recent news
|
|
220
233
|
- Tech stack and engineering culture
|
|
@@ -222,38 +235,33 @@ Research everything you can and produce a complete dossier covering:
|
|
|
222
235
|
- Employee sentiment and Glassdoor/Blind highlights
|
|
223
236
|
- Any red flags or concerns
|
|
224
237
|
|
|
225
|
-
###
|
|
238
|
+
### 4. Role Analysis
|
|
226
239
|
- What you'd actually be doing day-to-day
|
|
227
240
|
- Team context — who you'd work with
|
|
228
241
|
- Growth trajectory for this role
|
|
229
242
|
- How this role fits into the company's current priorities
|
|
230
243
|
|
|
231
|
-
###
|
|
244
|
+
### 5. Fit Assessment
|
|
232
245
|
- Matching skills and experience (be specific, reference the candidate's actual background)
|
|
233
246
|
- Skill gaps to acknowledge or address
|
|
234
247
|
- Adjacent experience that transfers
|
|
235
248
|
- Overall fit rating: Strong / Good / Stretch
|
|
236
249
|
|
|
237
|
-
###
|
|
250
|
+
### 6. Cover Letter Talking Points
|
|
238
251
|
- 5-7 specific bullet points to mention, each referencing the candidate's real experience
|
|
239
252
|
- What angle to take (technical depth? leadership? domain expertise?)
|
|
240
253
|
- What to emphasize vs. downplay
|
|
241
254
|
|
|
242
|
-
###
|
|
255
|
+
### 7. Contact Info
|
|
243
256
|
- Email addresses from the posting
|
|
244
257
|
- Who posted (HN username from the URL if available)
|
|
245
258
|
- Best way to reach out
|
|
246
259
|
|
|
247
|
-
###
|
|
260
|
+
### 8. Interview Prep
|
|
248
261
|
- Questions they'll likely ask for this role
|
|
249
262
|
- Questions the candidate should ask
|
|
250
263
|
- Topics to research before interviewing
|
|
251
264
|
|
|
252
|
-
### 7. Compensation Context
|
|
253
|
-
- Market rate for this role/level/location
|
|
254
|
-
- How the listed salary compares
|
|
255
|
-
- Negotiation leverage points
|
|
256
|
-
|
|
257
265
|
Be thorough, specific, and opinionated. Reference the candidate's actual experience when making recommendations.`;
|
|
258
266
|
|
|
259
267
|
try {
|
|
@@ -458,6 +466,22 @@ Be thorough, specific, and opinionated. Reference the candidate's actual experie
|
|
|
458
466
|
return filename;
|
|
459
467
|
}, []);
|
|
460
468
|
|
|
469
|
+
// Seed cache with server-side dossier flags from job list
|
|
470
|
+
const seedDossierFlags = useCallback((jobs) => {
|
|
471
|
+
let changed = false;
|
|
472
|
+
for (const job of jobs) {
|
|
473
|
+
if (job.has_dossier && !dossierCache.current.has(job.id)) {
|
|
474
|
+
dossierCache.current.set(job.id, {
|
|
475
|
+
text: '',
|
|
476
|
+
done: true,
|
|
477
|
+
loading: false,
|
|
478
|
+
});
|
|
479
|
+
changed = true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (changed) bumpTick();
|
|
483
|
+
}, []);
|
|
484
|
+
|
|
461
485
|
// Expose dossier status for job list icons
|
|
462
486
|
// Returns: 'generating' | 'done' | null
|
|
463
487
|
const getDossierStatus = useCallback((jobId) => {
|
|
@@ -468,6 +492,19 @@ Be thorough, specific, and opinionated. Reference the candidate's actual experie
|
|
|
468
492
|
return null;
|
|
469
493
|
}, []);
|
|
470
494
|
|
|
495
|
+
const regenerateDossier = useCallback(
|
|
496
|
+
(job, api) => {
|
|
497
|
+
// Clear cached dossier so dossier() starts fresh
|
|
498
|
+
dossierCache.current.delete(job.id);
|
|
499
|
+
cancel();
|
|
500
|
+
setText('');
|
|
501
|
+
setError(null);
|
|
502
|
+
bumpTick();
|
|
503
|
+
dossier(job, api);
|
|
504
|
+
},
|
|
505
|
+
[dossier, cancel]
|
|
506
|
+
);
|
|
507
|
+
|
|
471
508
|
return {
|
|
472
509
|
text,
|
|
473
510
|
loading,
|
|
@@ -476,8 +513,10 @@ Be thorough, specific, and opinionated. Reference the candidate's actual experie
|
|
|
476
513
|
mode,
|
|
477
514
|
hasActiveProcess: Boolean(childRef.current),
|
|
478
515
|
getDossierStatus,
|
|
516
|
+
seedDossierFlags,
|
|
479
517
|
summarizeJob,
|
|
480
518
|
dossier,
|
|
519
|
+
regenerateDossier,
|
|
481
520
|
batchReview,
|
|
482
521
|
exportDossier,
|
|
483
522
|
cancel,
|
package/src/tui/useJobs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import { getCached, setCache, updateCachedJob } from '../cache.js';
|
|
3
3
|
|
|
4
|
-
export function useJobs(api, activeFilters, tab, searchId) {
|
|
4
|
+
export function useJobs(api, activeFilters, tab, searchId, getDossierStatus) {
|
|
5
5
|
const [allJobs, setAllJobs] = useState([]);
|
|
6
6
|
const [loading, setLoading] = useState(true);
|
|
7
7
|
const [reranking, setReranking] = useState(false);
|
|
@@ -135,6 +135,22 @@ export function useJobs(api, activeFilters, tab, searchId) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
if (tab === 'new')
|
|
139
|
+
return filtered.filter(
|
|
140
|
+
(j) =>
|
|
141
|
+
!j.state &&
|
|
142
|
+
!j.has_dossier &&
|
|
143
|
+
(!getDossierStatus ||
|
|
144
|
+
(getDossierStatus(j.id) !== 'done' &&
|
|
145
|
+
getDossierStatus(j.id) !== 'generating'))
|
|
146
|
+
);
|
|
147
|
+
if (tab === 'reviewed')
|
|
148
|
+
return filtered.filter(
|
|
149
|
+
(j) =>
|
|
150
|
+
!j.state &&
|
|
151
|
+
(j.has_dossier ||
|
|
152
|
+
(getDossierStatus && getDossierStatus(j.id) === 'done'))
|
|
153
|
+
);
|
|
138
154
|
if (tab === 'interested')
|
|
139
155
|
return filtered.filter((j) => j.state === 'interested');
|
|
140
156
|
if (tab === 'applied') return filtered.filter((j) => j.state === 'applied');
|
|
@@ -145,7 +161,7 @@ export function useJobs(api, activeFilters, tab, searchId) {
|
|
|
145
161
|
return filtered.filter(
|
|
146
162
|
(j) => j.state !== 'not_interested' && j.state !== 'dismissed'
|
|
147
163
|
);
|
|
148
|
-
}, [allJobs, activeFilters, tab]);
|
|
164
|
+
}, [allJobs, activeFilters, tab, getDossierStatus]);
|
|
149
165
|
|
|
150
166
|
const markJob = useCallback(
|
|
151
167
|
async (id, state, feedback) => {
|