Files
krowworkforce_controltower/src/components/krowcommand/CommandAnswer.jsx
2026-08-17 19:45:29 +05:30

136 lines
5.4 KiB
JavaScript

import React, { useState, useRef, useEffect } from "react";
import { Loader2, GitBranch, Cpu, Network } from "lucide-react";
import { buildContextForQuestion, synthesizeAnswer } from "@/lib/krowCommandEngine";
import { explainRouting } from "@/lib/engines/queryRouter";
import { getEngine } from "@/lib/engines/engineRegistry";
import CausalChainView from "@/components/krowcommand/CausalChainView";
export default function CommandAnswer({ data, question, onAskAnother }) {
const [answer, setAnswer] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [routing, setRouting] = useState(null);
const askedRef = useRef(null);
useEffect(() => {
if (!data || !question || askedRef.current === question) return;
askedRef.current = question;
// Show routing immediately (before LLM call) for transparency
setRouting(explainRouting(question));
const run = async () => {
setLoading(true);
setError(null);
try {
const context = buildContextForQuestion(question, data);
const result = await synthesizeAnswer(question, data, context);
setAnswer(result);
} catch (e) {
setError(e.message || "Failed to generate answer");
} finally {
setLoading(false);
}
};
run();
}, [data, question]);
const primaryEngine = routing?.primaryEngine || getEngine("operational_intelligence");
return (
<div className="bg-white rounded-2xl border border-slate-200 overflow-hidden">
{/* Header — shows which engine is handling this query */}
<div className="bg-gradient-to-r from-[#0039A6] to-[#1a56c4] px-5 py-4 flex items-center gap-3">
<div className="w-8 h-8 bg-white/20 rounded-lg flex items-center justify-center">
<GitBranch className="w-4 h-4 text-white" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[10px] text-blue-200 font-medium uppercase tracking-wide">KROW Command — Causal Reasoning</p>
<p className="text-sm font-semibold text-white truncate">{question}</p>
</div>
<button
onClick={onAskAnother}
className="text-xs text-blue-200 hover:text-white underline"
>
Ask another
</button>
</div>
{/* Engine attribution bar — shows which engine(s) are reasoning */}
{routing && (
<div className="bg-slate-50 border-b border-slate-200 px-5 py-2.5 flex items-center gap-2 flex-wrap">
<Cpu className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
<div className="flex items-center gap-1.5">
<span
className="text-[11px] font-bold px-2 py-0.5 rounded text-white"
style={{ backgroundColor: primaryEngine.color }}
>
{primaryEngine.number}. {primaryEngine.shortName}
</span>
{routing.secondaryEngines?.map(e => (
<span
key={e.key}
className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-slate-200 text-slate-600"
>
+ {e.shortName}
</span>
))}
</div>
<span className="text-[11px] text-slate-400 ml-1">{routing.explanation}</span>
</div>
)}
<div className="p-5">
{loading && (
<div className="flex flex-col items-center gap-3 py-10">
<Loader2 className="w-6 h-6 text-[#0039A6] animate-spin" />
<div className="text-center">
<p className="text-sm font-medium text-slate-700">
{primaryEngine.name} is reasoning…
</p>
<p className="text-xs text-slate-400 mt-1">
Building the Digital Twin and tracing the causal chain to root cause(s).
</p>
</div>
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm text-red-700 font-medium">Unable to generate reasoning</p>
<p className="text-xs text-red-600 mt-1">{error}</p>
</div>
)}
{!loading && !error && answer && (
<>
<CausalChainView
observation={answer.observation}
causalChain={answer.causal_chain}
rootCauses={answer.root_causes}
recommendation={answer.recommendation}
/>
{/* Engine footer — shows attribution and twin snapshot */}
{answer._engineName && (
<div className="mt-4 pt-4 border-t border-slate-100 flex items-center gap-2 flex-wrap text-[11px] text-slate-400">
<Network className="w-3 h-3" />
<span>Reasoned by <strong className="text-slate-600">{answer._engineName}</strong></span>
{answer._twinHealth && (
<span className="ml-2">
Digital Twin health: <strong className="text-slate-600">{answer._twinHealth.overall}/100</strong> ({answer._twinHealth.status})
</span>
)}
{answer.confidence_score != null && (
<span className="ml-2">
Confidence: <strong className="text-slate-600">{answer.confidence_score}%</strong>
</span>
)}
</div>
)}
</>
)}
</div>
</div>
);
}