BlueFin / frontend /src /components /OfficerNotesPanel.jsx
vedanshmadan21's picture
Deploy Intelli-Credit to HuggingFace Spaces
7f6dd09
import { useState } from "react";
import { ClipboardList, Loader, AlertTriangle } from "lucide-react";
import { submitOfficerNotes } from "../api/client.js";
const QUICK_FILLS = [
{ label: "Factory at 40% capacity", text: "Factory operating at 40% capacity.", mode: "set" },
{ label: "MD evasive about order book", text: " MD was evasive about the order book.", mode: "append" },
{ label: "Inventory appears rusted", text: " Inventory appears aged and rusted.", mode: "append" },
{ label: "Strong order book — 3 LOIs from Tata", text: " Strong order book with 3 Letters of Intent from Tata Steel.", mode: "append" },
{ label: "Books well maintained, MD cooperative", text: " Books well maintained. MD was cooperative and transparent.", mode: "append" },
];
function scoreColor(score) {
if (score >= 75) return "var(--success)";
if (score >= 55) return "var(--warning)";
return "var(--danger)";
}
function decisionColor(decision) {
if (decision === "APPROVE") return "var(--success)";
if (decision === "CONDITIONAL") return "var(--warning)";
return "var(--danger)";
}
export default function OfficerNotesPanel({ jobId, currentScore, currentDecision, onScoreUpdate }) {
const [notes, setNotes] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [lastResult, setLastResult] = useState(null);
const charCount = notes.length;
const charColor = charCount >= 2000 ? "var(--danger)" : charCount >= 1800 ? "var(--warning)" : "var(--text-muted)";
async function handleSubmit() {
if (!notes.trim()) return;
setIsSubmitting(true);
try {
const response = await submitOfficerNotes(jobId, notes);
setLastResult(response);
if (!response.injection_detected) {
onScoreUpdate(response.score_after, response.decision_after, response.score_delta);
}
} catch (err) { console.error("Officer notes submission failed:", err); }
finally { setIsSubmitting(false); }
}
function applyQuickFill(chip) {
if (chip.mode === "set") setNotes(chip.text);
else setNotes((prev) => prev + chip.text);
setLastResult(null);
}
return (
<div className="card" style={{ marginTop: "24px" }}>
{/* Header */}
<div className="flex items-start gap-md" style={{ marginBottom: "16px" }}>
<div
style={{
width: "36px", height: "36px", borderRadius: "var(--radius-md)", flexShrink: 0,
background: "var(--accent-subtle)", display: "flex", alignItems: "center", justifyContent: "center",
}}
>
<ClipboardList size={18} style={{ color: "var(--accent)" }} />
</div>
<div>
<h3 style={{ fontFamily: "var(--font-body)", fontWeight: 600, fontSize: "16px", marginBottom: "4px" }}>
Officer Field Notes
</h3>
<p style={{ color: "var(--text-muted)", fontSize: "13px" }}>
Primary Due Diligence Integration — Field visit observations adjust the AI risk score
</p>
</div>
</div>
{/* Textarea */}
<textarea
className="input textarea"
style={{ fontFamily: "var(--font-mono)", fontSize: "13px", height: "160px", resize: "none" }}
placeholder={"Enter field visit observations...\ne.g. 'Factory operating at 40% capacity. Inventory appears aged and rusted.'"}
maxLength={2000}
value={notes}
onChange={(e) => setNotes(e.target.value)}
/>
{/* Character counter */}
<div style={{ textAlign: "right", fontSize: "11px", marginTop: "4px", color: charColor, fontFamily: "var(--font-mono)" }}>
{charCount} / 2000
</div>
{/* Quick-fill chips */}
<div className="flex flex-wrap gap-xs" style={{ marginTop: "12px" }}>
{QUICK_FILLS.map((chip) => (
<button
key={chip.label}
onClick={() => applyQuickFill(chip)}
style={{
background: "var(--bg-elevated)", border: "1px solid var(--border)",
borderRadius: "var(--radius-sm)", padding: "4px 10px",
fontSize: "11px", color: "var(--text-muted)", cursor: "pointer",
fontFamily: "var(--font-body)", transition: "all var(--transition-fast)",
}}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--accent)"; e.currentTarget.style.color = "var(--text-primary)"; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.color = "var(--text-muted)"; }}
>
{chip.label}
</button>
))}
</div>
{/* Submit button */}
<button
onClick={handleSubmit}
disabled={isSubmitting || !notes.trim()}
className="btn btn-primary w-full"
style={{ marginTop: "12px", padding: "14px" }}
>
{isSubmitting ? (
<span className="flex items-center justify-center gap-sm">
<Loader size={16} className="animate-spin" /> Processing...
</span>
) : "Submit Notes & Update Score"}
</button>
{/* Result display */}
{lastResult && (
<div style={{ marginTop: "20px" }}>
{lastResult.injection_detected ? (
<div
className="card animate-pulse"
style={{ background: "var(--danger-subtle)", borderColor: "rgba(239,68,68,0.4)", padding: "20px" }}
>
<div className="flex items-center gap-sm" style={{ marginBottom: "12px" }}>
<AlertTriangle size={20} style={{ color: "var(--danger)", flexShrink: 0 }} />
<p style={{ color: "var(--danger)", fontWeight: 700, fontSize: "15px" }}>
🚨 PROMPT INJECTION DETECTED
</p>
</div>
<p style={{ fontSize: "13px", marginBottom: "8px" }}>
The submitted note contained an instruction override attempt.
</p>
<p style={{ color: "var(--danger)", fontSize: "13px", marginBottom: "4px" }}>
Score penalty applied: -50 points
</p>
<p style={{ color: "var(--text-muted)", fontSize: "13px", marginBottom: "12px" }}>
Incident logged to compliance audit trail.
</p>
<pre
style={{
background: "var(--bg-elevated)", border: "1px solid rgba(239,68,68,0.3)",
borderRadius: "var(--radius-sm)", padding: "12px",
color: "var(--danger)", fontSize: "12px", fontFamily: "var(--font-mono)",
whiteSpace: "pre-wrap", wordBreak: "break-word",
}}
>
{notes}
</pre>
</div>
) : (
<div className="card" style={{ background: "var(--bg-elevated)", padding: "20px" }}>
{/* Score change */}
<div className="flex items-center gap-md flex-wrap" style={{ marginBottom: "16px" }}>
<div className="flex items-center gap-sm">
<span style={{ fontFamily: "var(--font-heading)", fontSize: "28px", fontWeight: 700, color: "var(--text-muted)", textDecoration: "line-through" }}>
{lastResult.score_before}
</span>
<span style={{ color: "var(--text-muted)", fontSize: "20px" }}></span>
<span style={{ fontFamily: "var(--font-heading)", fontSize: "36px", fontWeight: 700, color: scoreColor(lastResult.score_after) }}>
{lastResult.score_after}
</span>
</div>
{lastResult.score_delta !== 0 && (
<span
className={`badge ${lastResult.score_delta > 0 ? "badge-success" : "badge-danger"}`}
style={{ fontSize: "13px", fontWeight: 700, padding: "4px 12px" }}
>
{lastResult.score_delta > 0 ? "+" : ""}{lastResult.score_delta} pts
</span>
)}
</div>
{/* Decision change */}
{lastResult.decision_before !== lastResult.decision_after && (
<div className="flex items-center gap-sm" style={{ fontSize: "13px", marginBottom: "16px" }}>
<span style={{ color: "var(--text-muted)" }}>Decision:</span>
<span style={{ color: decisionColor(lastResult.decision_before), textDecoration: "line-through" }}>
{lastResult.decision_before}
</span>
<span style={{ color: "var(--text-muted)" }}></span>
<span style={{ color: decisionColor(lastResult.decision_after), fontWeight: 700 }}>
{lastResult.decision_after}
</span>
</div>
)}
{/* AI interpretation */}
{lastResult.interpretation && (
<p
style={{
color: "var(--text-muted)", fontSize: "13px", fontStyle: "italic",
lineHeight: 1.6, borderLeft: "2px solid var(--accent)", paddingLeft: "12px",
marginBottom: "16px",
}}
>
{lastResult.interpretation}
</p>
)}
{/* Adjustments list */}
{lastResult.adjustments && lastResult.adjustments.length > 0 && (
<div>
<span className="label" style={{ display: "block", marginBottom: "8px" }}>Score Adjustments Applied</span>
<div className="flex flex-col gap-xs">
{lastResult.adjustments.map((adj, idx) => (
<div key={idx} className="flex items-center gap-sm flex-wrap" style={{ fontSize: "13px" }}>
<span style={{ fontWeight: 600, minWidth: "120px", fontSize: "12px" }}>{adj.category}</span>
<span className={`badge ${adj.delta < 0 ? "badge-danger" : "badge-success"}`} style={{ fontSize: "11px" }}>
{adj.delta > 0 ? "+" : ""}{adj.delta} pts
</span>
<span style={{ color: "var(--text-muted)", fontSize: "12px" }}>{adj.reason}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
);
}