Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/missionmatch/Ask.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/missionmatch/Ask.tsx |
| Original SHA-256 | 6795dda527a89cd5df87b213dccb5abd97b5d479d9f36e87fa33c9499eaae8fb |
| Git blob SHA | 849a5d4a6c1c1a50b7db7395aade78038db522ce |
| Reading-copy SHA-256 | 6795dda527a89cd5df87b213dccb5abd97b5d479d9f36e87fa33c9499eaae8fb |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / frontend/src/tools/missionmatch/Ask.tsx | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / frontend/src/tools/missionmatch/Ask.tsx | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓205 lines · 8.1 KB
import { useState } from 'react'
import { BrainCircuit, BookOpen, CornerDownLeft, Info, Loader2, Sparkles } from 'lucide-react'
import { missionApi } from './api'
import { missionBrand } from './brand'
import { ApiError } from '../../lib/api'
import { Badge, Panel } from '../../components/ui'
import { ToolHeader } from '../../components/ToolHeader'
import type { AskAnswer } from '../../lib/types'
// Example prompts map to real registered intents and the seeded demo data, so the first click always
// returns a grounded answer. Clicking a chip fills the input; the user still presses Ask to send it.
const EXAMPLES = [
'How many candidates hold a TS/SCI clearance?',
'What are our hardest staffing gaps right now?',
'Who can support the Intelligence Analyst mission?',
'What is the win probability on the Air Force segment?',
'Show the clearance coverage of our bench.',
'Which vendors perform best on our submissions?',
]
// pathLabel names each retrieval route in plain words for the answer badge.
const PATH_LABEL: Record<AskAnswer['path'], string> = {
cag: 'Closed answer',
tag: 'Tool answer',
rag: 'Retrieval',
graph: 'Graph walk',
}
// The Ask panel is the UI over the closed Registry AI brain. The brain answers only from the tenant's
// own cleared data, cites its evidence, and refuses questions about protected attributes or out of scope
// questions. A refusal is a normal outcome the panel renders calmly, never an error.
export function Ask() {
const [question, setQuestion] = useState('')
const [loading, setLoading] = useState(false)
const [answer, setAnswer] = useState<AskAnswer | null>(null)
// A refusal is the brain's calm decline (HTTP 422). errorLine is a generic friendly line for any other
// failure. The two are kept separate so a refusal never reads as a crash.
const [refusal, setRefusal] = useState<string | null>(null)
const [errorLine, setErrorLine] = useState<string | null>(null)
const trimmed = question.trim()
const canAsk = trimmed.length > 0 && !loading
async function submit() {
if (!canAsk) return
setLoading(true)
setAnswer(null)
setRefusal(null)
setErrorLine(null)
try {
const res = await missionApi.ask(trimmed)
setAnswer(res)
} catch (err) {
// A refused question returns 422 with an error body. Treat it as an expected, calm decline.
if (err instanceof ApiError && err.status === 422) {
setRefusal(err.message || 'That question is out of scope for the mission brain.')
} else {
setErrorLine('Something went wrong reaching the mission brain. Please try again.')
}
} finally {
setLoading(false)
}
}
function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
// Enter with a modifier sends, so a plain Enter can still add a newline in a longer question.
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
void submit()
}
}
return (
<div className="mx-auto max-w-3xl space-y-4">
<ToolHeader name={missionBrand.name} tagline={missionBrand.tagline} accent={missionBrand.accent} />
<div className="flex items-center gap-2">
<BrainCircuit size={18} className="text-gold" />
<h2 className="text-lg font-bold tracking-tight">Ask the Brain™</h2>
</div>
<p className="text-sm text-muted">
This is the grounded mission brain. It answers from your tenant's own cleared data and cites the
evidence behind every answer.
</p>
<Panel>
<label htmlFor="ask-input" className="sr-only">
Ask the mission brain a question
</label>
<textarea
id="ask-input"
value={question}
onChange={(e) => setQuestion(e.target.value)}
onKeyDown={onKeyDown}
rows={3}
placeholder="Ask about your bench, your gaps, your win probability, or your incumbents."
className="w-full resize-y rounded-lg border border-line/70 bg-navy-950/60 px-3 py-2.5 text-sm text-ink outline-none placeholder:text-faint focus:border-blue/70"
/>
<div className="mt-2 flex items-center justify-between gap-3">
<span className="text-[11px] text-faint">
Press Ask, or Command plus Enter, to send.
</span>
<button
onClick={() => void submit()}
disabled={!canAsk}
className="flex items-center gap-2 rounded-lg bg-gradient-to-r from-blue to-navy-700 px-4 py-2 text-sm font-bold text-white hover:brightness-110 disabled:opacity-50"
>
{loading ? <Loader2 size={15} className="animate-spin" /> : <CornerDownLeft size={15} />}
{loading ? 'Thinking…' : 'Ask'}
</button>
</div>
{/* Example prompt chips, mapped to real registered intents and the seeded demo data. */}
<div className="mt-3 flex flex-wrap gap-1.5 border-t border-line/50 pt-3">
<span className="mr-1 flex items-center gap-1 text-[11px] font-semibold uppercase tracking-wide text-faint">
<Sparkles size={12} /> Try
</span>
{EXAMPLES.map((ex) => (
<button
key={ex}
type="button"
onClick={() => setQuestion(ex)}
disabled={loading}
className="rounded-full border border-line/70 bg-navy-800/50 px-3 py-1 text-[11px] text-blue-soft hover:border-blue/60 hover:text-ink disabled:opacity-50"
>
{ex}
</button>
))}
</div>
</Panel>
{loading && (
<Panel className="grid place-items-center py-10 text-muted">
<Loader2 size={22} className="animate-spin" />
<span className="mt-2 text-sm">Grounding the answer in your cleared data…</span>
</Panel>
)}
{refusal && !loading && (
<Panel>
<div className="flex items-start gap-2.5 text-sm text-muted">
<Info size={16} className="mt-0.5 shrink-0 text-blue-soft" />
<div>
<div className="font-semibold text-ink">The mission brain declined this question.</div>
<p className="mt-0.5">{refusal}</p>
</div>
</div>
</Panel>
)}
{errorLine && !loading && (
<Panel>
<p className="text-sm text-warn">{errorLine}</p>
</Panel>
)}
{answer && !loading && <AnswerCard answer={answer} />}
</div>
)
}
// AnswerCard renders the grounded answer prose, a labeled route badge and a faithful chip, the evidence
// count, and the citations the brain grounded on. When there are no citations the grounding section is
// omitted rather than shown empty.
function AnswerCard({ answer }: { answer: AskAnswer }) {
return (
<Panel>
<div className="flex flex-wrap items-center gap-2">
<Badge color="var(--color-blue-soft)" subtle>
{PATH_LABEL[answer.path]}
</Badge>
<Badge color={answer.faithful ? 'var(--color-gold)' : 'var(--color-warn)'} subtle>
{answer.faithful ? 'Faithful to evidence' : 'Not fully grounded'}
</Badge>
<span className="text-[11px] text-faint">
{answer.evidence_count} evidence {answer.evidence_count === 1 ? 'row' : 'rows'}
</span>
</div>
<p className="mt-3 whitespace-pre-wrap text-sm leading-relaxed text-ink">{answer.answer}</p>
{answer.fallback_reason && (
<p className="mt-2 text-[11px] text-faint">Note: {answer.fallback_reason}</p>
)}
{answer.citations.length > 0 && (
<div className="mt-4 border-t border-line/50 pt-3">
<div className="mb-1.5 flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-wider text-muted">
<BookOpen size={13} /> Grounded on
</div>
<ul className="space-y-1">
{/* Keyed on the citation itself, not its position. The whole answer is replaced on every
ask, so an index key lets React reuse the previous answer's row for a different source. */}
{answer.citations.map((c) => (
<li key={c} className="font-mono text-[11px] text-faint">
{c}
</li>
))}
</ul>
</div>
)}
</Panel>
)
}