TechScout/dashboard/app/page.tsx

1476 lines
54 KiB
TypeScript

'use client';
import { useState } from 'react';
import { FiSearch, FiLoader, FiZap, FiTarget, FiDatabase, FiFileText, FiCheckCircle, FiAlertCircle, FiHelpCircle, FiMinusCircle, FiChevronRight, FiRefreshCw, FiCheck } from 'react-icons/fi';
import type {
DiscoveryResult,
TechnologyCandidate,
DeepDiveResult,
Technology,
WorkflowStep,
StepSearchResult,
StepExtractionResult,
StepEvaluationResult,
TechnologyItem,
SearchResultItem,
} from './types';
type SearchMode = 'guided' | 'discover';
export default function Home() {
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false);
const [loadingStep, setLoadingStep] = useState<string>('');
const [searchMode, setSearchMode] = useState<SearchMode>('guided');
// Workflow state
const [workflowStep, setWorkflowStep] = useState<WorkflowStep>('input');
// Step results
const [searchResult, setSearchResult] = useState<StepSearchResult | null>(null);
const [extractionResult, setExtractionResult] = useState<StepExtractionResult | null>(null);
const [evaluationResult, setEvaluationResult] = useState<StepEvaluationResult | null>(null);
// Selection state for step 3
const [selectedTechIds, setSelectedTechIds] = useState<Set<string>>(new Set());
// Legacy discovery state
const [result, setResult] = useState<DiscoveryResult | null>(null);
const [selectedCandidate, setSelectedCandidate] = useState<TechnologyCandidate | null>(null);
// Detail view state
const [selectedTechnology, setSelectedTechnology] = useState<Technology | null>(null);
// Deep dive state (shared)
const [deepDiveLoading, setDeepDiveLoading] = useState(false);
const [deepDiveResult, setDeepDiveResult] = useState<DeepDiveResult | null>(null);
// Reset workflow
const resetWorkflow = () => {
setWorkflowStep('input');
setSearchResult(null);
setExtractionResult(null);
setEvaluationResult(null);
setSelectedTechIds(new Set());
setSelectedTechnology(null);
};
// Step 1: Search
const handleStepSearch = async () => {
if (!query.trim()) return;
setLoading(true);
setLoadingStep('Parsing capability need and searching sources...');
resetWorkflow();
try {
const response = await fetch('http://localhost:8000/api/match/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Search failed');
}
const data = await response.json();
if (data.result) {
setSearchResult(data.result);
setWorkflowStep('search');
}
} catch (error) {
console.error('Search failed:', error);
if (error instanceof TypeError && error.message.includes('fetch')) {
alert('Cannot connect to API server. Make sure to run launch.bat first!');
} else {
alert(`Search failed: ${error}`);
}
} finally {
setLoading(false);
setLoadingStep('');
}
};
// Step 2: Extract
const handleStepExtract = async () => {
if (!searchResult) return;
setLoading(true);
setLoadingStep('Extracting and grouping technologies...');
try {
const response = await fetch('http://localhost:8000/api/match/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ search_id: searchResult.id }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Extraction failed');
}
const data = await response.json();
if (data.result) {
setExtractionResult(data.result);
setWorkflowStep('extract');
// Pre-select all technologies by default
setSelectedTechIds(new Set(data.result.technologies.map((t: TechnologyItem) => t.id)));
}
} catch (error) {
console.error('Extraction failed:', error);
alert(`Extraction failed: ${error}`);
} finally {
setLoading(false);
setLoadingStep('');
}
};
// Step 3: Evaluate
const handleStepEvaluate = async () => {
if (!extractionResult || selectedTechIds.size === 0) return;
setLoading(true);
setLoadingStep(`Evaluating ${selectedTechIds.size} technologies against criteria...`);
try {
const response = await fetch('http://localhost:8000/api/match/evaluate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
extraction_id: extractionResult.id,
technology_ids: Array.from(selectedTechIds),
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Evaluation failed');
}
const data = await response.json();
if (data.result) {
setEvaluationResult(data.result);
setWorkflowStep('evaluate');
}
} catch (error) {
console.error('Evaluation failed:', error);
alert(`Evaluation failed: ${error}`);
} finally {
setLoading(false);
setLoadingStep('');
}
};
// Legacy discovery
const handleDiscover = async () => {
if (!query.trim()) return;
setLoading(true);
setLoadingStep('Starting discovery...');
setResult(null);
setSelectedCandidate(null);
try {
const response = await fetch('http://localhost:8000/api/discover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Discovery failed');
}
const data = await response.json();
if (data.result) {
setResult(data.result);
}
} catch (error) {
console.error('Discovery failed:', error);
alert(`Discovery failed: ${error}`);
} finally {
setLoading(false);
setLoadingStep('');
}
};
const handleDeepDive = async (candidate: TechnologyCandidate) => {
if (!candidate || !query) return;
setDeepDiveLoading(true);
setDeepDiveResult(null);
try {
const response = await fetch('http://localhost:8000/api/deepdive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
organization: candidate.organization,
technology: candidate.description,
gap: query,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Deep dive failed');
}
const data = await response.json();
if (data.result) {
setDeepDiveResult(data.result);
}
} catch (error) {
console.error('Deep dive failed:', error);
alert(`Deep dive failed: ${error}`);
} finally {
setDeepDiveLoading(false);
}
};
const toggleTechSelection = (id: string) => {
const newSelected = new Set(selectedTechIds);
if (newSelected.has(id)) {
newSelected.delete(id);
} else {
newSelected.add(id);
}
setSelectedTechIds(newSelected);
};
const selectAllTechs = () => {
if (extractionResult) {
setSelectedTechIds(new Set(extractionResult.technologies.map(t => t.id)));
}
};
const deselectAllTechs = () => {
setSelectedTechIds(new Set());
};
const exampleQueries = [
"Need a technology that improves Space Domain Awareness for objects beyond the diffraction limit",
"Looking for autonomous underwater vehicle navigation in GPS-denied environments",
"Require hypersonic threat detection and tracking capabilities for missile defense",
"Need secure tactical communications in contested electromagnetic environments",
];
return (
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-4xl font-bold mb-2">
<span className="gradient-text">Technology Scout</span>
</h1>
<p className="text-gray-400 text-lg">
Describe your capability need in natural language. TechScout will find technologies that could address it.
</p>
</div>
{/* Query Input */}
<div className="bg-[#12121a] rounded-2xl border border-[#1e1e2e] p-6 mb-8">
{/* Mode Toggle */}
<div className="flex gap-2 mb-4">
<button
onClick={() => { setSearchMode('guided'); resetWorkflow(); setResult(null); }}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
searchMode === 'guided'
? 'bg-gradient-to-r from-[#00d4ff] to-[#7c3aed] text-white'
: 'bg-[#1e1e2e] text-gray-400 hover:text-white'
}`}
>
<FiTarget className="inline mr-2" />
Guided Match (3-Step)
</button>
<button
onClick={() => { setSearchMode('discover'); resetWorkflow(); setResult(null); }}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
searchMode === 'discover'
? 'bg-gradient-to-r from-[#00d4ff] to-[#7c3aed] text-white'
: 'bg-[#1e1e2e] text-gray-400 hover:text-white'
}`}
>
<FiSearch className="inline mr-2" />
Discovery (Legacy)
</button>
</div>
<label className="block text-sm text-gray-400 mb-2">
Describe Your Capability Need
</label>
<textarea
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="e.g., Need a technology that improves Space Domain Awareness for objects beyond the diffraction limit"
className="w-full h-32 bg-[#0a0a12] border border-[#1e1e2e] rounded-xl p-4 text-white placeholder-gray-600 focus:border-[#00d4ff] focus:outline-none resize-none"
disabled={loading}
/>
{/* Example Queries */}
<div className="mt-4">
<p className="text-xs text-gray-500 mb-2">Example queries:</p>
<div className="flex flex-wrap gap-2">
{exampleQueries.map((eq, i) => (
<button
key={i}
onClick={() => setQuery(eq)}
disabled={loading}
className="text-xs px-3 py-1 bg-[#1e1e2e] text-gray-400 rounded-full hover:text-[#00d4ff] hover:border-[#00d4ff] border border-transparent transition-colors disabled:opacity-50"
>
{eq.slice(0, 40)}...
</button>
))}
</div>
</div>
{/* Search Button */}
<button
onClick={searchMode === 'guided' ? handleStepSearch : handleDiscover}
disabled={loading || !query.trim()}
className={`mt-6 w-full py-4 rounded-xl font-semibold text-lg flex items-center justify-center gap-3 transition-all ${
loading || !query.trim()
? 'bg-[#1e1e2e] text-gray-500 cursor-not-allowed'
: 'bg-gradient-to-r from-[#00d4ff] to-[#7c3aed] text-white hover:opacity-90 glow-accent'
}`}
>
{loading ? (
<>
<FiLoader className="animate-spin" />
{loadingStep}
</>
) : (
<>
<FiSearch />
{searchMode === 'guided' ? 'Step 1: Search Sources' : 'Discover Technologies'}
</>
)}
</button>
</div>
{/* Guided Workflow */}
{searchMode === 'guided' && workflowStep !== 'input' && (
<div className="animate-fadeIn">
{/* Workflow Progress */}
<div className="flex items-center gap-2 mb-6">
<WorkflowStepIndicator
step={1}
label="Search"
isActive={workflowStep === 'search'}
isComplete={workflowStep === 'extract' || workflowStep === 'evaluate'}
/>
<FiChevronRight className="text-gray-600" />
<WorkflowStepIndicator
step={2}
label="Extract"
isActive={workflowStep === 'extract'}
isComplete={workflowStep === 'evaluate'}
/>
<FiChevronRight className="text-gray-600" />
<WorkflowStepIndicator
step={3}
label="Evaluate"
isActive={workflowStep === 'evaluate'}
isComplete={false}
/>
<div className="ml-auto">
<button
onClick={resetWorkflow}
className="px-4 py-2 bg-[#1e1e2e] rounded-lg text-sm text-gray-400 hover:text-white flex items-center gap-2"
>
<FiRefreshCw /> Start Over
</button>
</div>
</div>
{/* Guidance Panel */}
<GuidancePanel
step={workflowStep}
searchResult={searchResult}
extractionResult={extractionResult}
evaluationResult={evaluationResult}
loading={loading}
onExtract={handleStepExtract}
onEvaluate={handleStepEvaluate}
selectedCount={selectedTechIds.size}
/>
{/* Step 1: Search Results */}
{workflowStep === 'search' && searchResult && (
<SearchResultsPanel
result={searchResult}
/>
)}
{/* Step 2: Technology Selection */}
{workflowStep === 'extract' && extractionResult && (
<TechnologySelectionPanel
result={extractionResult}
selectedIds={selectedTechIds}
onToggle={toggleTechSelection}
onSelectAll={selectAllTechs}
onDeselectAll={deselectAllTechs}
/>
)}
{/* Step 3: Evaluation Results */}
{workflowStep === 'evaluate' && evaluationResult && (
<EvaluationResultsPanel
result={evaluationResult}
searchResult={searchResult}
selectedTechnology={selectedTechnology}
onSelectTechnology={setSelectedTechnology}
onDeepDive={handleDeepDive}
deepDiveLoading={deepDiveLoading}
query={query}
/>
)}
</div>
)}
{/* Legacy Discovery Results */}
{result && searchMode === 'discover' && (
<LegacyDiscoveryResults
result={result}
selectedCandidate={selectedCandidate}
onSelectCandidate={setSelectedCandidate}
onDeepDive={handleDeepDive}
deepDiveLoading={deepDiveLoading}
/>
)}
{/* Empty State */}
{!result && workflowStep === 'input' && !loading && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<FeatureCard
icon={<FiDatabase className="text-[#00d4ff]" />}
title="Multi-Source Search"
description="Searches SBIR/STTR awards, USPTO patents, federal contracts, and defense news"
/>
<FeatureCard
icon={<FiZap className="text-[#7c3aed]" />}
title="AI-Powered Analysis"
description="Extracts technologies, groups duplicates, and evaluates capability fit"
/>
<FeatureCard
icon={<FiTarget className="text-[#22c55e]" />}
title="Guided Workflow"
description="Step-by-step process with review points - search, extract, then evaluate"
/>
</div>
)}
{/* Deep Dive Modal */}
{deepDiveResult && (
<DeepDiveModal
result={deepDiveResult}
onClose={() => setDeepDiveResult(null)}
/>
)}
</div>
);
}
// Workflow Step Indicator
function WorkflowStepIndicator({
step,
label,
isActive,
isComplete
}: {
step: number;
label: string;
isActive: boolean;
isComplete: boolean;
}) {
return (
<div className={`flex items-center gap-2 px-4 py-2 rounded-lg ${
isActive ? 'bg-[#00d4ff]/20 border border-[#00d4ff]/50' :
isComplete ? 'bg-[#22c55e]/20 border border-[#22c55e]/50' :
'bg-[#1e1e2e] border border-[#1e1e2e]'
}`}>
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold ${
isActive ? 'bg-[#00d4ff] text-black' :
isComplete ? 'bg-[#22c55e] text-black' :
'bg-[#2e2e3e] text-gray-400'
}`}>
{isComplete ? <FiCheck /> : step}
</div>
<span className={`text-sm font-medium ${
isActive ? 'text-[#00d4ff]' :
isComplete ? 'text-[#22c55e]' :
'text-gray-400'
}`}>
{label}
</span>
</div>
);
}
// Guidance Panel
function GuidancePanel({
step,
searchResult,
extractionResult,
evaluationResult,
loading,
onExtract,
onEvaluate,
selectedCount
}: {
step: WorkflowStep;
searchResult: StepSearchResult | null;
extractionResult: StepExtractionResult | null;
evaluationResult: StepEvaluationResult | null;
loading: boolean;
onExtract: () => void;
onEvaluate: () => void;
selectedCount: number;
}) {
const getMessage = () => {
if (step === 'search' && searchResult) {
return searchResult.guidance_message;
}
if (step === 'extract' && extractionResult) {
return extractionResult.guidance_message;
}
if (step === 'evaluate' && evaluationResult) {
return evaluationResult.guidance_message;
}
return '';
};
const renderMarkdown = (text: string) => {
// Simple markdown-like rendering for **bold**
return text.split(/(\*\*[^*]+\*\*)/).map((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={i} className="text-white">{part.slice(2, -2)}</strong>;
}
return part;
});
};
return (
<div className="bg-gradient-to-r from-[#00d4ff]/10 to-[#7c3aed]/10 rounded-2xl border border-[#00d4ff]/30 p-6 mb-6">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<FiZap className="text-[#00d4ff]" />
<span className="text-sm font-semibold text-[#00d4ff]">
{step === 'search' ? 'Step 1: Review Search Results' :
step === 'extract' ? 'Step 2: Select Technologies' :
'Step 3: Review Evaluations'}
</span>
</div>
<p className="text-gray-300">
{renderMarkdown(getMessage())}
</p>
</div>
{/* Action Button */}
{step === 'search' && searchResult && searchResult.search_results.length > 0 && (
<button
onClick={onExtract}
disabled={loading}
className="px-6 py-3 bg-gradient-to-r from-[#00d4ff] to-[#7c3aed] rounded-xl font-semibold text-white hover:opacity-90 transition-opacity disabled:opacity-50 flex items-center gap-2 whitespace-nowrap"
>
{loading ? <FiLoader className="animate-spin" /> : <FiChevronRight />}
Extract Technologies
</button>
)}
{step === 'extract' && extractionResult && extractionResult.technologies.length > 0 && (
<button
onClick={onEvaluate}
disabled={loading || selectedCount === 0}
className="px-6 py-3 bg-gradient-to-r from-[#00d4ff] to-[#7c3aed] rounded-xl font-semibold text-white hover:opacity-90 transition-opacity disabled:opacity-50 flex items-center gap-2 whitespace-nowrap"
>
{loading ? <FiLoader className="animate-spin" /> : <FiChevronRight />}
Evaluate {selectedCount} Selected
</button>
)}
</div>
</div>
);
}
// Search Results Panel (Step 1)
function SearchResultsPanel({ result }: { result: StepSearchResult }) {
return (
<div>
{/* Capability Analysis */}
{result.capability_need && (
<div className="bg-[#12121a] rounded-2xl border border-[#1e1e2e] p-6 mb-6">
<h2 className="text-lg font-semibold text-[#00d4ff] mb-3 flex items-center gap-2">
<FiZap /> Parsed Capability Need
</h2>
<p className="text-gray-300 mb-4">{result.capability_need.functional_need}</p>
<div className="flex flex-wrap gap-2 mb-4">
<span className="px-3 py-1 bg-[#7c3aed]/20 text-[#7c3aed] rounded-full text-sm">
{result.capability_need.domain}
</span>
{result.capability_need.technology_types_sought.map((type, i) => (
<span key={i} className="px-3 py-1 bg-[#1e1e2e] text-gray-400 rounded-full text-sm">
{type}
</span>
))}
</div>
{/* Evaluation Criteria */}
{result.capability_criteria.length > 0 && (
<div className="mb-4">
<p className="text-xs text-gray-500 mb-2">Evaluation Criteria:</p>
<div className="flex flex-wrap gap-2">
{result.capability_criteria.map((criterion, i) => (
<span
key={i}
className={`px-3 py-1 rounded-full text-sm border ${
criterion.weight === 'must_have'
? 'bg-[#ef4444]/10 text-[#ef4444] border-[#ef4444]/30'
: criterion.weight === 'should_have'
? 'bg-[#f59e0b]/10 text-[#f59e0b] border-[#f59e0b]/30'
: 'bg-[#22c55e]/10 text-[#22c55e] border-[#22c55e]/30'
}`}
title={criterion.criterion}
>
{criterion.weight === 'must_have' ? '●' : criterion.weight === 'should_have' ? '○' : '◌'}{' '}
{criterion.criterion.slice(0, 40)}{criterion.criterion.length > 40 ? '...' : ''}
</span>
))}
</div>
</div>
)}
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#00d4ff]">{result.search_results.length}</div>
<div className="text-xs text-gray-500">Results Found</div>
</div>
{Object.entries(result.source_counts).slice(0, 3).map(([source, count]) => (
<div key={source} className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-gray-400">{count}</div>
<div className="text-xs text-gray-500 capitalize">{source}</div>
</div>
))}
</div>
</div>
)}
{/* Search Results List */}
<div className="bg-[#12121a] rounded-2xl border border-[#1e1e2e] p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<FiFileText className="text-[#00d4ff]" />
Search Results ({result.search_results.length})
</h2>
<div className="space-y-3 max-h-[500px] overflow-y-auto">
{result.search_results.map((item, i) => (
<SearchResultCard key={item.id} item={item} rank={i + 1} />
))}
</div>
</div>
</div>
);
}
function SearchResultCard({ item, rank }: { item: SearchResultItem; rank: number }) {
return (
<a
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="block bg-[#0a0a12] rounded-xl border border-[#1e1e2e] p-4 hover:border-[#2e2e3e] transition-colors"
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-gray-500">#{rank}</span>
<span className={`text-xs px-2 py-0.5 rounded ${
item.source_type === 'sbir' ? 'bg-[#22c55e]/20 text-[#22c55e]' :
item.source_type === 'patent' ? 'bg-[#f59e0b]/20 text-[#f59e0b]' :
item.source_type === 'contract' ? 'bg-[#00d4ff]/20 text-[#00d4ff]' :
'bg-[#1e1e2e] text-gray-400'
}`}>
{item.source_type}
</span>
</div>
{item.organization && (
<span className="text-xs text-[#7c3aed]">{item.organization}</span>
)}
</div>
<h3 className="font-medium text-white mb-1 line-clamp-2">{item.title}</h3>
<p className="text-sm text-gray-500 line-clamp-2">{item.snippet}</p>
</a>
);
}
// Technology Selection Panel (Step 2)
function TechnologySelectionPanel({
result,
selectedIds,
onToggle,
onSelectAll,
onDeselectAll
}: {
result: StepExtractionResult;
selectedIds: Set<string>;
onToggle: (id: string) => void;
onSelectAll: () => void;
onDeselectAll: () => void;
}) {
return (
<div className="bg-[#12121a] rounded-2xl border border-[#1e1e2e] p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<FiTarget className="text-[#00d4ff]" />
Extracted Technologies ({result.technologies.length})
</h2>
<div className="flex gap-2">
<button
onClick={onSelectAll}
className="px-3 py-1 bg-[#1e1e2e] rounded text-sm text-gray-400 hover:text-white"
>
Select All
</button>
<button
onClick={onDeselectAll}
className="px-3 py-1 bg-[#1e1e2e] rounded text-sm text-gray-400 hover:text-white"
>
Deselect All
</button>
</div>
</div>
<p className="text-sm text-gray-500 mb-4">
{selectedIds.size} of {result.technologies.length} selected for evaluation
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{result.technologies.map((tech) => (
<TechnologySelectCard
key={tech.id}
technology={tech}
isSelected={selectedIds.has(tech.id)}
onToggle={() => onToggle(tech.id)}
/>
))}
</div>
</div>
);
}
function TechnologySelectCard({
technology,
isSelected,
onToggle
}: {
technology: TechnologyItem;
isSelected: boolean;
onToggle: () => void;
}) {
return (
<div
onClick={onToggle}
className={`bg-[#0a0a12] rounded-xl border p-4 cursor-pointer transition-all ${
isSelected
? 'border-[#00d4ff] bg-[#00d4ff]/5'
: 'border-[#1e1e2e] hover:border-[#2e2e3e]'
}`}
>
<div className="flex items-start gap-3">
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 mt-1 ${
isSelected ? 'bg-[#00d4ff] border-[#00d4ff]' : 'border-gray-500'
}`}>
{isSelected && <FiCheck className="text-black text-sm" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs px-2 py-0.5 rounded bg-[#1e1e2e] text-gray-400">
{technology.technology_type}
</span>
{technology.trl_estimate && (
<span className="text-xs text-gray-500">TRL {technology.trl_estimate}</span>
)}
</div>
<h3 className="font-medium text-white mb-1">{technology.canonical_name}</h3>
<p className="text-sm text-gray-500 line-clamp-2">{technology.description}</p>
{technology.developers.length > 0 && (
<p className="text-xs text-[#7c3aed] mt-2">
{technology.developers.map(d => d.name).join(', ')}
</p>
)}
<div className="flex items-center gap-2 mt-2">
<FiFileText className="text-gray-500 text-xs" />
<span className="text-xs text-gray-500">{technology.source_count} sources</span>
</div>
</div>
</div>
</div>
);
}
// Evaluation Results Panel (Step 3)
function EvaluationResultsPanel({
result,
searchResult,
selectedTechnology,
onSelectTechnology,
onDeepDive,
deepDiveLoading,
query
}: {
result: StepEvaluationResult;
searchResult: StepSearchResult | null;
selectedTechnology: Technology | null;
onSelectTechnology: (tech: Technology | null) => void;
onDeepDive: (candidate: TechnologyCandidate) => void;
deepDiveLoading: boolean;
query: string;
}) {
return (
<div>
{/* Summary Stats */}
<div className="bg-[#12121a] rounded-2xl border border-[#1e1e2e] p-6 mb-6">
<h2 className="text-lg font-semibold text-[#00d4ff] mb-4 flex items-center gap-2">
<FiTarget /> Evaluation Summary
</h2>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-center">
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#00d4ff]">{result.summary.total_technologies}</div>
<div className="text-xs text-gray-500">Evaluated</div>
</div>
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#22c55e]">{result.summary.high_fit_count}</div>
<div className="text-xs text-gray-500">High Fit</div>
</div>
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#f59e0b]">{result.summary.medium_fit_count}</div>
<div className="text-xs text-gray-500">Medium Fit</div>
</div>
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#ef4444]">{result.summary.low_fit_count}</div>
<div className="text-xs text-gray-500">Low Fit</div>
</div>
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-gray-400">{result.processing_time_seconds.toFixed(1)}s</div>
<div className="text-xs text-gray-500">Time</div>
</div>
</div>
</div>
{/* Technology List */}
<div className="flex gap-6">
<div className="flex-1">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<FiTarget className="text-[#00d4ff]" />
Evaluated Technologies
</h2>
<div className="space-y-3">
{result.technologies.map((tech, i) => (
<TechnologyCard
key={tech.id}
technology={tech}
rank={i + 1}
isSelected={selectedTechnology?.id === tech.id}
onClick={() => onSelectTechnology(tech)}
/>
))}
</div>
</div>
{/* Selected Technology Detail */}
{selectedTechnology && (
<TechnologyDetailPanel
technology={selectedTechnology}
onDeepDive={() => {
const developer = selectedTechnology.developers?.[0]?.name;
if (developer && developer !== 'Unknown') {
onDeepDive({
id: selectedTechnology.id,
title: selectedTechnology.canonical_name,
organization: developer,
description: selectedTechnology.description,
source_type: selectedTechnology.sources?.[0]?.source_type || 'unknown',
source: selectedTechnology.sources?.[0]?.source_name || 'unknown',
url: selectedTechnology.sources?.[0]?.url || '',
score: selectedTechnology.capability_match.fit_score / 100,
relevance_score: selectedTechnology.capability_match.fit_score / 100,
trl_estimate: selectedTechnology.trl_estimate,
award_amount: null,
published_date: null,
award_id: null,
patent_number: null,
});
}
}}
deepDiveLoading={deepDiveLoading}
/>
)}
</div>
</div>
);
}
// Legacy Discovery Results
function LegacyDiscoveryResults({
result,
selectedCandidate,
onSelectCandidate,
onDeepDive,
deepDiveLoading
}: {
result: DiscoveryResult;
selectedCandidate: TechnologyCandidate | null;
onSelectCandidate: (c: TechnologyCandidate) => void;
onDeepDive: (c: TechnologyCandidate) => void;
deepDiveLoading: boolean;
}) {
return (
<div className="animate-fadeIn">
{/* Query Understanding */}
<div className="bg-[#12121a] rounded-2xl border border-[#1e1e2e] p-6 mb-6">
<h2 className="text-lg font-semibold text-[#00d4ff] mb-3 flex items-center gap-2">
<FiZap /> Query Analysis
</h2>
<p className="text-gray-300 mb-4">{result.decomposition.understanding}</p>
<div className="flex flex-wrap gap-2 mb-4">
{result.decomposition.technical_domains.map((domain, i) => (
<span
key={i}
className="px-3 py-1 bg-[#7c3aed]/20 text-[#7c3aed] rounded-full text-sm border border-[#7c3aed]/30"
>
{domain}
</span>
))}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#00d4ff]">{result.candidates.length}</div>
<div className="text-xs text-gray-500">Candidates</div>
</div>
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#22c55e]">{result.source_stats.sbir || 0}</div>
<div className="text-xs text-gray-500">SBIR Awards</div>
</div>
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#f59e0b]">{result.source_stats.patents || 0}</div>
<div className="text-xs text-gray-500">Patents</div>
</div>
<div className="bg-[#0a0a12] rounded-lg p-3">
<div className="text-2xl font-bold text-[#ef4444]">{result.search_duration_seconds.toFixed(1)}s</div>
<div className="text-xs text-gray-500">Search Time</div>
</div>
</div>
</div>
{/* Candidates Grid */}
<div className="flex gap-6">
{/* Candidate List */}
<div className="flex-1">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<FiTarget className="text-[#00d4ff]" />
Technology Candidates
</h2>
<div className="space-y-3">
{result.candidates.slice(0, 20).map((candidate, i) => (
<div
key={candidate.id}
onClick={() => onSelectCandidate(candidate)}
className={`bg-[#12121a] rounded-xl border p-4 cursor-pointer transition-all ${
selectedCandidate?.id === candidate.id
? 'border-[#00d4ff] glow-accent'
: 'border-[#1e1e2e] hover:border-[#2e2e3e]'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-gray-500">#{i + 1}</span>
<span className={`text-xs px-2 py-0.5 rounded ${
candidate.source_type === 'sbir' ? 'bg-[#22c55e]/20 text-[#22c55e]' :
candidate.source_type === 'patent' ? 'bg-[#f59e0b]/20 text-[#f59e0b]' :
candidate.source_type === 'contract' ? 'bg-[#00d4ff]/20 text-[#00d4ff]' :
'bg-[#1e1e2e] text-gray-400'
}`}>
{candidate.source_type}
</span>
</div>
<div className="text-right">
<div className="text-sm font-semibold text-[#00d4ff]">
{(candidate.score * 100).toFixed(0)}%
</div>
{candidate.trl_estimate && (
<div className="text-xs text-gray-500">TRL {candidate.trl_estimate}</div>
)}
</div>
</div>
<h3 className="font-medium text-white mb-1 line-clamp-2">
{candidate.title}
</h3>
<p className="text-sm text-gray-500">
{candidate.organization}
</p>
{candidate.award_amount && (
<p className="text-xs text-[#22c55e] mt-1">
${candidate.award_amount.toLocaleString()}
</p>
)}
</div>
))}
</div>
</div>
{/* Selected Candidate Detail */}
{selectedCandidate && (
<div className="w-96 bg-[#12121a] rounded-2xl border border-[#00d4ff] p-6 sticky top-8 h-fit glow-accent">
<h3 className="text-lg font-semibold text-white mb-4">
{selectedCandidate.title}
</h3>
<div className="space-y-4">
<div>
<label className="text-xs text-gray-500">Organization</label>
<p className="text-white">{selectedCandidate.organization}</p>
</div>
<div>
<label className="text-xs text-gray-500">Description</label>
<p className="text-gray-300 text-sm">{selectedCandidate.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-gray-500">Score</label>
<p className="text-[#00d4ff] font-semibold">
{(selectedCandidate.score * 100).toFixed(1)}%
</p>
</div>
<div>
<label className="text-xs text-gray-500">TRL</label>
<p className="text-white">{selectedCandidate.trl_estimate || 'Unknown'}</p>
</div>
<div>
<label className="text-xs text-gray-500">Source</label>
<p className="text-white capitalize">{selectedCandidate.source_type}</p>
</div>
<div>
<label className="text-xs text-gray-500">Date</label>
<p className="text-white">{selectedCandidate.published_date || 'N/A'}</p>
</div>
</div>
{selectedCandidate.award_amount && (
<div>
<label className="text-xs text-gray-500">Award Amount</label>
<p className="text-[#22c55e] font-semibold">
${selectedCandidate.award_amount.toLocaleString()}
</p>
</div>
)}
<div className="flex gap-2 pt-4">
<a
href={selectedCandidate.url}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-2 bg-[#1e1e2e] text-center text-sm rounded-lg hover:bg-[#2e2e3e] transition-colors"
>
View Source
</a>
<button
onClick={() => onDeepDive(selectedCandidate)}
disabled={deepDiveLoading || selectedCandidate.organization === 'Unknown'}
title={selectedCandidate.organization === 'Unknown' ? 'Deep Dive requires a known organization' : 'Run deep dive analysis'}
className="flex-1 py-2 bg-gradient-to-r from-[#00d4ff] to-[#7c3aed] text-center text-sm rounded-lg font-semibold hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{deepDiveLoading ? (
<>
<FiLoader className="animate-spin" />
Analyzing...
</>
) : selectedCandidate.organization === 'Unknown' ? (
'No Organization'
) : (
'Deep Dive'
)}
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}
function FeatureCard({
icon,
title,
description
}: {
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<div className="bg-[#12121a] rounded-2xl border border-[#1e1e2e] p-6">
<div className="w-12 h-12 bg-[#1e1e2e] rounded-xl flex items-center justify-center mb-4 text-2xl">
{icon}
</div>
<h3 className="font-semibold text-white mb-2">{title}</h3>
<p className="text-sm text-gray-400">{description}</p>
</div>
);
}
function DeepDiveModal({
result,
onClose
}: {
result: DeepDiveResult;
onClose: () => void;
}) {
return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-8">
<div className="bg-[#0a0a12] rounded-2xl border border-[#1e1e2e] max-w-4xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-[#0a0a12] border-b border-[#1e1e2e] p-6 flex items-start justify-between">
<div>
<h2 className="text-2xl font-bold gradient-text">{result.organization}</h2>
<p className="text-gray-500 text-sm mt-1">Deep Dive Analysis</p>
</div>
<button
onClick={onClose}
className="text-gray-500 hover:text-white text-2xl"
>
&times;
</button>
</div>
<div className="p-6 space-y-6">
{/* Recommendation Banner */}
<div className={`p-4 rounded-xl border ${
result.recommendation.includes('STRONGLY RECOMMEND') ? 'bg-[#22c55e]/10 border-[#22c55e]/30 text-[#22c55e]' :
result.recommendation.includes('RECOMMEND') ? 'bg-[#00d4ff]/10 border-[#00d4ff]/30 text-[#00d4ff]' :
result.recommendation.includes('CONSIDER') ? 'bg-[#f59e0b]/10 border-[#f59e0b]/30 text-[#f59e0b]' :
'bg-[#ef4444]/10 border-[#ef4444]/30 text-[#ef4444]'
}`}>
<div className="font-semibold text-lg">{result.recommendation}</div>
</div>
{/* Assessment */}
<Section title="Assessment">
<p className="text-gray-300 whitespace-pre-wrap">{result.assessment}</p>
</Section>
{/* Company Profile */}
<Section title="Company Profile">
<div className="grid grid-cols-2 gap-4">
<InfoItem label="Headquarters" value={result.company_profile.headquarters} />
<InfoItem label="Founded" value={result.company_profile.founded} />
<InfoItem label="Employees" value={result.company_profile.employee_count} />
<InfoItem label="Website" value={result.company_profile.website} isLink />
</div>
<p className="text-gray-400 mt-4 text-sm">{result.company_profile.description}</p>
</Section>
{/* Technology Profile */}
<Section title="Technology Profile">
<div className="grid grid-cols-2 gap-4 mb-4">
<InfoItem label="TRL Assessment" value={`TRL ${result.technology_profile.trl_assessment}`} />
<InfoItem label="Technology" value={result.technology_profile.name} />
</div>
<div className="mb-4">
<label className="text-xs text-gray-500">Technical Approach</label>
<p className="text-gray-300 text-sm mt-1">{result.technology_profile.technical_approach}</p>
</div>
{result.technology_profile.key_capabilities.length > 0 && (
<div className="mb-4">
<label className="text-xs text-gray-500">Key Capabilities</label>
<ul className="list-disc list-inside text-gray-300 text-sm mt-1">
{result.technology_profile.key_capabilities.map((cap, i) => (
<li key={i}>{cap}</li>
))}
</ul>
</div>
)}
</Section>
{/* Contract History */}
<Section title="Contract History">
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="bg-[#1e1e2e] rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-[#00d4ff]">{result.contract_history.total_contracts}</div>
<div className="text-xs text-gray-500">Total Contracts</div>
</div>
<div className="bg-[#1e1e2e] rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-[#22c55e]">
${(result.contract_history.total_value / 1000000).toFixed(1)}M
</div>
<div className="text-xs text-gray-500">Total Value</div>
</div>
<div className="bg-[#1e1e2e] rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-[#f59e0b]">{result.contract_history.sbir_awards.length}</div>
<div className="text-xs text-gray-500">SBIR Awards</div>
</div>
</div>
</Section>
{/* Risk Factors */}
{result.risk_factors.length > 0 && (
<Section title="Risk Factors">
<ul className="space-y-2">
{result.risk_factors.map((risk, i) => (
<li key={i} className="flex items-start gap-2">
<span className="text-[#f59e0b]"></span>
<span className="text-gray-300">{risk}</span>
</li>
))}
</ul>
</Section>
)}
</div>
</div>
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="bg-[#12121a] rounded-xl border border-[#1e1e2e] p-5">
<h3 className="text-lg font-semibold text-[#00d4ff] mb-4">{title}</h3>
{children}
</div>
);
}
function InfoItem({ label, value, isLink }: { label: string; value: unknown; isLink?: boolean }) {
if (!value) return null;
const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
return (
<div>
<label className="text-xs text-gray-500">{label}</label>
{isLink ? (
<a href={displayValue} target="_blank" rel="noopener noreferrer" className="block text-[#00d4ff] hover:underline text-sm">
{displayValue}
</a>
) : (
<p className="text-white text-sm">{displayValue}</p>
)}
</div>
);
}
// Technology Card for Capability Match Results
function TechnologyCard({
technology,
rank,
isSelected,
onClick
}: {
technology: Technology;
rank: number;
isSelected: boolean;
onClick: () => void;
}) {
const fitColors = {
HIGH: { bg: 'bg-[#22c55e]/20', text: 'text-[#22c55e]' },
MEDIUM: { bg: 'bg-[#f59e0b]/20', text: 'text-[#f59e0b]' },
LOW: { bg: 'bg-[#ef4444]/20', text: 'text-[#ef4444]' },
UNCERTAIN: { bg: 'bg-[#6b7280]/20', text: 'text-[#6b7280]' },
};
const fit = technology.capability_match?.overall_fit || 'UNCERTAIN';
const colors = fitColors[fit];
const fitScore = technology.capability_match?.fit_score || 0;
return (
<div
onClick={onClick}
className={`bg-[#12121a] rounded-xl border p-4 cursor-pointer transition-all ${
isSelected
? 'border-[#00d4ff] glow-accent'
: 'border-[#1e1e2e] hover:border-[#2e2e3e]'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-gray-500">#{rank}</span>
<span className={`text-xs px-2 py-0.5 rounded ${colors.bg} ${colors.text}`}>
{fit}
</span>
<span className="text-xs px-2 py-0.5 rounded bg-[#1e1e2e] text-gray-400">
{technology.technology_type}
</span>
</div>
<div className="text-right">
<div className="text-sm font-semibold text-[#00d4ff]">{fitScore}%</div>
{technology.trl_estimate && (
<div className="text-xs text-gray-500">TRL {technology.trl_estimate}</div>
)}
</div>
</div>
<h3 className="font-medium text-white mb-1 line-clamp-2">{technology.canonical_name}</h3>
<p className="text-sm text-gray-500 line-clamp-2">{technology.description}</p>
{technology.developers.length > 0 && (
<p className="text-xs text-[#7c3aed] mt-2">
{technology.developers.map(d => d.name).join(', ')}
</p>
)}
<div className="flex items-center gap-2 mt-2">
<FiFileText className="text-gray-500 text-xs" />
<span className="text-xs text-gray-500">{technology.source_count} sources</span>
{technology.capability_match?.investigation_worthy && (
<span className="ml-auto text-xs px-2 py-0.5 rounded bg-[#7c3aed]/20 text-[#7c3aed]">
Worth Investigating
</span>
)}
</div>
</div>
);
}
// Technology Detail Panel for Capability Match Results
function TechnologyDetailPanel({
technology,
onDeepDive,
deepDiveLoading
}: {
technology: Technology;
onDeepDive: () => void;
deepDiveLoading: boolean;
}) {
const fitColors = {
HIGH: { bg: 'bg-[#22c55e]/10', text: 'text-[#22c55e]', border: 'border-[#22c55e]/30' },
MEDIUM: { bg: 'bg-[#f59e0b]/10', text: 'text-[#f59e0b]', border: 'border-[#f59e0b]/30' },
LOW: { bg: 'bg-[#ef4444]/10', text: 'text-[#ef4444]', border: 'border-[#ef4444]/30' },
UNCERTAIN: { bg: 'bg-[#6b7280]/10', text: 'text-[#6b7280]', border: 'border-[#6b7280]/30' },
};
const assessmentIcons = {
SUPPORTS: <FiCheckCircle className="text-[#22c55e]" />,
PARTIAL: <FiMinusCircle className="text-[#f59e0b]" />,
DOES_NOT_SUPPORT: <FiAlertCircle className="text-[#ef4444]" />,
UNKNOWN: <FiHelpCircle className="text-[#6b7280]" />,
};
const match = technology.capability_match;
const fit = match?.overall_fit || 'UNCERTAIN';
const colors = fitColors[fit];
const primaryDeveloper = technology.developers?.[0]?.name;
const canDeepDive = primaryDeveloper && primaryDeveloper !== 'Unknown' && primaryDeveloper !== 'unknown';
return (
<div className="w-96 bg-[#12121a] rounded-2xl border border-[#00d4ff] p-6 sticky top-8 h-fit glow-accent max-h-[80vh] overflow-y-auto">
<h3 className="text-lg font-semibold text-white mb-2">{technology.canonical_name}</h3>
{/* Fit Badge */}
<div className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm font-medium mb-4 ${colors.bg} ${colors.text} border ${colors.border}`}>
{fit} FIT ({match?.fit_score || 0}%)
</div>
<div className="space-y-4">
{/* How it addresses the need */}
{match?.how_it_addresses_need && (
<div>
<label className="text-xs text-gray-500">How It Addresses Your Need</label>
<p className="text-gray-300 text-sm mt-1">{match.how_it_addresses_need}</p>
</div>
)}
{/* Criteria Assessment */}
{match?.criteria_results && match.criteria_results.length > 0 && (
<div>
<label className="text-xs text-gray-500">Criteria Assessment</label>
<div className="space-y-2 mt-2">
{match.criteria_results.map((cr, i) => (
<div key={i} className="flex items-start gap-2 text-sm">
{assessmentIcons[cr.assessment as keyof typeof assessmentIcons] || assessmentIcons.UNKNOWN}
<div className="flex-1">
<span className="text-white">{cr.criterion}</span>
<p className="text-gray-500 text-xs mt-0.5">{cr.evidence}</p>
</div>
</div>
))}
</div>
</div>
)}
{/* Key Strengths */}
{match?.key_strengths && match.key_strengths.length > 0 && (
<div>
<label className="text-xs text-gray-500">Key Strengths</label>
<ul className="list-disc list-inside text-gray-300 text-sm mt-1">
{match.key_strengths.map((s, i) => (
<li key={i}>{s}</li>
))}
</ul>
</div>
)}
{/* Key Limitations */}
{match?.key_limitations && match.key_limitations.length > 0 && (
<div>
<label className="text-xs text-gray-500">Limitations</label>
<ul className="list-disc list-inside text-gray-400 text-sm mt-1">
{match.key_limitations.map((l, i) => (
<li key={i}>{l}</li>
))}
</ul>
</div>
)}
{/* Developers */}
{technology.developers.length > 0 && (
<div>
<label className="text-xs text-gray-500">Developers</label>
<div className="space-y-1 mt-1">
{technology.developers.map((dev, i) => (
<div key={i} className="flex items-center gap-2">
<span className="text-white text-sm">{dev.name}</span>
<span className="text-xs px-2 py-0.5 bg-[#1e1e2e] rounded text-gray-400">{dev.type}</span>
</div>
))}
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-2 pt-4">
{technology.sources?.[0]?.url && (
<a
href={technology.sources[0].url}
target="_blank"
rel="noopener noreferrer"
className="flex-1 py-2 bg-[#1e1e2e] text-center text-sm rounded-lg hover:bg-[#2e2e3e] transition-colors"
>
View Source
</a>
)}
<button
onClick={onDeepDive}
disabled={deepDiveLoading || !canDeepDive}
className="flex-1 py-2 bg-gradient-to-r from-[#00d4ff] to-[#7c3aed] text-center text-sm rounded-lg font-semibold hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{deepDiveLoading ? (
<>
<FiLoader className="animate-spin" />
Analyzing...
</>
) : !canDeepDive ? (
'No Developer'
) : (
'Deep Dive'
)}
</button>
</div>
</div>
</div>
);
}