Identity-only senses no longer lock a new cue. Learning stages, journal-generate review, and additive docs follow the closed section 6. Co-authored-by: Cursor <cursoragent@cursor.com>
101 lines
3.8 KiB
JavaScript
101 lines
3.8 KiB
JavaScript
function HighlightedExcerpt({ excerpt, highlightStart, highlightEnd, label }) {
|
|
if (!excerpt) return null
|
|
const start = Number(highlightStart)
|
|
const end = Number(highlightEnd)
|
|
if (Number.isInteger(start) && Number.isInteger(end) && start >= 0 && end <= excerpt.length && end > start) {
|
|
return (
|
|
<p className="mask-excerpt">
|
|
{excerpt.slice(0, start)}
|
|
<mark>{excerpt.slice(start, end)}</mark>
|
|
{excerpt.slice(end)}
|
|
</p>
|
|
)
|
|
}
|
|
const needle = label || ''
|
|
const at = needle ? excerpt.toLowerCase().indexOf(needle.toLowerCase()) : -1
|
|
if (at < 0) return <p className="mask-excerpt">{excerpt}</p>
|
|
return (
|
|
<p className="mask-excerpt">
|
|
{excerpt.slice(0, at)}
|
|
<mark>{excerpt.slice(at, at + needle.length)}</mark>
|
|
{excerpt.slice(at + needle.length)}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
function reviewCopy(kind) {
|
|
if (kind === 'confirm_remote') {
|
|
return {
|
|
title: 'Klartext-Detect bestätigen',
|
|
status: 'GLiNER fehlt. Ein öffentliches Detect darf den Klartext nur senden, wenn du das ausdrücklich bestätigst. „Identität“ heißt senden, „Nicht schützenswert“ bricht ab.'
|
|
}
|
|
}
|
|
if (kind === 'manual') {
|
|
return {
|
|
title: 'Maskierung prüfen',
|
|
status: 'Manuelles Lernen: bitte jede vorgeschlagene Nennung bestätigen oder als nicht schützenswert markieren.'
|
|
}
|
|
}
|
|
return {
|
|
title: 'Maskierung prüfen',
|
|
status: 'Abweichung oder neue Identität. Nur die gelb markierte Nennung. Bestätigen legt die Bezeichnung lokal ab. „Nicht schützenswert“ verhindert die Maskierung.'
|
|
}
|
|
}
|
|
|
|
export default function MaskReviewPanel({ review, busy, onSubmit }) {
|
|
if (!review?.candidates?.length) return null
|
|
const copy = reviewCopy(review.kind)
|
|
return (
|
|
<div className="runlog-overlay" role="dialog" aria-labelledby="mask-review-title">
|
|
<div className="runlog-modal">
|
|
<div className="runlog-head">
|
|
<div>
|
|
<h2 id="mask-review-title">{copy.title}</h2>
|
|
<p className="runlog-status">{copy.status}</p>
|
|
</div>
|
|
</div>
|
|
<form
|
|
className="stack"
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
const data = new FormData(event.currentTarget)
|
|
const decisions = review.candidates.map((item) => ({
|
|
id: item.id,
|
|
decision: data.get(`decision-${item.id}`) || 'identity'
|
|
}))
|
|
onSubmit(decisions)
|
|
}}
|
|
>
|
|
<ul className="stack">
|
|
{review.candidates.map((item) => (
|
|
<li key={item.id} className="row-item">
|
|
<div>
|
|
<strong>{item.text}</strong>
|
|
<span className="muted"> · {item.entity_type}{item.ambiguous ? ' · mehrdeutig' : ''}</span>
|
|
<HighlightedExcerpt
|
|
excerpt={item.excerpt}
|
|
highlightStart={item.highlight_start}
|
|
highlightEnd={item.highlight_end}
|
|
label={item.text}
|
|
/>
|
|
</div>
|
|
<fieldset className="row-actions">
|
|
<label className="check">
|
|
<input type="radio" name={`decision-${item.id}`} value="identity" defaultChecked={item.suggested !== 'not_identity'} />
|
|
Identität
|
|
</label>
|
|
<label className="check">
|
|
<input type="radio" name={`decision-${item.id}`} value="not_identity" defaultChecked={item.suggested === 'not_identity'} />
|
|
Nicht schützenswert
|
|
</label>
|
|
</fieldset>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<button type="submit" disabled={busy}>{busy ? 'Übernimmt …' : 'Übernehmen und weiter'}</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|