@gafj/gafj 0.1.11 → 0.1.13
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/http/api.js +1 -0
- package/package.json +1 -1
- package/store/onboarding.js +19 -2
- package/ui/app.css +4 -0
- package/ui/screens/kb.js +12 -4
package/http/api.js
CHANGED
|
@@ -230,6 +230,7 @@ route("POST", "/api/run/:op", async (c, p, b, q) => {
|
|
|
230
230
|
}
|
|
231
231
|
const r = saveReply(c.db, { run_id: opened.run_id, content: reply.content, route: routeName, actor: ACTOR("run"), now: c.now(), provider: reply.provider, model: reply.model, tokens_in: reply.tokens_in, tokens_out: reply.tokens_out, latency_ms: reply.latency_ms });
|
|
232
232
|
if (r.status !== "passed" && reply.stop_reason === "max_tokens") r.errors = [...(r.errors || []), "the reply was cut off at the token limit"];
|
|
233
|
+
Object.assign(r, { tokens_in: reply.tokens_in ?? null, tokens_out: reply.tokens_out ?? null, latency_ms: reply.latency_ms ?? null, model: reply.model || null });
|
|
233
234
|
return reply.credits_left === undefined ? r : { ...r, charged: reply.charged, credits_left: reply.credits_left };
|
|
234
235
|
});
|
|
235
236
|
|
package/package.json
CHANGED
package/store/onboarding.js
CHANGED
|
@@ -96,11 +96,28 @@ function sourceText(db, source_document_id) {
|
|
|
96
96
|
|
|
97
97
|
// ----- propose_from_source -----
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Excerpt or nothing, with the counting done here: a model quotes reliably but cannot count
|
|
101
|
+
* characters, so a span that does not match is relocated to where the quoted text actually
|
|
102
|
+
* sits in the source (the occurrence nearest the claimed start; whitespace runs may differ).
|
|
103
|
+
* A value that appears nowhere in the source, byte for byte and case for case, is refused.
|
|
104
|
+
*/
|
|
99
105
|
function checkAtom(atom, text, label, errors) {
|
|
100
106
|
if (!atom) return;
|
|
101
107
|
const { value, start, end } = atom;
|
|
102
|
-
if (!Number.isInteger(start) || !Number.isInteger(end)
|
|
103
|
-
if (
|
|
108
|
+
if (!Number.isInteger(start) || !Number.isInteger(end)) { errors.push(`${label}: span [${start}, ${end}) is not inside the source`); return; }
|
|
109
|
+
if (start >= 0 && end <= text.length && end > start && text.slice(start, end) === value) return;
|
|
110
|
+
const v = String(value);
|
|
111
|
+
const near = Number.isInteger(start) ? Math.max(0, start) : 0;
|
|
112
|
+
let hits = [];
|
|
113
|
+
if (v.length) for (let i = text.indexOf(v); i !== -1; i = text.indexOf(v, i + 1)) hits.push([i, i + v.length]);
|
|
114
|
+
if (!hits.length && v.trim()) {
|
|
115
|
+
const re = new RegExp(v.trim().split(/\s+/).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+"), "g");
|
|
116
|
+
for (let m = re.exec(text); m; m = re.exec(text)) { hits.push([m.index, m.index + m[0].length]); if (!m[0].length) break; }
|
|
117
|
+
}
|
|
118
|
+
if (!hits.length) { errors.push(`${label}: "${v.slice(0, 40)}" is not the source text at [${start}, ${end})`); return; }
|
|
119
|
+
const [s, e] = hits.reduce((best, h) => (Math.abs(h[0] - near) < Math.abs(best[0] - near) ? h : best));
|
|
120
|
+
atom.start = s; atom.end = e; atom.value = text.slice(s, e);
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
/** Validate a draft against its source: every atom cited and verbatim, else the whole draft is rejected. */
|
package/ui/app.css
CHANGED
|
@@ -182,3 +182,7 @@ mark { background: var(--good-bg); color: var(--good-bright); border-radius: 3px
|
|
|
182
182
|
.switch input:checked + .track { background: var(--accent); }
|
|
183
183
|
.switch input:checked + .track::after { left: 18px; }
|
|
184
184
|
.switch input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
185
|
+
/* a small spinner beside anything that is with the model */
|
|
186
|
+
.spin { display: inline-block; width: 10px; height: 10px; border: 2px solid var(--line-2); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; vertical-align: -1px; }
|
|
187
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
188
|
+
@media (prefers-reduced-motion: reduce) { .spin { animation: none; border-top-color: var(--line-2); } }
|
package/ui/screens/kb.js
CHANGED
|
@@ -15,13 +15,21 @@ export function Onboarding({ d, reload, setErr, route, compact }) {
|
|
|
15
15
|
const b = d.batch;
|
|
16
16
|
const direct = route === "byo_key" || route === "credits";
|
|
17
17
|
// with a provider or credits the extraction runs here; otherwise the packet opens for paste
|
|
18
|
+
// a ticking clock while a file is with the model, so a long call is visibly alive
|
|
19
|
+
const [, tick] = useState(0);
|
|
20
|
+
useEffect(() => { const t = setInterval(() => tick((n) => n + 1), 1000); return () => clearInterval(t); }, []);
|
|
21
|
+
const since = {};
|
|
22
|
+
const elapsed = (ms) => { const sec = Math.round(ms / 1000); return `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, "0")}`; };
|
|
18
23
|
const runOne = async (s) => {
|
|
19
|
-
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
setRunning((r) => ({ ...r, [s.source_document_id]: { started } }));
|
|
20
26
|
try {
|
|
21
27
|
const r = await api.post(`/api/run/extract?source_document_id=${s.source_document_id}`, {});
|
|
22
|
-
|
|
28
|
+
const took = `${elapsed(Date.now() - started)}${r.tokens_in ? ` · ${(r.tokens_in + (r.tokens_out || 0)).toLocaleString()} tokens` : ""}`;
|
|
29
|
+
setRunning((x) => ({ ...x, [s.source_document_id]: r.status === "passed" ? `${r.pending_ids.length} records proposed in ${took}` : `refused after ${took}: ${(r.errors || []).slice(0, 2).join("; ")}` }));
|
|
23
30
|
} catch (e) { setRunning((x) => ({ ...x, [s.source_document_id]: "failed: " + e.message })); }
|
|
24
31
|
};
|
|
32
|
+
const isRunning = (id) => running[id] && typeof running[id] === "object";
|
|
25
33
|
const extract = async (s) => {
|
|
26
34
|
if (!direct) { setModal({ source: s }); return; }
|
|
27
35
|
await runOne(s);
|
|
@@ -57,9 +65,9 @@ export function Onboarding({ d, reload, setErr, route, compact }) {
|
|
|
57
65
|
${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
66
|
${s.empty ? html`<span class="tag blocked">no text</span>` : html`<span class="muted small">${s.chars} chars</span>`}
|
|
59
67
|
<span class="muted small">${s.pending} pending, ${s.runs} runs</span>
|
|
60
|
-
${!s.empty ? html`<button class="small" disabled=${
|
|
68
|
+
${!s.empty ? html`<button class="small" disabled=${isRunning(s.source_document_id)} onClick=${() => extract(s)}>${isRunning(s.source_document_id) ? html`<span class="spin"></span> extracting · ${elapsed(Date.now() - running[s.source_document_id].started)}` : direct ? "Extract" : "Extract (paste)"}</button>` : ""}
|
|
61
69
|
<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>
|
|
62
|
-
${running[s.source_document_id] &&
|
|
70
|
+
${running[s.source_document_id] && !isRunning(s.source_document_id) ? html`<span class="small muted">${running[s.source_document_id]}</span>` : ""}</li>`)}
|
|
63
71
|
${b.sources.length ? "" : html`<li class="muted">upload a resume to start</li>`}</ul>` : ""}
|
|
64
72
|
${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>` : ""}
|
|
65
73
|
${groups.map((g) => html`<div class="block warn" key=${g.id}><b>These may be the same accomplishment</b>
|