Kansho/frontend/src/components/MaskReviewPanel.jsx

101 lines
3.8 KiB
React
Raw Normal View History

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>
)
}