1476 lines
54 KiB
TypeScript
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"
|
|
>
|
|
×
|
|
</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>
|
|
);
|
|
}
|