136 lines
5.4 KiB
JavaScript
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>
|
|
);
|
|
} |