[31188f42] einfügen

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

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>