2026-09-10 20:08:59 +02:00
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 >
)
}
2026-10-12 08:59:18 +02:00
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.'
}
}
2026-09-09 08:34:43 +02:00
export default function MaskReviewPanel ( { review , busy , onSubmit } ) {
if ( ! review ? . candidates ? . length ) return null
2026-10-12 08:59:18 +02:00
const copy = reviewCopy ( review . kind )
2026-09-09 08:34:43 +02:00
return (
< div className = "runlog-overlay" role = "dialog" aria - labelledby = "mask-review-title" >
< div className = "runlog-modal" >
< div className = "runlog-head" >
< div >
2026-10-12 08:59:18 +02:00
< h2 id = "mask-review-title" > { copy . title } < / h2 >
< p className = "runlog-status" > { copy . status } < / p >
2026-09-09 08:34:43 +02:00
< / 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 >
2026-09-10 20:08:59 +02:00
< HighlightedExcerpt
excerpt = { item . excerpt }
highlightStart = { item . highlight _start }
highlightEnd = { item . highlight _end }
label = { item . text }
/ >
2026-09-09 08:34:43 +02:00
< / div >
< fieldset className = "row-actions" >
< label className = "check" >
2026-09-09 08:47:44 +02:00
< input type = "radio" name = { ` decision- ${ item . id } ` } value = "identity" defaultChecked = { item . suggested !== 'not_identity' } / >
2026-09-09 08:34:43 +02:00
Identität
< / label >
< label className = "check" >
2026-09-09 08:47:44 +02:00
< input type = "radio" name = { ` decision- ${ item . id } ` } value = "not_identity" defaultChecked = { item . suggested === 'not_identity' } / >
2026-09-09 08:34:43 +02:00
Nicht schützenswert
< / label >
< / fieldset >
< / li >
) ) }
< / ul >
< button type = "submit" disabled = { busy } > { busy ? 'Übernimmt …' : 'Übernehmen und weiter' } < / button >
< / form >
< / div >
< / div >
)
}