File size: 4,399 Bytes
b1c84b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
 * WordHighlighter — Phase 8: Suspicious Word Highlighter
 * Highlights suspicious / clickbait trigger words in the claim text.
 * Uses triggered_features from Layer 1 as hint words.
 *
 * architect-review: pure presentational, no side-effects.
 * web-design-guidelines: uses <mark> with visible styles, screen-reader friendly.
 */

// Common suspicious/misinformation signal words to highlight
const SUSPICIOUS_PATTERNS = [
    // English signals
    /\b(shocking|exposed|revealed|secret|hoax|fake|false|confirmed|breaking|urgent|emergency|exclusive|banned|cover[\s-]?up|conspiracy|miracle|crisis|scandal|leaked|hidden|truth|they don't want you to know)\b/gi,
    // Filipino signals
    /\b(grabe|nakakagulat|totoo|peke|huwag maniwala|nagsisinungaling|lihim|inilabas|natuklasan|katotohanan|panlilinlang|kahirap-hirap|itinatago)\b/gi,
]

function getHighlightedSegments(text, triggerWords = []) {
    if (!text) return []

    // Build a combined pattern from both static patterns + dynamic trigger words
    const allPatterns = [...SUSPICIOUS_PATTERNS]

    if (triggerWords.length > 0) {
        const escaped = triggerWords.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
        allPatterns.push(new RegExp(`\\b(${escaped.join('|')})\\b`, 'gi'))
    }

    // Find all match intervals
    const matches = []
    for (const pattern of allPatterns) {
        pattern.lastIndex = 0
        let m
        while ((m = pattern.exec(text)) !== null) {
            matches.push({ start: m.index, end: m.index + m[0].length, word: m[0] })
        }
    }

    if (matches.length === 0) return [{ text, highlighted: false }]

    // Sort + merge overlapping intervals
    matches.sort((a, b) => a.start - b.start)
    const merged = []
    for (const m of matches) {
        const last = merged[merged.length - 1]
        if (last && m.start <= last.end) {
            last.end = Math.max(last.end, m.end)
        } else {
            merged.push({ ...m })
        }
    }

    // Build segments
    const segments = []
    let cursor = 0
    for (const { start, end, word } of merged) {
        if (cursor < start) segments.push({ text: text.slice(cursor, start), highlighted: false })
        segments.push({ text: text.slice(start, end), highlighted: true, word })
        cursor = end
    }
    if (cursor < text.length) segments.push({ text: text.slice(cursor), highlighted: false })

    return segments
}

export default function WordHighlighter({ text = '', triggerWords = [], className = '' }) {
    const segments = getHighlightedSegments(text, triggerWords)
    const hitCount = segments.filter(s => s.highlighted).length

    if (segments.length === 1 && !segments[0].highlighted) {
        // No suspicious words found
        return (
            <p className={className}
                style={{ fontFamily: 'var(--font-body)', lineHeight: 1.7, color: 'var(--text-primary)' }}>
                {text}
            </p>
        )
    }

    return (
        <div>
            {hitCount > 0 && (
                <p className="text-xs mb-2"
                    style={{
                        color: 'var(--accent-gold)',
                        fontFamily: 'var(--font-display)',
                        letterSpacing: '0.08em',
                    }}
                    aria-live="polite">
                    ⚠ {hitCount} suspicious signal{hitCount !== 1 ? 's' : ''} detected
                </p>
            )}
            <p className={className} style={{ fontFamily: 'var(--font-body)', lineHeight: 1.7, color: 'var(--text-primary)' }}>
                {segments.map((seg, i) =>
                    seg.highlighted ? (
                        <mark key={i}
                            title={`Suspicious signal: "${seg.word}"`}
                            style={{
                                background: 'rgba(220, 38, 38, 0.18)',
                                color: '#f87171',
                                borderRadius: 2,
                                padding: '0 2px',
                                fontWeight: 600,
                                outline: '1px solid rgba(220,38,38,0.3)',
                            }}>
                            {seg.text}
                        </mark>
                    ) : (
                        <span key={i}>{seg.text}</span>
                    )
                )}
            </p>
        </div>
    )
}