@gafj/gafj 0.1.6 → 0.1.8

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.
@@ -22,7 +22,8 @@ function checkUrl(url) {
22
22
  return u.toString();
23
23
  }
24
24
 
25
- async function post(fetchImpl, url, headers, body, timeoutMs = 120000) {
25
+ // a long resume with a span for every quoted phrase can take a model several minutes; the key test passes its own short limit
26
+ async function post(fetchImpl, url, headers, body, timeoutMs = 600000) {
26
27
  const f = fetchImpl || globalThis.fetch;
27
28
  const ac = new AbortController();
28
29
  const t = setTimeout(() => ac.abort(), timeoutMs);
package/http/api.js CHANGED
@@ -81,6 +81,7 @@ route("POST", "/api/sources", async (c, p, b) => {
81
81
  const buffer = Buffer.from(need(b, "data_base64"), "base64");
82
82
  return onboarding.addSource(c.db, { candidate_id: c.candidate_id, batch_id: batch, filename: need(b, "filename"), buffer, now: c.now(), actor: ACTOR("upload") });
83
83
  });
84
+ route("DELETE", "/api/sources/:id", (c, p) => onboarding.removeSource(c.db, { candidate_id: c.candidate_id, source_document_id: p.id, now: c.now(), actor: ACTOR("remove_source") }));
84
85
  route("GET", "/api/sources/:id", (c, p) => { const s = onboarding.sourceText(c.db, p.id); if (s.candidate_id !== c.candidate_id) throw bad("unknown source document", 404); return { source_document_id: s.id, filename: s.filename, kind: s.kind, extractor: s.extractor, extractor_version: s.extractor_version, text: s.text }; });
85
86
  route("POST", "/api/onboarding/batches/:id/dedup", (c, p) => onboarding.suggestDedup(c.db, { candidate_id: c.candidate_id, batch_id: p.id, now: c.now(), actor: ACTOR("dedup") }));
86
87
  route("POST", "/api/kb/groups/:id/merge", (c, p, b) => onboarding.mergeGroup(c.db, { candidate_id: c.candidate_id, dedup_group_id: p.id, keep_id: b.keep_id, now: c.now(), actor: ACTOR("merge") }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gafj/gafj",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "GAF-J: a local campaign engine for job search. One SQLite file, one ingest door, your own AI subscription.",
5
5
  "homepage": "https://gaf-j.com",
6
6
  "repository": {
@@ -70,6 +70,24 @@ function listSources(db, candidate_id, batch_id) {
70
70
  .map((s) => ({ ...s, empty: s.chars < 20 }));
71
71
  }
72
72
 
73
+ /** Remove an uploaded file and everything that came only from it: its pending rows, its extraction runs and attempts. Confirmed records stay; they are the candidate's, not the file's. */
74
+ function removeSource(db, { candidate_id, source_document_id, now, actor }) {
75
+ return withTx(db, (d) => {
76
+ const s = d.prepare("SELECT id, filename FROM source_document WHERE id = ? AND candidate_id = ?").get(source_document_id, candidate_id);
77
+ if (!s) throw new NotFound("source document");
78
+ const pending = d.prepare("DELETE FROM pending_accomplishment WHERE source_document_id = ?").run(s.id).changes;
79
+ const runs = d.prepare("SELECT id FROM ai_run WHERE source_document_id = ?").all(s.id).map((r) => r.id);
80
+ for (const id of runs) {
81
+ d.prepare("UPDATE pending_accomplishment SET ai_attempt_id = NULL WHERE ai_attempt_id IN (SELECT id FROM ai_attempt WHERE ai_run_id = ?)").run(id);
82
+ d.prepare("DELETE FROM ai_attempt WHERE ai_run_id = ?").run(id);
83
+ d.prepare("DELETE FROM ai_run WHERE id = ?").run(id);
84
+ }
85
+ d.prepare("DELETE FROM source_document WHERE id = ?").run(s.id);
86
+ appendEvent(d, { candidate_id, entity: "source_document", entity_id: s.id, event: "removed", actor_type: actor.type, actor_ref: actor.ref, payload: { filename: s.filename, pending, runs: runs.length }, at: now });
87
+ return { source_document_id: s.id, pending_removed: pending, runs_removed: runs.length };
88
+ });
89
+ }
90
+
73
91
  function sourceText(db, source_document_id) {
74
92
  const s = db.prepare("SELECT * FROM source_document WHERE id = ?").get(source_document_id);
75
93
  if (!s) throw new NotFound("source document");
@@ -265,4 +283,4 @@ function batchView(db, candidate_id) {
265
283
  return { batch_id: batch.id, status: batch.status, created_at: batch.created_at, sources, pending, groups: [...new Set(pending.map((p) => p.dedup_group_id).filter(Boolean))] };
266
284
  }
267
285
 
268
- module.exports = { openBatch, setBatchStatus, addSource, listSources, sourceText, checkDraft, proposeFromSource, saveExtraction, suggestDedup, mergeGroup, keepSeparate, proposeStyle, batchView, MAX_ATTEMPTS };
286
+ module.exports = { openBatch, setBatchStatus, addSource, removeSource, listSources, sourceText, checkDraft, proposeFromSource, saveExtraction, suggestDedup, mergeGroup, keepSeparate, proposeStyle, batchView, MAX_ATTEMPTS };
package/ui/lib.js CHANGED
@@ -9,7 +9,7 @@ async function req(method, path, body) {
9
9
  if (!r.ok) { const e = new Error(data.error || r.statusText); e.status = r.status; e.allowed = data.allowed; e.detail = data.detail; throw e; }
10
10
  return data;
11
11
  }
12
- export const api = { get: (p) => req("GET", p), post: (p, b) => req("POST", p, b === undefined ? {} : b), put: (p, b) => req("PUT", p, b === undefined ? {} : b) };
12
+ export const api = { get: (p) => req("GET", p), post: (p, b) => req("POST", p, b === undefined ? {} : b), put: (p, b) => req("PUT", p, b === undefined ? {} : b), del: (p) => req("DELETE", p, {}) };
13
13
 
14
14
  export function fmtWhen(iso, tz) {
15
15
  if (!iso) return "";
package/ui/screens/kb.js CHANGED
@@ -52,12 +52,13 @@ export function Onboarding({ d, reload, setErr, route, compact }) {
52
52
  const groups = b ? b.groups.map((g) => ({ id: g, rows: b.pending.filter((p) => p.dedup_group_id === g) })) : [];
53
53
  return html`<div class="card">
54
54
  <div class="row"><input type="file" multiple accept=".docx,.pdf,.md,.txt" disabled=${busy} onChange=${upload} /><span class="small muted">docx, pdf, md, txt; text is extracted locally, no OCR</span></div>
55
- ${direct && notYet > 1 ? html`<div class="row"><button class="primary" disabled=${!!all} onClick=${extractAll}>${all ? "Extracting " + all : `Extract all ${notYet} files`}</button><span class="small muted">one after another through your AI; a few seconds each</span></div>` : ""}
55
+ ${direct && notYet > 1 ? html`<div class="row"><button class="primary" disabled=${!!all} onClick=${extractAll}>${all ? "Extracting " + all : `Extract all ${notYet} files`}</button><span class="small muted">one after another through your AI; one to three minutes each for a full resume</span></div>` : ""}
56
56
  ${!direct && notYet > 1 ? html`<p class="small muted">On the paste route each file is one packet you carry to your chat and back; with a provider key in Settings the whole pile runs in one click.</p>` : ""}
57
57
  ${b ? html`<ul class="list">${b.sources.map((s) => html`<li key=${s.source_document_id}><b>${s.filename}</b> <span class="tag">${s.kind}</span>
58
58
  ${s.empty ? html`<span class="tag blocked">no text</span>` : html`<span class="muted small">${s.chars} chars</span>`}
59
59
  <span class="muted small">${s.pending} pending, ${s.runs} runs</span>
60
60
  ${!s.empty ? html`<button class="small" disabled=${running[s.source_document_id] === "running"} onClick=${() => extract(s)}>${running[s.source_document_id] === "running" ? "extracting" : direct ? "Extract" : "Extract (paste)"}</button>` : ""}
61
+ <button class="small danger" title="remove this file and anything proposed from it; confirmed records stay" onClick=${async () => { if (!confirm(`Remove ${s.filename}? Records already confirmed stay; pending rows from this file go.`)) return; try { await api.del(`/api/sources/${s.source_document_id}`); await reload(); } catch (e) { setErr(e.message); } }}>remove</button>
61
62
  ${running[s.source_document_id] && running[s.source_document_id] !== "running" ? html`<span class="small muted">${running[s.source_document_id]}</span>` : ""}</li>`)}
62
63
  ${b.sources.length ? "" : html`<li class="muted">upload a resume to start</li>`}</ul>` : ""}
63
64
  ${b && b.pending.length ? html`<div class="row"><button onClick=${() => post(`/api/onboarding/batches/${b.batch_id}/dedup`)}>Suggest duplicates</button><span class="small muted">same employer, overlapping dates, a shared figure; nothing merges without you</span></div>` : ""}