feat(Explorer): Enhance metric extraction, source transparency, and UI display

- **Standardization & Formula Logic:** Fixed NameError/SyntaxError in formula parser; added support for comments and capitalized placeholders.
- **Source URL Tracking:** Extended DB schema and cascade logic to store and track specific source URLs.
- **Frontend & UI:**
  - Added 'Standardized Potential' display in Inspector.
  - Added clickable source link with icon.
  - Fixed Settings tab layout collapse (flex-shrink-0).
- **Export Capabilities:**
  - Single-company JSON export now includes full quantitative metadata.
  - New global CSV export endpoint /api/companies/export.
- **System Integrity:**
  - Fixed Notion sync typo ('Stanardization').
  - Corrected Nginx proxy routing and FastAPI route ordering.
  - Ensured DB persistence via explicit docker-compose volume mapping.
This commit is contained in:
2026-01-24 09:56:59 +00:00
parent 127aadb85a
commit 5670660907
11 changed files with 304 additions and 380 deletions

View File

@@ -167,6 +167,18 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
industry_ai: data.industry_ai,
created_at: data.created_at
},
quantitative_potential: {
calculated_metric_name: data.calculated_metric_name,
calculated_metric_value: data.calculated_metric_value,
calculated_metric_unit: data.calculated_metric_unit,
standardized_metric_value: data.standardized_metric_value,
standardized_metric_unit: data.standardized_metric_unit,
metric_source: data.metric_source,
metric_source_url: data.metric_source_url,
metric_proof_text: data.metric_proof_text,
metric_confidence: data.metric_confidence,
metric_confidence_reason: data.metric_confidence_reason
},
enrichment: data.enrichment_data,
signals: data.signals
};
@@ -912,6 +924,23 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
</div>
)}
{/* Standardized Metric */}
{data.standardized_metric_value != null && (
<div className="flex items-start gap-3 pt-4 border-t border-slate-200 dark:border-slate-800">
<div className="p-2 bg-white dark:bg-slate-800 rounded-lg text-green-500 mt-1">
<Ruler className="h-4 w-4" />
</div>
<div>
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Standardized Potential ({data.standardized_metric_unit})</div>
<div className="text-xl text-green-600 dark:text-green-400 font-bold">
{data.standardized_metric_value.toLocaleString('de-DE')}
<span className="text-sm font-medium text-slate-500 ml-1">{data.standardized_metric_unit}</span>
</div>
<p className="text-xs text-slate-500 mt-1">Comparable value for potential analysis.</p>
</div>
</div>
)}
{/* Source & Confidence */}
{data.metric_source && (
<div className="flex justify-between items-center text-[10px] text-slate-500 pt-2 border-t border-slate-200 dark:border-slate-800">

View File

@@ -104,7 +104,7 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
</div>
{/* Tab Nav */}
<div className="flex border-b border-slate-200 dark:border-slate-800 px-6 bg-white dark:bg-slate-900 overflow-x-auto">
<div className="flex flex-shrink-0 border-b border-slate-200 dark:border-slate-800 px-6 bg-white dark:bg-slate-900 overflow-x-auto">
{[
{ id: 'robotics', label: 'Robotics Potential', icon: Bot },
{ id: 'industries', label: 'Industry Focus', icon: Target },
@@ -130,72 +130,66 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
{isLoading && <div className="text-center py-12 text-slate-500">Loading...</div>}
{!isLoading && activeTab === 'robotics' && (
<div key="robotics-content" className="grid grid-cols-1 md:grid-cols-2 gap-6">
{roboticsCategories.map(cat => ( <CategoryCard key={cat.id} category={cat} onSave={handleUpdateRobotics} /> ))}
</div>
)}
<div key="robotics-content" className={clsx("grid grid-cols-1 md:grid-cols-2 gap-6", { 'hidden': isLoading || activeTab !== 'robotics' })}>
{roboticsCategories.map(cat => ( <CategoryCard key={cat.id} category={cat} onSave={handleUpdateRobotics} /> ))}
</div>
{!isLoading && activeTab === 'industries' && (
<div key="industries-content" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Industry Verticals (Synced from Notion)</h3>
</div>
<div className="grid grid-cols-1 gap-3">
{industries.map(ind => (
<div key={ind.id} className="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg p-4 flex flex-col gap-3 group relative overflow-hidden">
{ind.notion_id && (
<div className="absolute top-0 right-0 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 text-[9px] font-bold px-2 py-0.5 rounded-bl">SYNCED</div>
)}
<div className="flex gap-4 items-start pr-12">
<div className="flex-1">
<h4 className="font-bold text-slate-900 dark:text-white text-sm">{ind.name}</h4>
<div className="flex flex-wrap gap-2 mt-1">
{ind.status_notion && <span className="text-[10px] border border-slate-300 dark:border-slate-700 px-1.5 rounded text-slate-500">{ind.status_notion}</span>}
</div>
</div>
<div className="text-right">
<div className="flex items-center gap-1.5 justify-end">
<span className={clsx("w-2 h-2 rounded-full", ind.is_focus ? "bg-green-500" : "bg-slate-300 dark:bg-slate-700")} />
<span className="text-xs text-slate-500">{ind.is_focus ? "Focus" : "Standard"}</span>
</div>
<div key="industries-content" className={clsx("space-y-4", { 'hidden': isLoading || activeTab !== 'industries' })}>
<div className="flex justify-between items-center">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Industry Verticals (Synced from Notion)</h3>
</div>
<div className="grid grid-cols-1 gap-3">
{industries.map(ind => (
<div key={ind.id} className="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg p-4 flex flex-col gap-3 group relative overflow-hidden">
{ind.notion_id && (
<div className="absolute top-0 right-0 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 text-[9px] font-bold px-2 py-0.5 rounded-bl">SYNCED</div>
)}
<div className="flex gap-4 items-start pr-12">
<div className="flex-1">
<h4 className="font-bold text-slate-900 dark:text-white text-sm">{ind.name}</h4>
<div className="flex flex-wrap gap-2 mt-1">
{ind.status_notion && <span className="text-[10px] border border-slate-300 dark:border-slate-700 px-1.5 rounded text-slate-500">{ind.status_notion}</span>}
</div>
</div>
<p className="text-xs text-slate-600 dark:text-slate-300 italic whitespace-pre-wrap">{ind.description || "No definition"}</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-[10px] bg-white dark:bg-slate-900 p-2 rounded border border-slate-200 dark:border-slate-800">
<div><span className="block text-slate-400 font-bold uppercase">Whale &gt;</span><span className="text-slate-700 dark:text-slate-200">{ind.whale_threshold || "-"}</span></div>
<div><span className="block text-slate-400 font-bold uppercase">Min Req</span><span className="text-slate-700 dark:text-slate-200">{ind.min_requirement || "-"}</span></div>
<div><span className="block text-slate-400 font-bold uppercase">Unit</span><span className="text-slate-700 dark:text-slate-200 truncate">{ind.scraper_search_term || "-"}</span></div>
<div><span className="block text-slate-400 font-bold uppercase">Product</span><span className="text-slate-700 dark:text-slate-200 truncate">{roboticsCategories.find(c => c.id === ind.primary_category_id)?.name || "-"}</span></div>
<div className="text-right">
<div className="flex items-center gap-1.5 justify-end">
<span className={clsx("w-2 h-2 rounded-full", ind.is_focus ? "bg-green-500" : "bg-slate-300 dark:bg-slate-700")} />
<span className="text-xs text-slate-500">{ind.is_focus ? "Focus" : "Standard"}</span>
</div>
</div>
{ind.scraper_keywords && <div className="text-[10px]"><span className="text-slate-400 font-bold uppercase mr-2">Keywords:</span><span className="text-slate-600 dark:text-slate-400 font-mono">{ind.scraper_keywords}</span></div>}
{ind.standardization_logic && <div className="text-[10px]"><span className="text-slate-400 font-bold uppercase mr-2">Standardization:</span><span className="text-slate-600 dark:text-slate-400 font-mono">{ind.standardization_logic}</span></div>}
</div>
))}
</div>
<p className="text-xs text-slate-600 dark:text-slate-300 italic whitespace-pre-wrap">{ind.description || "No definition"}</p>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-[10px] bg-white dark:bg-slate-900 p-2 rounded border border-slate-200 dark:border-slate-800">
<div><span className="block text-slate-400 font-bold uppercase">Whale &gt;</span><span className="text-slate-700 dark:text-slate-200">{ind.whale_threshold || "-"}</span></div>
<div><span className="block text-slate-400 font-bold uppercase">Min Req</span><span className="text-slate-700 dark:text-slate-200">{ind.min_requirement || "-"}</span></div>
<div><span className="block text-slate-400 font-bold uppercase">Unit</span><span className="text-slate-700 dark:text-slate-200 truncate">{ind.scraper_search_term || "-"}</span></div>
<div><span className="block text-slate-400 font-bold uppercase">Product</span><span className="text-slate-700 dark:text-slate-200 truncate">{roboticsCategories.find(c => c.id === ind.primary_category_id)?.name || "-"}</span></div>
</div>
{ind.scraper_keywords && <div className="text-[10px]"><span className="text-slate-400 font-bold uppercase mr-2">Keywords:</span><span className="text-slate-600 dark:text-slate-400 font-mono">{ind.scraper_keywords}</span></div>}
{ind.standardization_logic && <div className="text-[10px]"><span className="text-slate-400 font-bold uppercase mr-2">Standardization:</span><span className="text-slate-600 dark:text-slate-400 font-mono">{ind.standardization_logic}</span></div>}
</div>
))}
</div>
)}
</div>
{!isLoading && activeTab === 'roles' && (
<div key="roles-content" className="space-y-4">
<div className="flex justify-between items-center"><h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Job Title Mapping Patterns</h3><button onClick={handleAddJobRole} className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded"><Plus className="h-3 w-3" /> ADD PATTERN</button></div>
<div className="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden">
<table className="w-full text-left text-xs">
<thead className="bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase"><tr><th className="p-3">Job Title Pattern (Regex/Text)</th><th className="p-3">Mapped Role</th><th className="p-3 w-10"></th></tr></thead>
<tbody className="divide-y divide-slate-200 dark:divide-slate-800">
{jobRoles.map(role => (
<tr key={role.id} className="group">
<td className="p-2"><input className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.pattern} /></td>
<td className="p-2"><select className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.role}><option>Operativer Entscheider</option><option>Infrastruktur-Verantwortlicher</option><option>Wirtschaftlicher Entscheider</option><option>Innovations-Treiber</option></select></td>
<td className="p-2 text-center"><button onClick={() => handleDeleteJobRole(role.id)} className="text-slate-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 className="h-4 w-4" /></button></td>
</tr>
))}
{jobRoles.length === 0 && (<tr><td colSpan={3} className="p-8 text-center text-slate-500 italic">No patterns defined yet.</td></tr>)}
</tbody>
</table>
</div>
<div key="roles-content" className={clsx("space-y-4", { 'hidden': isLoading || activeTab !== 'roles' })}>
<div className="flex justify-between items-center"><h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Job Title Mapping Patterns</h3><button onClick={handleAddJobRole} className="flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded"><Plus className="h-3 w-3" /> ADD PATTERN</button></div>
<div className="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden">
<table className="w-full text-left text-xs">
<thead className="bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase"><tr><th className="p-3">Job Title Pattern (Regex/Text)</th><th className="p-3">Mapped Role</th><th className="p-3 w-10"></th></tr></thead>
<tbody className="divide-y divide-slate-200 dark:divide-slate-800">
{jobRoles.map(role => (
<tr key={role.id} className="group">
<td className="p-2"><input className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.pattern} /></td>
<td className="p-2"><select className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-2 py-1 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.role}><option>Operativer Entscheider</option><option>Infrastruktur-Verantwortlicher</option><option>Wirtschaftlicher Entscheider</option><option>Innovations-Treiber</option></select></td>
<td className="p-2 text-center"><button onClick={() => handleDeleteJobRole(role.id)} className="text-slate-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"><Trash2 className="h-4 w-4" /></button></td>
</tr>
))}
{jobRoles.length === 0 && (<tr><td colSpan={3} className="p-8 text-center text-slate-500 italic">No patterns defined yet.</td></tr>)}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
</div>