import { useEffect, useState, useMemo } from 'react' import axios from 'axios' import { X, Bot, Tag, Target, Users, Plus, Trash2, Save, Flag, Check, Ban, ExternalLink, ChevronDown, Grid, Database, Download, UploadCloud, Play, AlertTriangle, Lightbulb, Sparkles } from 'lucide-react' import clsx from 'clsx' import { MarketingMatrixManager } from './MarketingMatrixManager' interface RoboticsSettingsProps { isOpen: boolean onClose: () => void apiBase: string } type JobRolePatternType = { id: number; pattern_type: 'exact' | 'regex'; pattern_value: string; role: string; // Should match Persona.name priority: number; is_active: boolean; created_by: string; created_at: string; updated_at: string; } type RawJobTitleType = { id: number; title: string; count: number; source: string; is_mapped: boolean; created_at: string; updated_at: string; } type ReportedMistake = { id: number; company_id: number; company: { name: string }; // Assuming company name is eagerly loaded field_name: string; wrong_value: string | null; corrected_value: string | null; source_url: string | null; quote: string | null; user_comment: string | null; status: 'PENDING' | 'APPROVED' | 'REJECTED'; created_at: string; updated_at: string; } type SuggestionItem = { word: string, count: number }; type RoleSuggestions = Record; type OptimizationProposal = { target_role: string; regex: string; explanation: string; priority: number; covered_pattern_ids: number[]; covered_titles: string[]; false_positives: string[]; } export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsProps) { const [activeTab, setActiveTab] = useState<'robotics' | 'industries' | 'roles' | 'matrix' | 'mistakes' | 'database'>( localStorage.getItem('roboticsSettingsActiveTab') as 'robotics' | 'industries' | 'roles' | 'matrix' | 'mistakes' | 'database' || 'robotics' ) const [roboticsCategories, setRoboticsCategories] = useState([]) const [industries, setIndustries] = useState([]) const [jobRoles, setJobRoles] = useState([]) const [rawJobTitles, setRawJobTitles] = useState([]) const [reportedMistakes, setReportedMistakes] = useState([]) const [suggestions, setSuggestions] = useState({}); const [optimizationProposals, setOptimizationProposals] = useState([]); const [showOptimizationModal, setShowOptimizationModal] = useState(false); const [currentMistakeStatusFilter, setCurrentMistakeStatusFilter] = useState("PENDING"); const [isLoading, setIsLoading] = useState(false); const [isClassifying, setIsClassifying] = useState(false); const [isOptimizing, setIsOptimizing] = useState(false); const [roleSearch, setRoleSearch] = useState(""); // Database & Regex State const [fileToUpload, setFileToUpload] = useState(null); const [uploadStatus, setUploadStatus] = useState(""); const [testPattern, setTestPattern] = useState(""); const [testPatternType, setTestPatternType] = useState("regex"); const [testString, setTestString] = useState(""); const [testResult, setTestResult] = useState(null); const [testError, setTestError] = useState(null); const groupedAndFilteredRoles = useMemo(() => { const grouped = jobRoles.reduce((acc: Record, role) => { const key = role.role || 'Unassigned'; if (!acc[key]) { acc[key] = []; } acc[key].push(role); return acc; }, {} as Record); if (!roleSearch) { return grouped; } const filtered = {} as Record; for (const roleName in grouped) { const roles = grouped[roleName].filter((r: JobRolePatternType) => r.pattern_value.toLowerCase().includes(roleSearch.toLowerCase()) ); if (roles.length > 0) { filtered[roleName] = roles; } } return filtered; }, [jobRoles, roleSearch]); const fetchAllData = async () => { setIsLoading(true); try { const [resRobotics, resIndustries, resJobRoles, resRawTitles, resMistakes, resSuggestions] = await Promise.all([ axios.get(`${apiBase}/robotics/categories`), axios.get(`${apiBase}/industries`), axios.get(`${apiBase}/job_roles`), axios.get(`${apiBase}/job_roles/raw?unmapped_only=true`), // Ensure we only get unmapped axios.get(`${apiBase}/mistakes?status=${currentMistakeStatusFilter}`), axios.get(`${apiBase}/job_roles/suggestions`), ]); setRoboticsCategories(resRobotics.data); setIndustries(resIndustries.data); setJobRoles(resJobRoles.data); setRawJobTitles(resRawTitles.data); setReportedMistakes(resMistakes.data.items); setSuggestions(resSuggestions.data); } catch (e) { console.error("Failed to fetch settings data:", e); alert("Fehler beim Laden der Settings. Siehe Konsole."); } finally { setIsLoading(false); } }; useEffect(() => { if (isOpen) { fetchAllData(); } }, [isOpen]); useEffect(() => { if (isOpen) { // Refetch mistakes when filter changes const fetchMistakes = async () => { setIsLoading(true); try { const res = await axios.get(`${apiBase}/mistakes?status=${currentMistakeStatusFilter}`); setReportedMistakes(res.data.items); } catch (e) { console.error(e) } finally { setIsLoading(false) } } fetchMistakes(); } }, [currentMistakeStatusFilter]); useEffect(() => { localStorage.setItem('roboticsSettingsActiveTab', activeTab); }, [activeTab]); const handleUpdateRobotics = async (id: number, description: string, reasoning: string) => { setIsLoading(true); try { await axios.put(`${apiBase}/robotics/categories/${id}`, { description, reasoning_guide: reasoning }); fetchAllData(); } catch (e) { alert("Update failed"); console.error(e); } finally { setIsLoading(false); } } const handleUpdateMistakeStatus = async (mistakeId: number, newStatus: 'APPROVED' | 'REJECTED') => { setIsLoading(true); try { await axios.put(`${apiBase}/mistakes/${mistakeId}`, { status: newStatus }); fetchAllData(); // Refresh all data, including mistakes } catch (e) { alert("Failed to update mistake status"); console.error(e); } finally { setIsLoading(false); } }; const handleBatchClassify = async () => { if (!window.confirm(`This will send all ${rawJobTitles.length} unmapped job titles to the AI for classification. This may take a few minutes. Continue?`)) { return; } setIsClassifying(true); try { await axios.post(`${apiBase}/job_roles/classify-batch`); alert("Batch classification started in the background. The list will update automatically as titles are processed. You can close this window."); } catch (e) { alert("Failed to start batch classification."); console.error(e); } finally { setIsClassifying(false); } }; const handleOptimizePatterns = async () => { setIsOptimizing(true); setOptimizationProposals([]); setShowOptimizationModal(true); try { // 1. Start Task await axios.post(`${apiBase}/job_roles/optimize-start`); // 2. Poll for Status const pollInterval = 2000; // 2 seconds const maxAttempts = 150; // 5 minutes let attempts = 0; const checkStatus = async () => { if (attempts >= maxAttempts) { alert("Optimization timed out. Please check logs."); setIsOptimizing(false); return; } try { const res = await axios.get(`${apiBase}/job_roles/optimize-status`); const status = res.data.state; if (status === 'completed') { setOptimizationProposals(res.data.result); setIsOptimizing(false); } else if (status === 'error') { alert(`Optimization failed: ${res.data.error}`); setIsOptimizing(false); } else { attempts++; setTimeout(checkStatus, pollInterval); } } catch (e) { console.error("Polling error", e); setIsOptimizing(false); } }; setTimeout(checkStatus, 1000); } catch (e) { alert("Failed to start optimization."); console.error(e); setShowOptimizationModal(false); setIsOptimizing(false); } }; const handleApplyOptimization = async (proposal: OptimizationProposal) => { try { await axios.post(`${apiBase}/job_roles/apply-optimization`, { target_role: proposal.target_role, regex: proposal.regex, priority: proposal.priority, ids_to_delete: proposal.covered_pattern_ids }); // Remove applied proposal from list setOptimizationProposals(prev => prev.filter(p => p.regex !== proposal.regex)); fetchAllData(); // Refresh main list } catch (e) { alert("Failed to apply optimization."); console.error(e); } }; const handleUpdateJobRole = async (roleId: number, field: string, value: any) => { const roleToUpdate = jobRoles.find(r => r.id === roleId); if (!roleToUpdate) return; const updatedRole = { ...roleToUpdate, [field]: value }; // Convert priority to number just in case if (field === 'priority') { updatedRole.priority = parseInt(value, 10); } try { await axios.put(`${apiBase}/job_roles/${roleId}`, updatedRole); // Optimistic update setJobRoles(jobRoles.map(r => r.id === roleId ? updatedRole : r)); } catch (e) { alert("Failed to update job role"); console.error(e); } }; const handleAddJobRole = async (value: string, type: 'exact' | 'regex' = 'exact', roleName?: string) => { setIsLoading(true); try { await axios.post(`${apiBase}/job_roles`, { pattern_type: type, pattern_value: value, role: roleName || "Influencer", priority: type === 'regex' ? 80 : 100 }); fetchAllData(); } catch (e) { alert("Failed to add job role"); console.error(e); } finally { setIsLoading(false); } } const handleDeleteJobRole = async (id: number) => { if (!window.confirm("Are you sure you want to delete this pattern?")) { return; } setIsLoading(true); try { await axios.delete(`${apiBase}/job_roles/${id}`); fetchAllData(); } catch (e) { alert("Failed to delete job role"); console.error(e); } finally { setIsLoading(false); } } const handleDownloadDb = () => { window.location.href = `${apiBase}/admin/database/download`; }; const handleFileSelect = (event: React.ChangeEvent) => { if (event.target.files && event.target.files[0]) { setFileToUpload(event.target.files[0]); setUploadStatus(""); } }; const handleUploadDb = async () => { if (!fileToUpload) return; if (!window.confirm("WARNING: This will overwrite the current database! A backup will be created, but any recent changes might be lost. You MUST restart the container afterwards. Continue?")) return; const formData = new FormData(); formData.append("file", fileToUpload); setUploadStatus("uploading"); try { await axios.post(`${apiBase}/admin/database/upload`, formData, { headers: { "Content-Type": "multipart/form-data" }, }); setUploadStatus("success"); alert("Upload successful! Please RESTART the Docker container to apply changes."); } catch (e: any) { console.error(e); setUploadStatus("error"); alert(`Upload failed: ${e.response?.data?.detail || e.message}`); } }; const handleTestPattern = async () => { setTestResult(null); setTestError(null); if (!testPattern || !testString) return; try { const res = await axios.post(`${apiBase}/job_roles/test-pattern`, { pattern: testPattern, pattern_type: testPatternType, test_string: testString }); if (res.data.error) { setTestError(res.data.error); } else { setTestResult(res.data.match); } } catch (e: any) { setTestError(e.message); } }; if (!isOpen) return null return (
{/* Header */}

Settings & Classification Logic

Define how AI evaluates leads and matches roles.

{/* Tab Nav */}
{[ { id: 'robotics', label: 'Robotics Potential', icon: Bot }, { id: 'industries', label: 'Industry Focus', icon: Target }, { id: 'roles', label: 'Job Role Mapping', icon: Users }, { id: 'matrix', label: 'Marketing Matrix', icon: Grid }, { id: 'mistakes', label: 'Reported Mistakes', icon: Flag }, { id: 'database', label: 'Database & Regex', icon: Database }, ].map(t => ( ))}
{/* Content */}
{isLoading &&
Loading...
}
{roboticsCategories.map(cat => ( ))}

Industry Verticals (Synced from Notion)

{industries.map(ind => (
{ind.notion_id && (
SYNCED
)}

{ind.name}

{ind.priority && ( {ind.priority} )} {ind.ops_focus_secondary && ( SEC-PRODUCT )}
{ind.is_focus ? "Focus" : "Standard"}

{ind.description || "No definition"}

{(ind.pains || ind.gains) && (
{ind.pains && (
Pains
{ind.pains}
)} {ind.gains && (
Gains
{ind.gains}
)}
)} {ind.notes && (
Notes: {ind.notes}
)}
Whale >{ind.whale_threshold || "-"}
Min Req{ind.min_requirement || "-"}
Unit{ind.scraper_search_term || "-"}
Product {roboticsCategories.find(c => c.id === ind.primary_category_id)?.name || "-"} {ind.secondary_category_id && (
Sec. Prod {roboticsCategories.find(c => c.id === ind.secondary_category_id)?.name || "-"}
)}
{ind.scraper_keywords &&
Keywords:{ind.scraper_keywords}
} {ind.standardization_logic &&
Standardization:{ind.standardization_logic}
}
))}
{/* Existing Patterns Grouped */}
setRoleSearch(e.target.value)} className="w-full bg-white dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-md px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none" />
{Object.keys(groupedAndFilteredRoles).sort().map(roleName => (
{roleName} ({groupedAndFilteredRoles[roleName].length} patterns)
{/* AI Suggestions Area */} {suggestions[roleName] && suggestions[roleName].length > 0 && (
AI Suggestions (Common Keywords)
{suggestions[roleName].map(s => ( ))}
)} {groupedAndFilteredRoles[roleName].map((role: JobRolePatternType) => ( ))}
Type Pattern Value Priority
handleUpdateJobRole(role.id, 'pattern_value', e.target.value)} /> handleUpdateJobRole(role.id, 'priority', e.target.value)} />
))}
{/* Discovery Inbox */}

