feat(company-explorer): Initial Web UI & Backend with Enrichment Flow
This commit introduces the foundational elements for the new "Company Explorer" web application, marking a significant step away from the legacy Google Sheets / CLI system. Key changes include: - Project Structure: A new directory with separate (FastAPI) and (React/Vite) components. - Data Persistence: Migration from Google Sheets to a local SQLite database () using SQLAlchemy. - Core Utilities: Extraction and cleanup of essential helper functions (LLM wrappers, text utilities) into . - Backend Services: , , for AI-powered analysis, and logic. - Frontend UI: Basic React application with company table, import wizard, and dynamic inspector sidebar. - Docker Integration: Updated and for multi-stage builds and sideloading. - Deployment & Access: Integrated into central Nginx proxy and dashboard, accessible via . Lessons Learned & Fixed during development: - Frontend Asset Loading: Addressed issues with Vite's path and FastAPI's . - TypeScript Configuration: Added and . - Database Schema Evolution: Solved errors by forcing a new database file and correcting override. - Logging: Implemented robust file-based logging (). This new foundation provides a powerful and maintainable platform for future B2B robotics lead generation.
This commit is contained in:
123
company-explorer/frontend/src/components/Inspector.tsx
Normal file
123
company-explorer/frontend/src/components/Inspector.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { X, ExternalLink, Robot, Briefcase, Calendar } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
interface InspectorProps {
|
||||
companyId: number | null
|
||||
onClose: () => void
|
||||
apiBase: string
|
||||
}
|
||||
|
||||
type Signal = {
|
||||
signal_type: string
|
||||
confidence: number
|
||||
value: string
|
||||
proof_text: string
|
||||
}
|
||||
|
||||
type CompanyDetail = {
|
||||
id: number
|
||||
name: string
|
||||
website: string | null
|
||||
industry_ai: string | null
|
||||
status: string
|
||||
created_at: string
|
||||
signals: Signal[]
|
||||
}
|
||||
|
||||
export function Inspector({ companyId, onClose, apiBase }: InspectorProps) {
|
||||
const [data, setData] = useState<CompanyDetail | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!companyId) return
|
||||
setLoading(true)
|
||||
axios.get(`${apiBase}/companies/${companyId}`)
|
||||
.then(res => setData(res.data))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false))
|
||||
}, [companyId])
|
||||
|
||||
if (!companyId) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-y-0 right-0 w-[500px] bg-slate-900 border-l border-slate-800 shadow-2xl transform transition-transform duration-300 ease-in-out z-40 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-800 bg-slate-950/50">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h2 className="text-xl font-bold text-white leading-tight">{data.name}</h2>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-white">
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-sm">
|
||||
{data.website && (
|
||||
<a href={data.website} target="_blank" className="flex items-center gap-1 text-blue-400 hover:underline">
|
||||
<ExternalLink className="h-3 w-3" /> {new URL(data.website).hostname.replace('www.', '')}
|
||||
</a>
|
||||
)}
|
||||
{data.industry_ai && (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 bg-slate-800 text-slate-300 rounded border border-slate-700">
|
||||
<Briefcase className="h-3 w-3" /> {data.industry_ai}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Robotics Scorecard */}
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||
<Robot 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-800/50 p-3 rounded-lg border border-slate-700">
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-sm text-slate-300 capitalize">{type}</span>
|
||||
<span className={clsx("text-sm font-bold", score > 70 ? "text-green-400" : score > 30 ? "text-yellow-400" : "text-slate-500")}>
|
||||
{score}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full 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-600")}
|
||||
style={{ width: `${score}%` }}
|
||||
/>
|
||||
</div>
|
||||
{sig?.proof_text && (
|
||||
<p className="text-xs 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-800">
|
||||
<div className="text-xs text-slate-500 flex items-center gap-2">
|
||||
<Calendar className="h-3 w-3" /> Added: {new Date(data.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user