/** * UnifiedRuleBlock — renders ALL rules as ONE continuous JSON code block. * * The entire rules array is displayed as a single [...] JSON with: * - Continuous line numbers across all rules * - Copy button to copy the clean JSON * - Inline suggestion blocks for flagged rules: * • Red-highlighted lines on differing fields * • Green suggestion preview lines (with line numbers) * • Accept / Edit / Keep buttons * - When a suggestion is acted on, the suggestion lines disappear * - Edit mode shows an inline textarea for the specific rule */ import React, { useState, useMemo, useCallback } from 'react' // ── Helpers ───────────────────────────────────────────────────────────────── const STANDARD_ACTIONS = [ 'AUTO_APPROVE', 'ROUTE_TO_AP_CLERK', 'ROUTE_TO_DEPT_HEAD', 'ESCALATE_TO_FINANCE_CONTROLLER', 'ESCALATE_TO_CFO', 'HOLD', 'REJECT', 'FLAG', 'ROUTE_TO_PROCUREMENT', 'COMPLIANCE_HOLD', ] function valuesMatch(a, b) { return JSON.stringify(a) === JSON.stringify(b) } function getDiffKeys(rule, fix) { if (!fix) return new Set() const skip = new Set(['conflict_with', 'suggested_fix', 'review_status', 'section']) const allKeys = new Set([...Object.keys(rule), ...Object.keys(fix)]) const diffs = new Set() for (const k of allKeys) { if (skip.has(k)) continue if (!valuesMatch(rule[k], fix[k])) diffs.add(k) } return diffs } function isFlaggedRule(rule) { return rule.confidence_score < 0.9 || (rule.conflict_with?.length > 0) } function isResolvedRule(rule) { return ['accepted', 'modified', 'kept_original'].includes(rule.review_status) } function getConflictExplanation(rule, conflicts) { if (!conflicts) return '' return conflicts .filter((c) => c.rule_id_b === rule.rule_id || c.rule_id_a === rule.rule_id) .map((c) => c.explanation) .join(' ') } function syntaxHighlight(text) { let s = text .replace(/&/g, '&') .replace(/, '<') .replace(/>/, '>') // JSON key → cyan s = s.replace( /("(?:[^"\\]|\\.)*")\s*:/g, '$1:' ) // String value → green s = s.replace( /:\s*("(?:[^"\\]|\\.)*")/g, (match, val) => match.replace(val, `${val}`) ) // Standalone strings in arrays s = s.replace( /(?<=[\[,]\s*)("(?:[^"\\]|\\.)*")/g, '$1' ) // Numbers → amber s = s.replace( /:\s*(-?\d+\.?\d*)/g, (match, num) => match.replace(num, `${num}`) ) // Booleans / null → purple s = s.replace( /\b(true|false|null)\b/g, '$1' ) return s } // ── Main Component ────────────────────────────────────────────────────────── export default function UnifiedRuleBlock({ rules, conflicts, onUpdate }) { const [editingRuleId, setEditingRuleId] = useState(null) const [editJson, setEditJson] = useState('') const [editError, setEditError] = useState('') const [copied, setCopied] = useState(false) const [selectedFixActions, setSelectedFixActions] = useState({}) // ── Copyable clean JSON (entire rules array) ────────────────────────── const copyText = useMemo(() => JSON.stringify(rules, null, 2), [rules]) // ── Build rendering items ───────────────────────────────────────────── // Each item has a `type`: // 'line' — numbered code line (rule JSON or suggestion preview) // 'banner' — suggestion explanation (no line number) // 'actions' — Accept / Edit / Keep buttons (no line number) // 'edit-zone' — inline textarea for the rule being edited const renderItems = useMemo(() => { const items = [] let lineNum = 0 // Opening bracket items.push({ type: 'line', num: ++lineNum, text: '[', hl: false, sug: false }) rules.forEach((rule, ruleIdx) => { // ── Edit zone placeholder ─────────────────────────────────────── if (editingRuleId === rule.rule_id) { items.push({ type: 'edit-zone', ruleId: rule.rule_id }) return } // ── Permanent rule header: always shows rule_id + live confidence badge ─ // This row is NEVER removed — even after Accept/Edit/Keep — so the user // can see the confidence score climb after taking action. items.push({ type: 'rule-header', ruleId: rule.rule_id, confidenceScore: rule.confidence_score ?? 0, reviewStatus: rule.review_status, }) // ── Build annotated lines for this rule ───────────────────────── const ruleJson = JSON.stringify(rule, null, 2) const ruleLines = ruleJson.split('\n') const flagged = isFlaggedRule(rule) const resolved = isResolvedRule(rule) const diffKeys = getDiffKeys(rule, rule.suggested_fix) let currentTopKey = null ruleLines.forEach((line, lineIdx) => { // Track top-level key (2-space indent = top-level inside object) const topKeyMatch = line.match(/^ "([^"]+)"/) if (topKeyMatch) currentTopKey = topKeyMatch[1] const trimmed = line.trim() if (trimmed === '{' || trimmed === '}') currentTopKey = null // Indent by 2 for array nesting + comma between rules let text = ' ' + line if (lineIdx === ruleLines.length - 1 && ruleIdx < rules.length - 1) { text += ',' } const highlighted = flagged && !resolved && currentTopKey != null && diffKeys.has(currentTopKey) items.push({ type: 'line', num: ++lineNum, text, hl: highlighted, sug: false }) }) // ── Inline suggestion (flagged + unresolved) ──────────────────── if (flagged && !resolved) { const explanation = getConflictExplanation(rule, conflicts) let previewText = null if (rule.suggested_fix && diffKeys.size > 0) { const previewObj = {} for (const k of diffKeys) previewObj[k] = rule.suggested_fix[k] previewText = JSON.stringify(previewObj, null, 2) } // Banner (explanation only — confidence badge is now in the permanent header above) items.push({ type: 'banner', ruleId: rule.rule_id, explanation: explanation || (rule.confidence_score < 0.9 ? `Low confidence (${Math.round(rule.confidence_score * 100)}%). Review condition and action for correctness.` : ''), }) // Suggestion preview lines (green bg, numbered) if (previewText) { previewText.split('\n').forEach((sLine) => { items.push({ type: 'line', num: ++lineNum, text: ' ' + sLine, hl: false, sug: true, }) }) } // Action buttons const fixAction = rule.suggested_fix?.action const isCustomAction = fixAction && !STANDARD_ACTIONS.includes(fixAction) items.push({ type: 'actions', ruleId: rule.rule_id, hasFix: !!rule.suggested_fix, isCustomAction, fixAction, }) } }) // Closing bracket items.push({ type: 'line', num: ++lineNum, text: ']', hl: false, sug: false }) return items }, [rules, conflicts, editingRuleId]) // ── Handlers ────────────────────────────────────────────────────────── const handleCopy = useCallback(async () => { try { await navigator.clipboard.writeText(copyText) } catch { const ta = document.createElement('textarea') ta.value = copyText document.body.appendChild(ta) ta.select() document.execCommand('copy') document.body.removeChild(ta) } setCopied(true) setTimeout(() => setCopied(false), 2000) }, [copyText]) const handleAccept = useCallback( (ruleId) => { const rule = rules.find((r) => r.rule_id === ruleId) if (!rule?.suggested_fix) return let finalAction = rule.suggested_fix.action const isCustomAction = finalAction && !STANDARD_ACTIONS.includes(finalAction) if (isCustomAction) { if (!selectedFixActions[ruleId]) return // MUST select something finalAction = selectedFixActions[ruleId] } // Merge: preserve current fields, override with suggested_fix, clear fix. // A human accepted it, so it is now 100% verified. onUpdate(ruleId, { ...rule, ...rule.suggested_fix, action: finalAction, review_status: 'accepted', suggested_fix: null, confidence_score: 1.0, }) // Clean up selection setSelectedFixActions(prev => { const next = { ...prev } delete next[ruleId] return next }) }, [rules, onUpdate, selectedFixActions] ) const handleEdit = useCallback( (ruleId) => { const rule = rules.find((r) => r.rule_id === ruleId) if (!rule) return setEditJson(JSON.stringify(rule, null, 2)) setEditError('') setEditingRuleId(ruleId) }, [rules] ) const handleSaveEdit = useCallback(() => { try { const parsed = JSON.parse(editJson) // Manual edit shows human engagement — 100% verified. onUpdate(editingRuleId, { ...parsed, review_status: 'modified', confidence_score: 1.0 }) setEditingRuleId(null) } catch (e) { setEditError(`Invalid JSON: ${e.message}`) } }, [editJson, editingRuleId, onUpdate]) const handleCancelEdit = useCallback(() => { setEditingRuleId(null) setEditError('') }, []) const handleKeep = useCallback( (ruleId) => { const rule = rules.find((r) => r.rule_id === ruleId) if (!rule) return // Human confirmed the original is correct — 100% verified. onUpdate(ruleId, { ...rule, review_status: 'kept_original', confidence_score: 1.0 }) }, [rules, onUpdate] ) // ── Render ──────────────────────────────────────────────────────────── return (
{renderItems.map((item, idx) => {
// ── Numbered code line ──────────────────────────────────
if (item.type === 'line') {
return (
{item.num}
)
}
// ── Permanent rule header with live confidence badge ────
if (item.type === 'rule-header') {
const confPct = Math.round((item.confidenceScore ?? 0) * 100)
const confColor =
confPct >= 90 ? '#34d399'
: confPct >= 70 ? '#fbbf24'
: '#f87171'
const confBg =
confPct >= 90 ? 'rgba(52,211,153,0.08)'
: confPct >= 70 ? 'rgba(251,191,36,0.08)'
: 'rgba(248,113,113,0.08)'
const statusBadge =
item.reviewStatus === 'accepted' ? { label: '✓ Accepted', color: '#34d399', bg: 'rgba(52,211,153,0.1)' }
: item.reviewStatus === 'modified' ? { label: '✎ Modified', color: '#818cf8', bg: 'rgba(129,140,248,0.1)' }
: item.reviewStatus === 'kept_original' ? { label: '◎ Kept', color: '#94a3b8', bg: 'rgba(148,163,184,0.1)' }
: null
return (
{/* Rule ID */}
{item.ruleId}
{/* Live confidence badge */}
⬤ {confPct}%
{/* Review status badge (shown only after action taken) */}
{statusBadge && (
{statusBadge.label}
)}
)
}
// ── Suggestion banner (no line number) ─────────────────
if (item.type === 'banner') {
// Find the rule to read its live confidence score
const ruleForBanner = rules.find(r => r.rule_id === item.ruleId)
const confScore = ruleForBanner?.confidence_score ?? 0
const confPct = Math.round(confScore * 100)
const confColor =
confPct >= 90 ? '#34d399'
: confPct >= 70 ? '#fbbf24'
: '#f87171'
const confBg =
confPct >= 90 ? 'rgba(52,211,153,0.12)'
: confPct >= 70 ? 'rgba(251,191,36,0.12)'
: 'rgba(248,113,113,0.12)'
return (
)
}
// ── Action buttons (no line number) ────────────────────
if (item.type === 'actions') {
const needsDropdown = item.hasFix && item.isCustomAction
const selectedAction = selectedFixActions[item.ruleId] || ''
const canAccept = item.hasFix && (!needsDropdown || selectedAction)
return (
{needsDropdown && (
Custom Action (Needs Review):
{item.fixAction}
)}
{item.hasFix && (
)}
)
}
// ── Inline edit zone ───────────────────────────────────
if (item.type === 'edit-zone') {
return (
{item.ruleId}
— editing
)
}
return null
})}