Discovery Inbox

Unmapped job titles from CRM, prioritized by frequency

{rawJobTitles.length > 0 && ( )}
{rawJobTitles.map((raw: RawJobTitleType) => ( ))} {rawJobTitles.length === 0 && ()}
Job Title from CRMFrequency
{raw.title} {raw.count}x
Discovery inbox is empty. Import raw job titles to see data here.
{/* ... existing mistakes content ... */}

Reported Data Mistakes

Filter:
{reportedMistakes.length > 0 ? ( reportedMistakes.map(mistake => ( )) ) : ( )}
Company Field Wrong Value Corrected Value Source / Quote / Comment Status Actions
{mistake.company.name} {mistake.field_name} {mistake.wrong_value || '-'} {mistake.corrected_value || '-'} {mistake.source_url && Source} {mistake.quote &&

"{mistake.quote}"

} {mistake.user_comment &&

Comment: {mistake.user_comment}

}
{mistake.status} {mistake.status === "PENDING" && (
)}
No reported mistakes found.
{/* Regex Tester */}

Regex Tester

Validate your patterns before adding them to the database.

setTestPattern(e.target.value)} placeholder="e.g. (leiter|head).{0,15}vertrieb" className="flex-1 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded p-2 text-xs font-mono text-slate-800 dark:text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none" />
setTestString(e.target.value)} placeholder="e.g. Leiter Vertrieb und Marketing" className="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded p-2 text-xs text-slate-800 dark:text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none" />
{testResult !== null && ( {testResult ? : } {testResult ? "MATCH" : "NO MATCH"} )} {testError && {testError}}
{/* Database Management */}

