[31188f42] einfügen

einfügen
This commit is contained in:
2026-02-24 06:47:35 +00:00
parent 3d34436f16
commit e39c745a78
21 changed files with 1575 additions and 152 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Company Explorer (Robotics)</title>
<script type="module" crossorigin src="/ce/assets/index-tQU9lyIc.js"></script>
<link rel="stylesheet" crossorigin href="/ce/assets/index-BgxQoHsm.css">
</head>
<body class="bg-slate-950 text-slate-100">
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,279 @@
import { useEffect, useState, useMemo } from 'react'
import axios from 'axios'
import { Search, Edit2, X, Check, Filter, Download } from 'lucide-react'
import clsx from 'clsx'
interface MarketingMatrixManagerProps {
apiBase: string
}
interface MatrixEntry {
id: number
industry_id: number
persona_id: number
industry_name: string
persona_name: string
subject: string | null
intro: string | null
social_proof: string | null
updated_at: string
}
interface Industry {
id: number
name: string
}
interface Persona {
id: number
name: string
}
export function MarketingMatrixManager({ apiBase }: MarketingMatrixManagerProps) {
const [entries, setEntries] = useState<MatrixEntry[]>([])
const [industries, setIndustries] = useState<Industry[]>([])
const [personas, setPersonas] = useState<Persona[]>([])
const [isLoading, setIsLoading] = useState(false)
// Filters
const [industryFilter, setIndustryFilter] = useState<number | 'all'>('all')
const [personaFilter, setPersonaFilter] = useState<number | 'all'>('all')
const [searchTerm, setSearchTerm] = useState('')
// Editing state
const [editingId, setEditingId] = useState<number | null>(null)
const [editValues, setEditValues] = useState<{
subject: string
intro: string
social_proof: string
}>({ subject: '', intro: '', social_proof: '' })
const fetchMetadata = async () => {
try {
const [resInd, resPers] = await Promise.all([
axios.get(`${apiBase}/industries`),
axios.get(`${apiBase}/matrix/personas`)
])
setIndustries(resInd.data)
setPersonas(resPers.data)
} catch (e) {
console.error("Failed to fetch metadata:", e)
}
}
const fetchEntries = async () => {
setIsLoading(true)
try {
const params: any = {}
if (industryFilter !== 'all') params.industry_id = industryFilter
if (personaFilter !== 'all') params.persona_id = personaFilter
const res = await axios.get(`${apiBase}/matrix`, { params })
setEntries(res.data)
} catch (e) {
console.error("Failed to fetch matrix entries:", e)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
fetchMetadata()
}, [])
useEffect(() => {
fetchEntries()
}, [industryFilter, personaFilter])
const filteredEntries = useMemo(() => {
if (!searchTerm) return entries
const s = searchTerm.toLowerCase()
return entries.filter(e =>
e.industry_name.toLowerCase().includes(s) ||
e.persona_name.toLowerCase().includes(s) ||
(e.subject?.toLowerCase().includes(s)) ||
(e.intro?.toLowerCase().includes(s)) ||
(e.social_proof?.toLowerCase().includes(s))
)
}, [entries, searchTerm])
const startEditing = (entry: MatrixEntry) => {
setEditingId(entry.id)
setEditValues({
subject: entry.subject || '',
intro: entry.intro || '',
social_proof: entry.social_proof || ''
})
}
const cancelEditing = () => {
setEditingId(null)
}
const saveEditing = async (id: number) => {
try {
await axios.put(`${apiBase}/matrix/${id}`, editValues)
setEntries(prev => prev.map(e => e.id === id ? { ...e, ...editValues } : e))
setEditingId(null)
} catch (e) {
alert("Save failed")
console.error(e)
}
}
const handleDownloadCSV = () => {
let url = `${apiBase}/matrix/export`
const params = new URLSearchParams()
if (industryFilter !== 'all') params.append('industry_id', industryFilter.toString())
if (personaFilter !== 'all') params.append('persona_id', personaFilter.toString())
if (params.toString()) {
url += `?${params.toString()}`
}
window.open(url, '_blank')
}
return (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex flex-wrap items-center gap-3 bg-slate-50 dark:bg-slate-950 p-3 rounded-lg border border-slate-200 dark:border-slate-800">
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-slate-400" />
<span className="text-xs font-bold text-slate-500 uppercase">Filters:</span>
</div>
<select
className="bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-2 py-1.5 text-xs outline-none focus:ring-1 focus:ring-blue-500"
value={industryFilter}
onChange={e => setIndustryFilter(e.target.value === 'all' ? 'all' : parseInt(e.target.value))}
>
<option value="all">All Industries</option>
{industries.map(i => <option key={i.id} value={i.id}>{i.name}</option>)}
</select>
<select
className="bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-2 py-1.5 text-xs outline-none focus:ring-1 focus:ring-blue-500"
value={personaFilter}
onChange={e => setPersonaFilter(e.target.value === 'all' ? 'all' : parseInt(e.target.value))}
>
<option value="all">All Personas</option>
{personas.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
<div className="flex-1 min-w-[200px] relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-slate-400" />
<input
type="text"
placeholder="Search in texts..."
className="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-9 py-1.5 text-xs outline-none focus:ring-1 focus:ring-blue-500"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<button
onClick={handleDownloadCSV}
className="flex items-center gap-2 bg-slate-200 dark:bg-slate-800 hover:bg-slate-300 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 px-3 py-1.5 rounded text-xs font-bold transition-all border border-slate-300 dark:border-slate-700"
>
<Download className="h-3.5 w-3.5" />
EXPORT CSV
</button>
</div>
{/* Table */}
<div className="border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden bg-white dark:bg-slate-950">
<table className="w-full text-left text-xs table-fixed">
<thead className="bg-slate-50 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase">
<tr>
<th className="p-3 w-40">Combination</th>
<th className="p-3 w-1/4">Subject Line</th>
<th className="p-3 w-1/3">Intro Text</th>
<th className="p-3">Social Proof</th>
<th className="p-3 w-20 text-center">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
{isLoading ? (
<tr><td colSpan={5} className="p-12 text-center text-slate-400 italic">Loading matrix entries...</td></tr>
) : filteredEntries.length === 0 ? (
<tr><td colSpan={5} className="p-12 text-center text-slate-400 italic">No entries found for the selected filters.</td></tr>
) : filteredEntries.map(entry => (
<tr key={entry.id} className={clsx("group transition-colors", editingId === entry.id ? "bg-blue-50/50 dark:bg-blue-900/10" : "hover:bg-slate-50/50 dark:hover:bg-slate-900/30")}>
<td className="p-3 align-top">
<div className="font-bold text-slate-900 dark:text-white leading-tight mb-1">{entry.industry_name}</div>
<div className="text-[10px] text-blue-600 dark:text-blue-400 font-bold uppercase tracking-wider">{entry.persona_name}</div>
</td>
<td className="p-3 align-top">
{editingId === entry.id ? (
<input
className="w-full bg-white dark:bg-slate-900 border border-blue-300 dark:border-blue-700 rounded p-1.5 outline-none"
value={editValues.subject}
onChange={e => setEditValues(v => ({ ...v, subject: e.target.value }))}
/>
) : (
<div className="text-slate-700 dark:text-slate-300">{entry.subject || <span className="text-slate-400 italic">Empty</span>}</div>
)}
</td>
<td className="p-3 align-top">
{editingId === entry.id ? (
<textarea
className="w-full bg-white dark:bg-slate-900 border border-blue-300 dark:border-blue-700 rounded p-1.5 outline-none h-24 text-[11px]"
value={editValues.intro}
onChange={e => setEditValues(v => ({ ...v, intro: e.target.value }))}
/>
) : (
<div className="text-slate-600 dark:text-slate-400 line-clamp-4 hover:line-clamp-none transition-all">{entry.intro || <span className="text-slate-400 italic">Empty</span>}</div>
)}
</td>
<td className="p-3 align-top">
{editingId === entry.id ? (
<textarea
className="w-full bg-white dark:bg-slate-900 border border-blue-300 dark:border-blue-700 rounded p-1.5 outline-none h-24 text-[11px]"
value={editValues.social_proof}
onChange={e => setEditValues(v => ({ ...v, social_proof: e.target.value }))}
/>
) : (
<div className="text-slate-600 dark:text-slate-400 line-clamp-4 hover:line-clamp-none transition-all">{entry.social_proof || <span className="text-slate-400 italic">Empty</span>}</div>
)}
</td>
<td className="p-3 align-top text-center">
{editingId === entry.id ? (
<div className="flex flex-col gap-2">
<button
onClick={() => saveEditing(entry.id)}
className="p-1.5 bg-green-600 text-white rounded hover:bg-green-500 transition-colors shadow-sm"
title="Save Changes"
>
<Check className="h-4 w-4" />
</button>
<button
onClick={cancelEditing}
className="p-1.5 bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded hover:bg-slate-300 dark:hover:bg-slate-700 transition-colors"
title="Cancel"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<button
onClick={() => startEditing(entry)}
className="p-2 text-slate-400 hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-full transition-all opacity-0 group-hover:opacity-100"
title="Edit Entry"
>
<Edit2 className="h-4 w-4" />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useMemo } from 'react'
import axios from 'axios'
import { X, Bot, Tag, Target, Users, Plus, Trash2, Save, Flag, Check, Ban, ExternalLink } from 'lucide-react'
import { X, Bot, Tag, Target, Users, Plus, Trash2, Save, Flag, Check, Ban, ExternalLink, ChevronDown, Grid } from 'lucide-react'
import clsx from 'clsx'
import { MarketingMatrixManager } from './MarketingMatrixManager'
interface RoboticsSettingsProps {
isOpen: boolean
@@ -9,6 +10,28 @@ interface RoboticsSettingsProps {
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;
@@ -25,17 +48,45 @@ type ReportedMistake = {
}
export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsProps) {
const [activeTab, setActiveTab] = useState<'robotics' | 'industries' | 'roles' | 'mistakes'>(
localStorage.getItem('roboticsSettingsActiveTab') as 'robotics' | 'industries' | 'roles' | 'mistakes' || 'robotics'
const [activeTab, setActiveTab] = useState<'robotics' | 'industries' | 'roles' | 'mistakes' | 'matrix'>(
localStorage.getItem('roboticsSettingsActiveTab') as 'robotics' | 'industries' | 'roles' | 'mistakes' | 'matrix' || 'robotics'
)
const [roboticsCategories, setRoboticsCategories] = useState<any[]>([])
const [industries, setIndustries] = useState<any[]>([])
const [jobRoles, setJobRoles] = useState<any[]>([])
const [rawJobTitles, setRawJobTitles] = useState<any[]>([])
const [jobRoles, setJobRoles] = useState<JobRolePatternType[]>([])
const [rawJobTitles, setRawJobTitles] = useState<RawJobTitleType[]>([])
const [reportedMistakes, setReportedMistakes] = useState<ReportedMistake[]>([])
const [currentMistakeStatusFilter, setCurrentMistakeStatusFilter] = useState<string>("PENDING");
const [isLoading, setIsLoading] = useState(false);
const [isClassifying, setIsClassifying] = useState(false);
const [roleSearch, setRoleSearch] = useState("");
const groupedAndFilteredRoles = useMemo(() => {
const grouped = jobRoles.reduce((acc: Record<string, JobRolePatternType[]>, role) => {
const key = role.role || 'Unassigned';
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(role);
return acc;
}, {} as Record<string, JobRolePatternType[]>);
if (!roleSearch) {
return grouped;
}
const filtered = {} as Record<string, JobRolePatternType[]>;
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);
@@ -44,7 +95,7 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
axios.get(`${apiBase}/robotics/categories`),
axios.get(`${apiBase}/industries`),
axios.get(`${apiBase}/job_roles`),
axios.get(`${apiBase}/job_roles/raw`),
axios.get(`${apiBase}/job_roles/raw?unmapped_only=true`), // Ensure we only get unmapped
axios.get(`${apiBase}/mistakes?status=${currentMistakeStatusFilter}`),
]);
setRoboticsCategories(resRobotics.data);
@@ -66,6 +117,21 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
}
}, [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]);
@@ -97,10 +163,56 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
}
};
const handleAddJobRole = async () => {
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.");
// Optionally, you can poll for completion or just let the user see the number go down on next refresh.
// For now, we just inform the user.
} catch (e) {
alert("Failed to start batch classification.");
console.error(e);
} finally {
setIsClassifying(false);
}
};
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);
// Revert on failure if needed, but for now just log it
}
};
const handleAddJobRole = async (title?: string) => {
const patternValue = title || "New Pattern";
setIsLoading(true);
try {
await axios.post(`${apiBase}/job_roles`, { pattern: "New Pattern", role: "Operativer Entscheider" });
await axios.post(`${apiBase}/job_roles`, {
pattern_type: "exact",
pattern_value: patternValue,
role: "Influencer",
priority: 100
});
fetchAllData();
} catch (e) {
alert("Failed to add job role");
@@ -109,7 +221,11 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
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}`);
@@ -144,6 +260,7 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
{ 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 },
].map(t => (
<button
@@ -255,28 +372,60 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
</div>
<div key="roles-content" className={clsx("space-y-8", { 'hidden': isLoading || activeTab !== 'roles' })}>
{/* Existing Patterns */}
{/* Existing Patterns Grouped */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<div>
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Active Mapping Patterns</h3>
<p className="text-[10px] text-slate-500 uppercase font-semibold">Deterministic Regex/Text rules</p>
<div className="flex justify-between items-center gap-4">
<div className="flex-1">
<input
type="text"
placeholder="Search patterns..."
value={roleSearch}
onChange={e => 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"
/>
</div>
<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 shadow-lg shadow-blue-500/20"><Plus className="h-3 w-3" /> ADD PATTERN</button>
<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 shadow-lg shadow-blue-500/20"><Plus className="h-3 w-3" /> ADD PATTERN</button>
</div>
<div className="bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm">
<table className="w-full text-left text-xs">
<thead className="bg-slate-50 dark:bg-slate-900/50 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase tracking-wider"><tr><th className="p-3">Pattern (% for wildcard)</th><th className="p-3">Target Persona Role</th><th className="p-3 w-10"></th></tr></thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
{jobRoles.map(role => (
<tr key={role.id} className="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
<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 font-mono" 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><option>Influencer</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-all transform hover:scale-110"><Trash2 className="h-4 w-4" /></button></td>
</tr>
))}
</tbody>
</table>
<div className="space-y-2">
{Object.keys(groupedAndFilteredRoles).sort().map(roleName => (
<details key={roleName} className="bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg group" open={!!roleSearch}>
<summary className="p-3 cursor-pointer flex justify-between items-center group-hover:bg-slate-50 dark:group-hover:bg-slate-900 transition-colors">
<div className="font-semibold text-slate-800 dark:text-slate-200 text-xs">
{roleName}
<span className="ml-2 text-slate-400 font-normal">({groupedAndFilteredRoles[roleName].length} patterns)</span>
</div>
<ChevronDown className="h-4 w-4 text-slate-400 transform group-open:rotate-180 transition-transform" />
</summary>
<div className="border-t border-slate-200 dark:border-slate-800">
<table className="w-full text-left text-xs">
<thead className="bg-slate-50 dark:bg-slate-900/50 text-slate-500 font-bold uppercase tracking-wider">
<tr>
<th className="p-2">Type</th>
<th className="p-2">Pattern Value</th>
<th className="p-2">Priority</th>
<th className="p-2 w-8"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
{groupedAndFilteredRoles[roleName].map((role: JobRolePatternType) => (
<tr key={role.id} className="group/row hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
<td className="p-1.5">
<select className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-1 py-0.5 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500" defaultValue={role.pattern_type} onChange={(e) => handleUpdateJobRole(role.id, 'pattern_type', e.target.value)}>
<option>exact</option>
<option>regex</option>
</select>
</td>
<td className="p-1.5"><input className="w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-1 py-0.5 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500 font-mono" defaultValue={role.pattern_value} onBlur={(e) => handleUpdateJobRole(role.id, 'pattern_value', e.target.value)} /></td>
<td className="p-1.5"><input type="number" className="w-16 bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-1 py-0.5 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500 font-mono" defaultValue={role.priority} onBlur={(e) => handleUpdateJobRole(role.id, 'priority', e.target.value)} /></td>
<td className="p-1.5 text-center"><button onClick={() => handleDeleteJobRole(role.id)} className="text-slate-400 hover:text-red-500 opacity-0 group-hover/row:opacity-100 transition-all transform hover:scale-110"><Trash2 className="h-4 w-4" /></button></td>
</tr>
))}
</tbody>
</table>
</div>
</details>
))}
</div>
</div>
@@ -287,19 +436,26 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Discovery Inbox</h3>
<p className="text-[10px] text-slate-500 uppercase font-semibold">Unmapped job titles from CRM, prioritized by frequency</p>
</div>
{rawJobTitles.length > 0 && (
<button
onClick={handleBatchClassify}
disabled={isClassifying}
className="flex items-center gap-1 px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-bold rounded shadow-lg shadow-green-500/20 disabled:bg-slate-400 disabled:shadow-none"
>
<Bot className="h-3 w-3" />
{isClassifying ? 'CLASSIFYING...' : `CLASSIFY ${rawJobTitles.length} TITLES`}
</button>
)}
</div>
<div className="bg-slate-50/50 dark:bg-slate-900/20 border border-dashed border-slate-300 dark:border-slate-700 rounded-xl overflow-hidden">
<table className="w-full text-left text-xs">
<thead className="bg-slate-100/50 dark:bg-slate-900/80 border-b border-slate-200 dark:border-slate-800 text-slate-400 font-bold uppercase tracking-wider"><tr><th className="p-3">Job Title from CRM</th><th className="p-3 w-20 text-center">Frequency</th><th className="p-3 w-10"></th></tr></thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
{rawJobTitles.map(raw => (
{rawJobTitles.map((raw: RawJobTitleType) => (
<tr key={raw.id} className="group hover:bg-white dark:hover:bg-slate-800 transition-colors">
<td className="p-3 font-medium text-slate-600 dark:text-slate-400 italic">{raw.title}</td>
<td className="p-3 text-center"><span className="px-2 py-1 bg-slate-200 dark:bg-slate-800 rounded-full font-bold text-[10px] text-slate-500">{raw.count}x</span></td>
<td className="p-3 text-center"><button onClick={async () => {
await axios.post(`${apiBase}/job_roles`, { pattern: `%${raw.title.toLowerCase()}%`, role: "Influencer" });
fetchAllData();
}} className="p-1 text-blue-500 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded transition-all"><Plus className="h-4 w-4" /></button></td>
<td className="p-3 text-center"><button onClick={() => handleAddJobRole(raw.title)} className="p-1 text-blue-500 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded transition-all"><Plus className="h-4 w-4" /></button></td>
</tr>
))}
{rawJobTitles.length === 0 && (<tr><td colSpan={3} className="p-12 text-center text-slate-400 italic">Discovery inbox is empty. Import raw job titles to see data here.</td></tr>)}
@@ -309,6 +465,10 @@ export function RoboticsSettings({ isOpen, onClose, apiBase }: RoboticsSettingsP
</div>
</div>
<div key="matrix-content" className={clsx("space-y-4", { 'hidden': isLoading || activeTab !== 'matrix' })}>
<MarketingMatrixManager apiBase={apiBase} />
</div>
<div key="mistakes-content" className={clsx("space-y-4", { 'hidden': isLoading || activeTab !== 'mistakes' })}>
<div className="flex justify-between items-center">
<h3 className="text-sm font-bold text-slate-700 dark:text-slate-300">Reported Data Mistakes</h3>