From b8f9692eba0f081f0463ecc4b8e56472b7d2a8c0 Mon Sep 17 00:00:00 2001 From: Moltbot-Jarvis Date: Wed, 18 Feb 2026 10:26:07 +0000 Subject: [PATCH] fix: Restore Frontend Inspector code (full version) --- .../frontend/src/components/Inspector.tsx | 923 ++++++++++++++++-- 1 file changed, 861 insertions(+), 62 deletions(-) diff --git a/company-explorer/frontend/src/components/Inspector.tsx b/company-explorer/frontend/src/components/Inspector.tsx index e1382548..a2d74e24 100644 --- a/company-explorer/frontend/src/components/Inspector.tsx +++ b/company-explorer/frontend/src/components/Inspector.tsx @@ -6,7 +6,7 @@ import { ContactsManager, Contact } from './ContactsManager' interface InspectorProps { companyId: number | null - initialContactId?: number | null + initialContactId?: number | null onClose: () => void apiBase: string } @@ -57,7 +57,7 @@ type CompanyDetail = { // Industry Strategy (V2) industry_details?: IndustryDetails - // Metrics + // NEU v0.7.0: Quantitative Metrics calculated_metric_name: string | null calculated_metric_value: number | null calculated_metric_unit: string | null @@ -70,7 +70,6 @@ type CompanyDetail = { metric_confidence_reason: string | null } -// ... ReportedMistake type remains same ... type ReportedMistake = { id: number; field_name: string; @@ -83,13 +82,13 @@ type ReportedMistake = { created_at: string; }; + export function Inspector({ companyId, initialContactId, onClose, apiBase }: InspectorProps) { const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [isProcessing, setIsProcessing] = useState(false) const [activeTab, setActiveTab] = useState<'overview' | 'contacts'>('overview') - // ... (State for Mistakes, Overrides remains same) ... const [isReportingMistake, setIsReportingMistake] = useState(false) const [existingMistakes, setExistingMistakes] = useState([]) const [reportedFieldName, setReportedFieldName] = useState("") @@ -99,27 +98,16 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins const [reportedQuote, setReportedQuote] = useState("") const [reportedComment, setReportedComment] = useState("") - // Override States - const [isEditingWiki, setIsEditingWiki] = useState(false) - const [wikiUrlInput, setWikiUrlInput] = useState("") - const [isEditingWebsite, setIsEditingWebsite] = useState(false) - const [websiteInput, setWebsiteInput] = useState("") - const [isEditingImpressum, setIsEditingImpressum] = useState(false) - const [impressumUrlInput, setImpressumUrlInput] = useState("") - const [industries, setIndustries] = useState([]) - const [isEditingIndustry, setIsEditingIndustry] = useState(false) - const [industryInput, setIndustryInput] = useState("") - // Polling Logic useEffect(() => { let interval: NodeJS.Timeout; if (isProcessing) { interval = setInterval(() => { - fetchData(true) + fetchData(true) // Silent fetch }, 2000) } return () => clearInterval(interval) - }, [isProcessing, companyId]) + }, [isProcessing, companyId]) useEffect(() => { if (initialContactId) { @@ -129,6 +117,17 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins } }, [initialContactId, companyId]) + const [isEditingWiki, setIsEditingWiki] = useState(false) + const [wikiUrlInput, setWikiUrlInput] = useState("") + const [isEditingWebsite, setIsEditingWebsite] = useState(false) + const [websiteInput, setWebsiteInput] = useState("") + const [isEditingImpressum, setIsEditingImpressum] = useState(false) + const [impressumUrlInput, setImpressumUrlInput] = useState("") + + const [industries, setIndustries] = useState([]) + const [isEditingIndustry, setIsEditingIndustry] = useState(false) + const [industryInput, setIndustryInput] = useState("") + const fetchData = (silent = false) => { if (!companyId) return if (!silent) setLoading(true) @@ -145,6 +144,7 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins if (isProcessing) { const hasWiki = newData.enrichment_data?.some((e: any) => e.source_type === 'wikipedia') const hasAnalysis = newData.enrichment_data?.some((e: any) => e.source_type === 'ai_analysis') + if ((hasWiki && newData.status === 'DISCOVERED') || (hasAnalysis && newData.status === 'ENRICHED')) { setIsProcessing(false) } @@ -161,29 +161,255 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins setIsEditingImpressum(false) setIsEditingIndustry(false) setIsProcessing(false) + axios.get(`${apiBase}/industries`) .then(res => setIndustries(res.data)) .catch(console.error) }, [companyId]) - // ... (Handlers: handleDiscover, handleAnalyze, handleExport, handleOverride...) ... - const handleDiscover = async () => { if (!companyId) return; setIsProcessing(true); try { await axios.post(`${apiBase}/enrich/discover`, { company_id: companyId }); } catch (e) { console.error(e); setIsProcessing(false); } } - const handleAnalyze = async () => { if (!companyId) return; setIsProcessing(true); try { await axios.post(`${apiBase}/enrich/analyze`, { company_id: companyId }); } catch (e) { console.error(e); setIsProcessing(false); } } - - const handleExport = () => { /* Export logic same as before, omitted for brevity but should include new fields */ }; - - const handleWikiOverride = async () => { /* ... */ } - const handleWebsiteOverride = async () => { /* ... */ } - const handleImpressumOverride = async () => { /* ... */ } - const handleIndustryOverride = async () => { /* ... */ } - const handleReevaluateWikipedia = async () => { /* ... */ } - const handleDelete = async () => { /* ... */ } - const handleLockToggle = async (type: string, status: boolean) => { /* ... */ } - const handleReportMistake = async () => { /* ... */ } + const handleDiscover = async () => { + if (!companyId) return + setIsProcessing(true) + try { + await axios.post(`${apiBase}/enrich/discover`, { company_id: companyId }) + } catch (e) { + console.error(e) + setIsProcessing(false) + } + } + + const handleAnalyze = async () => { + if (!companyId) return + setIsProcessing(true) + try { + await axios.post(`${apiBase}/enrich/analyze`, { company_id: companyId }) + } catch (e) { + console.error(e) + setIsProcessing(false) + } + } + + const handleExport = () => { + if (!data) return; + const exportData = { + metadata: { + id: data.id, + exported_at: new Date().toISOString(), + source: "Company Explorer (Robotics Edition)" + }, + company: { + name: data.name, + website: data.website, + status: data.status, + 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 + }; + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `company-export-${data.id}-${data.name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const handleWikiOverride = async () => { + if (!companyId) return + setIsProcessing(true) + try { + await axios.post(`${apiBase}/companies/${companyId}/override/wiki?url=${encodeURIComponent(wikiUrlInput)}`) + setIsEditingWiki(false) + fetchData() + } catch (e) { + alert("Update failed") + console.error(e) + } finally { + setIsProcessing(false) + } + } + + const handleWebsiteOverride = async () => { + if (!companyId) return + setIsProcessing(true) + try { + await axios.post(`${apiBase}/companies/${companyId}/override/website?url=${encodeURIComponent(websiteInput)}`) + setIsEditingWebsite(false) + fetchData() + } catch (e) { + alert("Update failed") + console.error(e) + } finally { + setIsProcessing(false) + } + } + + const handleImpressumOverride = async () => { + if (!companyId) return + setIsProcessing(true) + try { + await axios.post(`${apiBase}/companies/${companyId}/override/impressum?url=${encodeURIComponent(impressumUrlInput)}`) + setIsEditingImpressum(false) + fetchData() + } catch (e) { + alert("Impressum update failed") + console.error(e) + } finally { + setIsProcessing(false) + } + } + + const handleIndustryOverride = async () => { + if (!companyId) return + setIsProcessing(true) + try { + await axios.put(`${apiBase}/companies/${companyId}/industry`, { industry_ai: industryInput }) + setIsEditingIndustry(false) + fetchData() + } catch (e) { + alert("Industry update failed") + console.error(e) + } finally { + setIsProcessing(false) + } + } + + const handleReevaluateWikipedia = async () => { + if (!companyId) return + setIsProcessing(true) + try { + await axios.post(`${apiBase}/companies/${companyId}/reevaluate-wikipedia`) + } catch (e) { + console.error(e) + setIsProcessing(false) + } + } + + const handleDelete = async () => { + if (!companyId) return; + if (!window.confirm(`Are you sure you want to delete "${data?.name}"? This action cannot be undone.`)) { + return + } + try { + await axios.delete(`${apiBase}/companies/${companyId}`) + onClose() + window.location.reload() + } catch (e: any) { + alert("Failed to delete company: " + (e.response?.data?.detail || e.message)) + } + } + + const handleLockToggle = async (sourceType: string, currentLockStatus: boolean) => { + if (!companyId) return + try { + await axios.post(`${apiBase}/enrichment/${companyId}/${sourceType}/lock?locked=${!currentLockStatus}`) + fetchData(true) + } catch (e) { + console.error("Lock toggle failed", e) + } + } + + interface ReportedMistakeRequest { + field_name: string; + wrong_value?: string | null; + corrected_value?: string | null; + source_url?: string | null; + quote?: string | null; + user_comment?: string | null; + } + + const handleReportMistake = async () => { + if (!companyId) return; + if (!reportedFieldName) { + alert("Field Name is required."); + return; + } + + setIsProcessing(true); + try { + const payload: ReportedMistakeRequest = { + field_name: reportedFieldName, + wrong_value: reportedWrongValue || null, + corrected_value: reportedCorrectedValue || null, + source_url: reportedSourceUrl || null, + quote: reportedQuote || null, + user_comment: reportedComment || null, + }; + + await axios.post(`${apiBase}/companies/${companyId}/report-mistake`, payload); + alert("Mistake reported successfully!"); + setIsReportingMistake(false); + setReportedFieldName(""); + setReportedWrongValue(""); + setReportedCorrectedValue(""); + setReportedSourceUrl(""); + setReportedQuote(""); + setReportedComment(""); + fetchData(true); + } catch (e) { + alert("Failed to report mistake."); + console.error(e); + } finally { + setIsProcessing(false); + } + }; + + const handleAddContact = async (contact: Contact) => { + if (!companyId) return + try { + await axios.post(`${apiBase}/contacts`, { ...contact, company_id: companyId }) + fetchData(true) + } catch (e) { + alert("Failed to add contact") + console.error(e) + } + } + + const handleEditContact = async (contact: Contact) => { + if (!contact.id) return + try { + await axios.put(`${apiBase}/contacts/${contact.id}`, contact) + fetchData(true) + } catch (e) { + alert("Failed to update contact") + console.error(e) + } + } if (!companyId) return null - // Helper renderers + const wikiEntry = data?.enrichment_data?.find(e => e.source_type === 'wikipedia') + const wiki = wikiEntry?.content + const isLocked = wikiEntry?.is_locked + const wikiDate = wikiEntry?.created_at + + const aiAnalysisEntry = data?.enrichment_data?.find(e => e.source_type === 'ai_analysis') + const aiAnalysis = aiAnalysisEntry?.content + const aiDate = aiAnalysisEntry?.created_at + + const scrapeEntry = data?.enrichment_data?.find(e => e.source_type === 'website_scrape') + const scrapeData = scrapeEntry?.content + const impressum = scrapeData?.impressum + const scrapeDate = scrapeEntry?.created_at + + // NEW: Strategy Card Renderer const renderStrategyCard = () => { if (!data?.industry_details) return null; const { pains, gains, priority, notes } = data.industry_details; @@ -231,6 +457,7 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins ) } + // NEW: CRM Comparison Renderer const renderCRMComparison = () => { // Only show if CRM data exists if (!data?.crm_name && !data?.crm_website) return null; @@ -268,7 +495,6 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
Name: {data.name}
Web: {data.website}
- {/* Add Scraping Impressum Data logic here if needed, currently shown below in Legal Data */}
@@ -276,9 +502,6 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins ) } - // ... (Reuse existing components for Wiki, Analysis, etc.) ... - // Need to reconstruct the return JSX with the new sections inserted. - return (
{loading ? ( @@ -289,48 +512,624 @@ export function Inspector({ companyId, initialContactId, onClose, apiBase }: Ins
{/* Header */}
- {/* ... Header Content (Name, Actions, Website) ... reuse existing code */} -
+

{data.name}

- {/* ... Actions ... */}
- + + + + +
-
- {/* ... */} - - {/* Tab Navigation */} -
- - -
+
+ +
+ {!isEditingWebsite ? ( +
+ {data.website && data.website !== "k.A." ? ( + + {new URL(data.website.startsWith('http') ? data.website : `https://${data.website}`).hostname.replace('www.', '')} + + ) : ( + No website + )} + +
+ ) : ( +
+ setWebsiteInput(e.target.value)} + placeholder="https://..." + className="bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded px-2 py-0.5 text-[10px] text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none w-48" + autoFocus + /> + + +
+ )} +
+ + {existingMistakes.length > 0 && ( +
+

+ + Existing Correction Proposals +

+
+ {existingMistakes.map(mistake => ( +
+
+ {mistake.field_name} + + {mistake.status} + +
+

+ {mistake.wrong_value || 'N/A'}{mistake.corrected_value || 'N/A'} +

+ {mistake.user_comment &&

"{mistake.user_comment}"

} +
+ ))} +
+
+ )} + +
+ + +
+ {activeTab === 'overview' && ( <> - {/* Actions */}
- {/* ... Discover/Analyze Buttons ... */} - - + +
- {/* NEW: CRM Comparison */} + {/* NEW: Comparison & Strategy */} {renderCRMComparison()} - - {/* NEW: Strategy Card */} {renderStrategyCard()} - {/* Core Classification */} -
- {/* ... Classification UI ... */} -
{data.industry_ai || "Not Classified"}
+
+
+
+
+ +
+ Official Legal Data +
+
+ {scrapeDate && ( +
+ {new Date(scrapeDate).toLocaleDateString()} +
+ )} + + {scrapeEntry && ( + + )} + + {!isEditingImpressum ? ( + + ) : (
+ + +
+ )} +
+
+ + {isEditingImpressum && ( +
+ setImpressumUrlInput(e.target.value)} + placeholder="https://.../impressum" + className="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-2 py-1 text-xs text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none" + autoFocus + /> +
+ )} + + {impressum ? ( + <> +
+ {impressum.legal_name || "Unknown Legal Name"} +
+ +
+ +
+
{impressum.street}
+
{impressum.zip} {impressum.city}
+
+
+ + {(impressum.email || impressum.phone) && ( +
+ {impressum.email && {impressum.email}} + {impressum.phone && {impressum.phone}} + {impressum.vat_id && VAT: {impressum.vat_id}} +
+ )} + + ) : !isEditingImpressum && ( +
+ No legal data found. Click pencil to provide direct Impressum link. +
+ )}
- {/* Wiki & Impressum Sections (Existing) */} - {/* ... (Omitted to keep script short, ideally I would splice this into the original file) ... */} - +
+
+
+
Industry Focus
+ {!isEditingIndustry ? ( +
+
+ +
+
+
{data.industry_ai || "Not Classified"}
+ +
+
+ ) : ( +
+ +
+ + +
+
+ )} +
+
+
Analysis Status
+
+
+ +
+
+ {data.status} +
+
+
+
+
+ + {aiAnalysis && ( +
+
+

+ AI Strategic Dossier +

+ {aiDate && ( +
+ {new Date(aiDate).toLocaleDateString()} +
+ )} +
+
+
+
Business Model
+

{aiAnalysis.business_model || "No summary available."}

+
+ {aiAnalysis.infrastructure_evidence && ( +
+
Infrastructure Evidence
+

"{aiAnalysis.infrastructure_evidence}"

+
+ )} +
+
+ )} + +
+
+

+ Company Profile (Wikipedia) +

+ +
+ {wikiDate && ( +
+ {new Date(wikiDate).toLocaleDateString()} +
+ )} + + {wikiEntry && ( + + )} + + + + {!isEditingWiki ? ( + + ) : (
+ + +
+ )} +
+
+ + {isEditingWiki && ( +
+ setWikiUrlInput(e.target.value)} + placeholder="Paste Wikipedia URL here..." + className="w-full bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded px-2 py-1 text-sm text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none" + /> +

Paste a valid URL. Saving will trigger a re-scan.

+
+ )} + + {wiki && wiki.url !== 'k.A.' && !isEditingWiki ? ( +
+
+
+ +
+ + {isLocked && ( +
+ Manual Override +
+ )} + +

+ "{wiki.first_paragraph}" +

+ +
+
+
+ +
+
+
Employees
+
{wiki.mitarbeiter || 'k.A.'}
+
+
+ +
+
+ +
+
+
Revenue
+
{wiki.umsatz && wiki.umsatz !== 'k.A.' ? `${wiki.umsatz} Mio. €` : 'k.A.'}
+
+
+ +
+
+ +
+
+
Headquarters
+
{wiki.sitz_stadt}{wiki.sitz_land ? `, ${wiki.sitz_land}` : ''}
+
+
+ +
+
+ +
+
+
Wiki Industry
+
{wiki.branche || 'k.A.'}
+
+
+
+ + {wiki.categories && wiki.categories !== 'k.A.' && ( +
+
+ Categories +
+
+ {wiki.categories.split(',').map((cat: string) => ( + + {cat.trim()} + + ))} +
+
+ )} + + +
+
+ ) : !isEditingWiki ? ( +
+ +

No Wikipedia profile found yet.

+
+ ) : null} +
+ +
+

+ Quantitative Potential +

+ + {data.calculated_metric_value != null || data.standardized_metric_value != null ? ( +
+ {data.calculated_metric_value != null && ( +
+
+ +
+
+
{data.calculated_metric_name || 'Calculated Metric'}
+
+ {data.calculated_metric_value.toLocaleString('de-DE')} + {data.calculated_metric_unit} +
+
+
+ )} + + {data.standardized_metric_value != null && ( +
+
+ +
+
+
Standardized Potential ({data.standardized_metric_unit})
+
+ {data.standardized_metric_value.toLocaleString('de-DE')} + {data.standardized_metric_unit} +
+

Comparable value for potential analysis.

+
+
+ )} + + {data.metric_source && ( +
+ + {data.metric_confidence != null && ( +
+ Confidence: +
+
= 0.8 ? "bg-green-500" : + data.metric_confidence >= 0.5 ? "bg-yellow-500" : "bg-red-500")} /> + = 0.8 ? "text-green-700 dark:text-green-400" : + data.metric_confidence >= 0.5 ? "text-yellow-700 dark:text-yellow-400" : "text-red-700 dark:text-red-400" + )}> + {(data.metric_confidence * 100).toFixed(0)}% + +
+
+ )} + +
+ + Source: + + {data.metric_source} + + {data.metric_source_url && ( + + + + )} +
+
+ )} + +
+ ) : ( +
+ +

No quantitative data calculated yet.

+

Run "Analyze Potential" to extract metrics.

+
+ )} +
)}