Files
Brancheneinstufung2/company-explorer/frontend/src/components/Inspector.tsx
Floke 86f9962199 fix(ce): Resolve database schema mismatch and restore docs
- Fixed a critical  in the company-explorer by forcing a database re-initialization with a new file (). This ensures the application code is in sync with the database schema.
- Documented the schema mismatch incident and its resolution in MIGRATION_PLAN.md.

- Restored and enhanced BUILDER_APPS_MIGRATION.md by recovering extensive, valuable content from the git history that was accidentally deleted. The guide now again includes detailed troubleshooting steps and code templates for common migration pitfalls.
2026-01-15 15:54:45 +00:00

787 lines
41 KiB
TypeScript

import { useEffect, useState } from 'react'
import axios from 'axios'
import { X, ExternalLink, Bot, Briefcase, Calendar, Globe, Users, DollarSign, MapPin, Tag, RefreshCw as RefreshCwIcon, Search as SearchIcon, Pencil, Check, Download, Clock, Lock, Unlock } from 'lucide-react'
import clsx from 'clsx'
import { ContactsManager, Contact } from './ContactsManager'
interface InspectorProps {
companyId: number | null
initialContactId?: number | null // NEW
onClose: () => void
apiBase: string
}
type Signal = {
signal_type: string
confidence: number
value: string
proof_text: string
}
type EnrichmentData = {
source_type: string
content: any
is_locked?: boolean
created_at?: string
}
type CompanyDetail = {
id: number
name: string
website: string | null
industry_ai: string | null
status: string
created_at: string
signals: Signal[]
enrichment_data: EnrichmentData[]
contacts?: Contact[]
}
export function Inspector({ companyId, initialContactId, onClose, apiBase }: InspectorProps) {
const [data, setData] = useState<CompanyDetail | null>(null)
const [loading, setLoading] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [activeTab, setActiveTab] = useState<'overview' | 'contacts'>('overview')
// Polling Logic
useEffect(() => {
let interval: NodeJS.Timeout;
if (isProcessing) {
interval = setInterval(() => {
fetchData(true) // Silent fetch
}, 2000)
}
return () => clearInterval(interval)
}, [isProcessing, companyId]) // Dependencies
// Auto-switch to contacts tab if initialContactId is present
useEffect(() => {
if (initialContactId) {
setActiveTab('contacts')
} else {
setActiveTab('overview')
}
}, [initialContactId, companyId])
// Manual Override State
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 fetchData = (silent = false) => {
if (!companyId) return
if (!silent) setLoading(true)
axios.get(`${apiBase}/companies/${companyId}`)
.then(res => {
const newData = res.data
setData(newData)
// Auto-stop processing if status changes to ENRICHED or we see data
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 we were waiting for Discover (Wiki) or Analyze (AI)
if ((hasWiki && newData.status === 'DISCOVERED') || (hasAnalysis && newData.status === 'ENRICHED')) {
setIsProcessing(false)
}
}
})
.catch(console.error)
.finally(() => { if (!silent) setLoading(false) })
}
useEffect(() => {
fetchData()
setIsEditingWiki(false)
setIsEditingWebsite(false)
setIsEditingImpressum(false)
setIsProcessing(false) // Reset on ID change
}, [companyId])
const handleDiscover = async () => {
if (!companyId) return
setIsProcessing(true)
try {
await axios.post(`${apiBase}/enrich/discover`, { company_id: companyId })
// Polling effect will handle the rest
} 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 })
// Polling effect will handle the rest
} catch (e) {
console.error(e)
setIsProcessing(false)
}
}
const handleExport = () => {
if (!data) return;
// Prepare full export object
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
},
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 handleLockToggle = async (sourceType: string, currentLockStatus: boolean) => {
if (!companyId) return
try {
await axios.post(`${apiBase}/enrichment/${companyId}/${sourceType}/lock?locked=${!currentLockStatus}`)
fetchData(true) // Silent refresh
} catch (e) {
console.error("Lock toggle failed", e)
}
}
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
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
return (
<div className="fixed inset-y-0 right-0 w-full md:w-[550px] bg-white dark:bg-slate-900 border-l border-slate-200 dark:border-slate-800 shadow-2xl transform transition-transform duration-300 ease-in-out z-50 overflow-y-auto">
{loading ? (
<div className="p-8 text-slate-500">Loading details...</div>
) : !data ? (
<div className="p-8 text-red-400">Failed to load data.</div>
) : (
<div className="flex flex-col h-full">
{/* Header */}
<div className="p-6 border-b border-slate-200 dark:border-slate-800 bg-slate-50/80 dark:bg-slate-950/50 backdrop-blur-sm sticky top-0 z-10">
<div className="flex justify-between items-start mb-4">
<h2 className="text-xl font-bold text-slate-900 dark:text-white leading-tight">{data.name}</h2>
<div className="flex items-center gap-2">
<button
onClick={handleExport}
className="p-1.5 text-slate-500 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
title="Export JSON"
>
<Download className="h-4 w-4" />
</button>
<button
onClick={() => fetchData(true)}
className="p-1.5 text-slate-500 hover:text-slate-900 dark:hover:text-white transition-colors"
title="Refresh"
>
<RefreshCwIcon className={clsx("h-4 w-4", (loading || isProcessing) && "animate-spin")} />
</button>
<button onClick={onClose} className="p-1.5 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors">
<X className="h-6 w-6" />
</button>
</div>
</div>
<div className="flex flex-wrap gap-2 text-sm items-center">
{!isEditingWebsite ? (
<div className="flex items-center gap-2">
{data.website && data.website !== "k.A." ? (
<a href={data.website} target="_blank" className="flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors font-medium">
<ExternalLink className="h-3 w-3" /> {new URL(data.website).hostname.replace('www.', '')}
</a>
) : (
<span className="text-slate-500 italic">No website</span>
)}
<button
onClick={() => { setWebsiteInput(data.website && data.website !== "k.A." ? data.website : ""); setIsEditingWebsite(true); }}
className="p-1 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors"
title="Edit Website URL"
>
<Pencil className="h-3 w-3" />
</button>
</div>
) : (
<div className="flex items-center gap-1 animate-in fade-in zoom-in duration-200">
<input
type="text"
value={websiteInput}
onChange={e => 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-xs text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none w-48"
autoFocus
/>
<button
onClick={handleWebsiteOverride}
className="p-1 bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-400 rounded hover:bg-green-200 dark:hover:bg-green-900 transition-colors"
>
<Check className="h-3 w-3" />
</button>
<button
onClick={() => setIsEditingWebsite(false)}
className="p-1 text-slate-500 hover:text-red-500 transition-colors"
>
<X className="h-3 w-3" />
</button>
</div>
)}
{data.industry_ai && (
<span className="flex items-center gap-1 px-2 py-0.5 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 rounded border border-slate-200 dark:border-slate-700">
<Briefcase className="h-3 w-3" /> {data.industry_ai}
</span>
)}
<span className={clsx(
"px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider",
data.status === 'ENRICHED' ? "bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-800/50" :
data.status === 'DISCOVERED' ? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-400 border border-blue-200 dark:border-blue-800/50" :
"bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700"
)}>
{data.status}
</span>
</div>
{/* Tab Navigation */}
<div className="mt-6 flex border-b border-slate-200 dark:border-slate-800">
<button
onClick={() => setActiveTab('overview')}
className={clsx(
"px-4 py-2 text-sm font-medium transition-colors border-b-2",
activeTab === 'overview'
? "border-blue-500 text-blue-600 dark:text-blue-400"
: "border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200"
)}
>
Overview
</button>
<button
onClick={() => setActiveTab('contacts')}
className={clsx(
"px-4 py-2 text-sm font-medium transition-colors border-b-2 flex items-center gap-2",
activeTab === 'contacts'
? "border-blue-500 text-blue-600 dark:text-blue-400"
: "border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200"
)}
>
Contacts
{data.contacts && data.contacts.length > 0 && (
<span className="bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300 px-1.5 py-0.5 rounded-full text-[10px] min-w-[1.25rem] text-center">
{data.contacts.length}
</span>
)}
</button>
</div>
</div>
<div className="p-6 space-y-8 bg-white dark:bg-slate-900">
{activeTab === 'overview' && (
<>
{/* Action Bar (Only for Overview) */}
<div className="flex gap-2 mb-6">
<button
onClick={handleDiscover}
disabled={isProcessing}
className="flex-1 flex items-center justify-center gap-2 bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 disabled:opacity-50 text-slate-700 dark:text-white text-xs font-bold py-2 rounded-md border border-slate-200 dark:border-slate-700 transition-all shadow-sm"
>
<SearchIcon className="h-3.5 w-3.5" />
{isProcessing ? "Processing..." : "DISCOVER"}
</button>
<button
onClick={handleAnalyze}
disabled={isProcessing || !data.website || data.website === 'k.A.'}
className="flex-1 flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-xs font-bold py-2 rounded-md transition-all shadow-lg shadow-blue-900/20"
>
<Bot className="h-3.5 w-3.5" />
{isProcessing ? "Analyzing..." : "ANALYZE POTENTIAL"}
</button>
</div>
{/* Impressum / Legal Data */}
<div className="bg-slate-50 dark:bg-slate-950 rounded-lg p-4 border border-slate-200 dark:border-slate-800 flex flex-col gap-2">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<div className="p-1 bg-white dark:bg-slate-800 rounded text-slate-400">
<Briefcase className="h-3 w-3" />
</div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Official Legal Data</span>
</div>
<div className="flex items-center gap-2">
{scrapeDate && (
<div className="text-[10px] text-slate-500 flex items-center gap-1">
<Clock className="h-3 w-3" /> {new Date(scrapeDate).toLocaleDateString()}
</div>
)}
{/* Lock Button for Impressum */}
{scrapeEntry && (
<button
onClick={() => handleLockToggle('website_scrape', scrapeEntry.is_locked || false)}
className={clsx(
"p-1 rounded transition-colors",
scrapeEntry.is_locked
? "text-green-600 dark:text-green-400 hover:text-green-700"
: "text-slate-400 hover:text-slate-900 dark:hover:text-white"
)}
title={scrapeEntry.is_locked ? "Data Locked (Safe from auto-overwrite)" : "Unlocked (Auto-overwrite enabled)"}
>
{scrapeEntry.is_locked ? <Lock className="h-3.5 w-3.5" /> : <Unlock className="h-3.5 w-3.5" />}
</button>
)}
{!isEditingImpressum ? (
<button
onClick={() => { setImpressumUrlInput(""); setIsEditingImpressum(true); }}
className="p-1 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors"
title="Set Impressum URL Manually"
>
<Pencil className="h-3 w-3" />
</button>
) : ( <div className="flex items-center gap-1 animate-in fade-in zoom-in duration-200">
<button
onClick={handleImpressumOverride}
className="p-1 bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 rounded hover:bg-green-200 dark:hover:bg-green-900 transition-colors"
>
<Check className="h-3 w-3" />
</button>
<button
onClick={() => setIsEditingImpressum(false)}
className="p-1 text-slate-500 hover:text-red-500 transition-colors"
>
<X className="h-3 w-3" />
</button>
</div>
)}
</div>
</div>
{isEditingImpressum && (
<div className="mb-2 animate-in slide-in-from-top-1 duration-200">
<input
type="text"
value={impressumUrlInput}
onChange={e => 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
/>
</div>
)}
{impressum ? (
<>
<div className="text-sm font-medium text-slate-900 dark:text-white">
{impressum.legal_name || "Unknown Legal Name"}
</div>
<div className="flex items-start gap-2 text-xs text-slate-500 dark:text-slate-400">
<MapPin className="h-3 w-3 mt-0.5 shrink-0" />
<div>
<div>{impressum.street}</div>
<div>{impressum.zip} {impressum.city}</div>
</div>
</div>
{(impressum.email || impressum.phone) && (
<div className="mt-2 pt-2 border-t border-slate-200 dark:border-slate-900 flex flex-wrap gap-4 text-[10px] text-slate-500 font-mono">
{impressum.email && <span>{impressum.email}</span>}
{impressum.phone && <span>{impressum.phone}</span>}
{impressum.vat_id && <span className="text-blue-600 dark:text-blue-400/80">VAT: {impressum.vat_id}</span>}
</div>
)}
</>
) : !isEditingImpressum && (
<div className="text-[10px] text-slate-500 italic py-2">
No legal data found. Click pencil to provide direct Impressum link.
</div>
)}
</div>
{/* AI Analysis Dossier */}
{aiAnalysis && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider flex items-center gap-2">
<Bot className="h-4 w-4" /> AI Strategic Dossier
</h3>
{aiDate && (
<div className="text-[10px] text-slate-500 flex items-center gap-1">
<Clock className="h-3 w-3" /> {new Date(aiDate).toLocaleDateString()}
</div>
)}
</div>
<div className="bg-white dark:bg-slate-800/30 rounded-xl p-5 border border-slate-200 dark:border-slate-800/50 space-y-4 shadow-sm">
<div>
<div className="text-[10px] text-blue-600 dark:text-blue-400 uppercase font-bold tracking-tight mb-1">Business Model</div>
<p className="text-sm text-slate-700 dark:text-slate-200 leading-relaxed">{aiAnalysis.business_model || "No summary available."}</p>
</div>
{aiAnalysis.infrastructure_evidence && (
<div className="pt-4 border-t border-slate-200 dark:border-slate-800/50">
<div className="text-[10px] text-orange-600 dark:text-orange-400 uppercase font-bold tracking-tight mb-1">Infrastructure Evidence</div>
<p className="text-sm text-slate-600 dark:text-slate-300 italic leading-relaxed">"{aiAnalysis.infrastructure_evidence}"</p>
</div>
)}
</div>
</div>
)}
{/* Wikipedia Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider flex items-center gap-2">
<Globe className="h-4 w-4" /> Company Profile (Wikipedia)
</h3>
<div className="flex items-center gap-2">
{wikiDate && (
<div className="text-[10px] text-slate-500 flex items-center gap-1 mr-2">
<Clock className="h-3 w-3" /> {new Date(wikiDate).toLocaleDateString()}
</div>
)}
{/* Lock Button for Wiki */}
{wikiEntry && (
<button
onClick={() => handleLockToggle('wikipedia', wikiEntry.is_locked || false)}
className={clsx(
"p-1 rounded transition-colors mr-1",
wikiEntry.is_locked
? "text-green-600 dark:text-green-400 hover:text-green-700"
: "text-slate-400 hover:text-slate-900 dark:hover:text-white"
)}
title={wikiEntry.is_locked ? "Wiki Data Locked" : "Wiki Data Unlocked"}
>
{wikiEntry.is_locked ? <Lock className="h-3.5 w-3.5" /> : <Unlock className="h-3.5 w-3.5" />}
</button>
)}
{!isEditingWiki ? (
<button
onClick={() => { setWikiUrlInput(wiki?.url || ""); setIsEditingWiki(true); }}
className="p-1 text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
title="Edit / Override URL"
>
<Pencil className="h-3.5 w-3.5" />
</button>
) : ( <div className="flex items-center gap-1">
<button
onClick={handleWikiOverride}
className="p-1 bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 rounded hover:bg-green-200 dark:hover:bg-green-900 transition-colors"
title="Save & Rescan"
>
<Check className="h-3.5 w-3.5" />
</button>
<button
onClick={() => setIsEditingWiki(false)}
className="p-1 text-slate-500 hover:text-red-500 transition-colors"
title="Cancel"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
)}
</div>
</div>
{isEditingWiki && (
<div className="mb-2">
<input
type="text"
value={wikiUrlInput}
onChange={e => 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"
/>
<p className="text-[10px] text-slate-500 mt-1">Paste a valid URL. Saving will trigger a re-scan.</p>
</div>
)}
{wiki && wiki.url !== 'k.A.' && !isEditingWiki ? (
<div>
{/* ... existing wiki content ... */}
<div className="bg-white dark:bg-slate-800/30 rounded-xl p-5 border border-slate-200 dark:border-slate-800/50 relative overflow-hidden shadow-sm">
<div className="absolute top-0 right-0 p-3 opacity-10">
<Globe className="h-16 w-16 text-slate-900 dark:text-white" />
</div>
{isLocked && (
<div className="absolute top-2 right-2 flex items-center gap-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800/50 rounded text-[9px] text-yellow-600 dark:text-yellow-500">
<Tag className="h-2.5 w-2.5" /> Manual Override
</div>
)}
<p className="text-sm text-slate-600 dark:text-slate-300 leading-relaxed italic mb-4">
"{wiki.first_paragraph}"
</p>
<div className="grid grid-cols-2 gap-y-4 gap-x-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-blue-500">
<Users className="h-4 w-4" />
</div>
<div>
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Employees</div>
<div className="text-sm text-slate-700 dark:text-slate-200 font-medium">{wiki.mitarbeiter || 'k.A.'}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-green-500">
<DollarSign className="h-4 w-4" />
</div>
<div>
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Revenue</div>
<div className="text-sm text-slate-700 dark:text-slate-200 font-medium">{wiki.umsatz && wiki.umsatz !== 'k.A.' ? `${wiki.umsatz} Mio. €` : 'k.A.'}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-orange-500">
<MapPin className="h-4 w-4" />
</div>
<div>
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Headquarters</div>
<div className="text-sm text-slate-700 dark:text-slate-200 font-medium">{wiki.sitz_stadt}{wiki.sitz_land ? `, ${wiki.sitz_land}` : ''}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-purple-500">
<Briefcase className="h-4 w-4" />
</div>
<div>
<div className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Wiki Industry</div>
<div className="text-sm text-slate-700 dark:text-slate-200 font-medium truncate max-w-[150px]" title={wiki.branche}>{wiki.branche || 'k.A.'}</div>
</div>
</div>
</div>
{wiki.categories && wiki.categories !== 'k.A.' && (
<div className="mt-6 pt-5 border-t border-slate-200 dark:border-slate-800/50">
<div className="flex items-start gap-2 text-xs text-slate-500 mb-2">
<Tag className="h-3 w-3 mt-0.5" /> Categories
</div>
<div className="flex flex-wrap gap-1.5">
{wiki.categories.split(',').map((cat: string) => (
<span key={cat} className="px-2 py-0.5 bg-slate-100 dark:bg-slate-900 text-slate-600 dark:text-slate-400 rounded-full text-[10px] border border-slate-200 dark:border-slate-800">
{cat.trim()}
</span>
))}
</div>
</div>
)}
<div className="mt-4 flex justify-end">
<a href={wiki.url} target="_blank" className="text-[10px] text-blue-600 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 flex items-center gap-1 font-bold">
WIKIPEDIA <ExternalLink className="h-2.5 w-2.5" />
</a>
</div>
</div>
</div>
) : !isEditingWiki ? (
<div className="p-4 rounded-xl border border-dashed border-slate-200 dark:border-slate-800 text-center text-slate-500 dark:text-slate-600">
<Globe className="h-5 w-5 mx-auto mb-2 opacity-20" />
<p className="text-xs">No Wikipedia profile found yet.</p>
</div>
) : null}
</div>
{/* Robotics Scorecard */}
<div>
<h3 className="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-3 flex items-center gap-2">
<Bot className="h-4 w-4" /> Robotics Potential
</h3>
<div className="grid grid-cols-2 gap-4">
{['cleaning', 'transport', 'security', 'service'].map(type => {
const sig = data.signals.find(s => s.signal_type.includes(type))
const score = sig ? sig.confidence : 0
return (
<div key={type} className="bg-slate-50 dark:bg-slate-800/50 p-3 rounded-lg border border-slate-200 dark:border-slate-700">
<div className="flex justify-between mb-1">
<span className="text-sm text-slate-700 dark:text-slate-300 capitalize">{type}</span>
<span className={clsx("text-sm font-bold", score > 70 ? "text-green-600 dark:text-green-400" : score > 30 ? "text-yellow-600 dark:text-yellow-400" : "text-slate-500")}>
{score}%
</span>
</div>
<div className="w-full bg-slate-200 dark:bg-slate-700 h-1.5 rounded-full overflow-hidden">
<div
className={clsx("h-full rounded-full", score > 70 ? "bg-green-500" : score > 30 ? "bg-yellow-500" : "bg-slate-500")}
style={{ width: `${score}%` }}
/>
</div>
{sig?.proof_text && (
<p className="text-xs text-slate-500 dark:text-slate-500 mt-2 line-clamp-2" title={sig.proof_text}>
"{sig.proof_text}"
</p>
)}
</div>
)
})}
</div>
</div>
{/* Meta Info */}
<div className="pt-6 border-t border-slate-200 dark:border-slate-800 flex items-center justify-between">
<div className="text-[10px] text-slate-500 flex items-center gap-2 uppercase font-bold tracking-widest">
<Calendar className="h-3 w-3" /> Added: {new Date(data.created_at).toLocaleDateString()}
</div>
<div className="text-[10px] text-slate-400 dark:text-slate-600 italic">
ID: CE-{data.id.toString().padStart(4, '0')}
</div>
</div>
</>
)}
{activeTab === 'contacts' && (
<ContactsManager
contacts={data.contacts}
initialContactId={initialContactId}
onAddContact={handleAddContact}
onEditContact={handleEditContact}
/>
)} </div>
</div>
)}
</div>
)
}