Database Management

Download the full database for offline analysis or restore a backup.

{/* Download */}

Export

{/* Upload */}

Restore / Import

Warning: Uploading will overwrite the current database. A backup will be created automatically.
{/* Optimization Modal */} {showOptimizationModal && (

Pattern Optimization Proposals

AI-generated Regex suggestions to consolidate exact matches.

{isOptimizing ? (

Analyzing patterns & checking for conflicts...

) : optimizationProposals.length === 0 ? (

No optimization opportunities found. Your patterns are already efficient!

) : (
{optimizationProposals.map((prop, idx) => (
{prop.target_role} Priority: {prop.priority}

{prop.regex}

{prop.explanation}

Covers ({prop.covered_titles.length})
{prop.covered_titles.map(t => ( {t} ))}
{prop.false_positives.length > 0 ? (
Conflicts (Matches other roles!)
{prop.false_positives.map(t => ( {t} ))}
) : (
No conflicts with other roles detected.
)}
))}
)}
)}
) } function CategoryCard({ category, onSave }: { category: any, onSave: any }) { const [desc, setDesc] = useState(category.description) const [guide, setGuide] = useState(category.reasoning_guide) const [isDirty, setIsDirty] = useState(false) useEffect(() => { setIsDirty(desc !== category.description || guide !== category.reasoning_guide) }, [desc, guide]) return (
{category.name}