spine-framework-cortex 0.1.19 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/{functions/custom_cortex-handler.ts → api/cortex-handler.ts} +9 -9
  2. package/components/CliInstancesCard.tsx +144 -0
  3. package/components/CortexSidebar.tsx +27 -53
  4. package/hooks/useTypeRegistry.ts +74 -0
  5. package/index.tsx +13 -24
  6. package/manifest.json +1 -13
  7. package/package.json +11 -20
  8. package/pages/courses/CoursesPage.tsx +14 -4
  9. package/pages/crm/AccountDetailPage.tsx +149 -194
  10. package/pages/crm/ContactsPage.tsx +7 -7
  11. package/pages/intelligence/IntelligencePage.tsx +24 -31
  12. package/pages/kb/KBEditorPage.tsx +9 -2
  13. package/pages/operations/AuditFunnelPage.tsx +378 -0
  14. package/pages/operations/InstallFunnelPage.tsx +410 -0
  15. package/pages/operations/OperationsDashboard.tsx +275 -0
  16. package/pages/support/RedactionReview.tsx +11 -2
  17. package/seed/link-types.json +8 -42
  18. package/seed/package.json +27 -0
  19. package/seed/roles.json +1 -1
  20. package/seed/types.json +2711 -596
  21. package/CHANGELOG.md +0 -46
  22. package/LICENSE.md +0 -223
  23. package/README.md +0 -69
  24. package/functions/custom_anonymous-sessions.ts +0 -356
  25. package/functions/custom_case_analysis.ts +0 -507
  26. package/functions/custom_community-escalation.ts +0 -234
  27. package/functions/custom_cortex-chunks.ts +0 -52
  28. package/functions/custom_funnel-scoring.ts +0 -256
  29. package/functions/custom_funnel-signal.ts +0 -446
  30. package/functions/custom_funnel-timers.ts +0 -449
  31. package/functions/custom_kb-chunker-test.ts +0 -364
  32. package/functions/custom_kb-chunker.ts +0 -576
  33. package/functions/custom_kb-embeddings.ts +0 -481
  34. package/functions/custom_kb-ingestion.ts +0 -448
  35. package/functions/custom_support-triage.ts +0 -649
  36. package/functions/custom_tag_management.ts +0 -314
  37. package/functions/webhook-handlers.ts +0 -29
  38. package/lib/resolveTypeId.ts +0 -16
  39. package/pages/crm/ContactDetailPage.tsx +0 -184
  40. package/pages/ops/AuditFunnelPage.tsx +0 -191
  41. package/pages/ops/CommandCenterPage.tsx +0 -377
  42. package/pages/ops/InstallFunnelPage.tsx +0 -226
  43. package/seed/accounts.json +0 -9
  44. package/seed/integrations.json +0 -24
  45. package/seed/pipelines.json +0 -59
  46. package/seed/triggers.json +0 -125
@@ -1,191 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import { useNavigate } from 'react-router-dom'
3
- import { apiFetch } from '@core/lib/api'
4
- import { Badge } from '@core/components/ui/badge'
5
- import { Button } from '@core/components/ui/button'
6
- import { Input } from '@core/components/ui/input'
7
- import { Skeleton } from '@core/components/ui/skeleton'
8
- import { ScanSearch, ChevronRight, Search } from 'lucide-react'
9
-
10
- interface AuditAccount {
11
- id: string
12
- slug: string
13
- display_name?: string
14
- created_at: string
15
- data?: {
16
- temperature?: 'cold' | 'warm' | 'hot'
17
- lifecycle_stage?: string
18
- lead_score?: number
19
- last_signal_at?: string
20
- ratings?: Record<string, { rating: number; raw_score: number; calculated_at?: string }>
21
- claim_status?: string
22
- }
23
- }
24
-
25
- function scoreBar(rating: number) {
26
- const val = Math.round((rating / 5) * 100)
27
- const color = val >= 80 ? 'bg-green-500' : val >= 50 ? 'bg-yellow-500' : 'bg-gray-300'
28
- return (
29
- <div className="flex items-center gap-2">
30
- <div className="w-24 h-1.5 bg-muted rounded-full overflow-hidden">
31
- <div className={`h-full rounded-full ${color}`} style={{ width: `${val}%` }} />
32
- </div>
33
- <span className="text-xs font-medium tabular-nums">{val}</span>
34
- </div>
35
- )
36
- }
37
-
38
- function recommendedOffer(rating: number): string {
39
- if (rating >= 5) return 'Paid Audit $5k'
40
- if (rating >= 4) return 'Discovery Call'
41
- if (rating >= 3) return 'Self-Audit'
42
- return 'Content Nurture'
43
- }
44
-
45
- function stageBadge(stage?: string) {
46
- const map: Record<string, string> = {
47
- anonymous: 'Anonymous',
48
- identified: 'Identified',
49
- installed: 'Installed',
50
- }
51
- return <Badge variant="secondary" className="text-xs capitalize">{map[stage || ''] || stage || '—'}</Badge>
52
- }
53
-
54
- function temperatureBadge(temp?: string) {
55
- if (temp === 'hot') return <Badge className="bg-red-100 text-red-700 border-red-200 text-xs">Hot</Badge>
56
- if (temp === 'warm') return <Badge className="bg-yellow-100 text-yellow-700 border-yellow-200 text-xs">Warm</Badge>
57
- return <Badge variant="outline" className="text-xs">Cold</Badge>
58
- }
59
-
60
- export default function AuditFunnelPage() {
61
- const navigate = useNavigate()
62
- const [accounts, setAccounts] = useState<AuditAccount[]>([])
63
- const [loading, setLoading] = useState(true)
64
- const [search, setSearch] = useState('')
65
-
66
- useEffect(() => {
67
- apiFetch('/api/admin-data?action=list&entity=accounts&limit=500')
68
- .then(r => r.json())
69
- .then(json => {
70
- const all: AuditAccount[] = Array.isArray(json?.data) ? json.data : Array.isArray(json) ? json : []
71
- // Keep accounts that have any audit funnel signal rating
72
- const auditAccounts = all.filter(a =>
73
- a.data?.ratings?.['funnel-ai-audit'] ||
74
- a.data?.ratings?.['funnel-ai-audit-portal-signup'] ||
75
- a.data?.ratings?.['funnel-ai-audit-hot']
76
- )
77
- // Sort by audit rating desc
78
- auditAccounts.sort((a, b) => {
79
- const ra = a.data?.ratings?.['funnel-ai-audit']?.rating || 0
80
- const rb = b.data?.ratings?.['funnel-ai-audit']?.rating || 0
81
- return rb - ra
82
- })
83
- setAccounts(auditAccounts)
84
- })
85
- .catch(() => setAccounts([]))
86
- .finally(() => setLoading(false))
87
- }, [])
88
-
89
- const filtered = accounts.filter(a => {
90
- const q = search.toLowerCase()
91
- return !q || (a.display_name || a.slug || '').toLowerCase().includes(q)
92
- })
93
-
94
- return (
95
- <div className="p-6 space-y-6">
96
- {/* Header */}
97
- <div className="flex items-center justify-between">
98
- <div>
99
- <div className="flex items-center gap-2">
100
- <ScanSearch className="h-5 w-5 text-muted-foreground" />
101
- <h1 className="text-2xl font-bold">Audit Funnel</h1>
102
- </div>
103
- <p className="text-sm text-muted-foreground mt-0.5">
104
- Accounts progressing through the AI readiness audit — from marketing visit to paid audit.
105
- </p>
106
- </div>
107
- <Button size="sm" onClick={() => navigate('/cortex/ops/command-center')}>
108
- Command Center <ChevronRight className="h-3.5 w-3.5 ml-1" />
109
- </Button>
110
- </div>
111
-
112
- {/* Summary stats */}
113
- <div className="grid grid-cols-4 gap-4">
114
- {[
115
- { label: 'In Funnel', value: loading ? '…' : accounts.length },
116
- { label: 'Hot (score ≥4)', value: loading ? '…' : accounts.filter(a => (a.data?.ratings?.['funnel-ai-audit']?.rating || 0) >= 4).length },
117
- { label: 'Warm (score 3)', value: loading ? '…' : accounts.filter(a => (a.data?.ratings?.['funnel-ai-audit']?.rating || 0) === 3).length },
118
- { label: 'Identified', value: loading ? '…' : accounts.filter(a => a.data?.lifecycle_stage === 'identified').length },
119
- ].map(s => (
120
- <div key={s.label} className="border rounded-lg p-4">
121
- <div className="text-xs text-muted-foreground mb-1">{s.label}</div>
122
- {loading ? <Skeleton className="h-7 w-12" /> : <div className="text-2xl font-bold">{s.value}</div>}
123
- </div>
124
- ))}
125
- </div>
126
-
127
- {/* Table */}
128
- <div className="border rounded-lg">
129
- <div className="flex items-center justify-between px-4 py-3 border-b">
130
- <span className="font-medium text-sm">Audit Prospects</span>
131
- <div className="relative w-56">
132
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
133
- <Input
134
- className="h-7 pl-8 text-xs"
135
- placeholder="Search accounts…"
136
- value={search}
137
- onChange={e => setSearch(e.target.value)}
138
- />
139
- </div>
140
- </div>
141
-
142
- {loading ? (
143
- <div className="p-4 space-y-3">
144
- {Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}
145
- </div>
146
- ) : filtered.length === 0 ? (
147
- <div className="p-10 text-center text-sm text-muted-foreground">
148
- <ScanSearch className="h-8 w-8 mx-auto mb-2 opacity-20" />
149
- {search ? 'No accounts match your search.' : 'No audit funnel activity yet. Send signals with pipeline_slug: funnel-ai-audit to populate this view.'}
150
- </div>
151
- ) : (
152
- <>
153
- {/* Header row */}
154
- <div className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_1fr] gap-4 px-4 py-2 text-xs text-muted-foreground font-medium border-b bg-muted/30">
155
- <span>Account</span>
156
- <span>Signal Score</span>
157
- <span>Stage</span>
158
- <span>Last Signal</span>
159
- <span>Recommended Offer</span>
160
- <span>Status</span>
161
- </div>
162
- <div className="divide-y">
163
- {filtered.map(a => {
164
- const rating = a.data?.ratings?.['funnel-ai-audit']?.rating || 0
165
- return (
166
- <div
167
- key={a.id}
168
- className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_1fr] gap-4 px-4 py-3 items-center hover:bg-accent/50 cursor-pointer transition-colors text-sm"
169
- onClick={() => navigate(`/cortex/crm/accounts/${a.id}`)}
170
- >
171
- <div className="min-w-0">
172
- <div className="font-medium truncate">{a.display_name || a.slug}</div>
173
- <div className="text-xs text-muted-foreground truncate">{a.slug}</div>
174
- </div>
175
- <div>{scoreBar(rating)}</div>
176
- <div>{stageBadge(a.data?.lifecycle_stage)}</div>
177
- <div className="text-xs text-muted-foreground">
178
- {a.data?.last_signal_at ? new Date(a.data.last_signal_at).toLocaleDateString() : '—'}
179
- </div>
180
- <div className="text-xs">{recommendedOffer(rating)}</div>
181
- <div>{temperatureBadge(a.data?.temperature)}</div>
182
- </div>
183
- )
184
- })}
185
- </div>
186
- </>
187
- )}
188
- </div>
189
- </div>
190
- )
191
- }
@@ -1,377 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import { useNavigate } from 'react-router-dom'
3
- import { apiFetch } from '@core/lib/api'
4
- import { Badge } from '@core/components/ui/badge'
5
- import { Button } from '@core/components/ui/button'
6
- import { Skeleton } from '@core/components/ui/skeleton'
7
- import { Tabs, TabsList, TabsTrigger, TabsContent } from '@core/components/ui/tabs'
8
- import {
9
- Flame, Download, ScanSearch, TrendingUp, Users,
10
- ArrowUpRight, ChevronRight, Circle
11
- } from 'lucide-react'
12
-
13
- // ─── TYPES ────────────────────────────────────────────────────────────────────
14
-
15
- interface KpiData {
16
- launchPipeline: number
17
- managedOpsArr: number
18
- paidAuditRequests: number
19
- claimedInstalls: number
20
- loading: boolean
21
- }
22
-
23
- interface Opportunity {
24
- id: string
25
- title: string
26
- account_id?: string
27
- data?: {
28
- recommendation?: { opportunity_type?: string; confidence?: string; suggested_priority?: number }
29
- trigger?: { trigger_stage?: string; trigger_rating?: number; trigger_reason?: string }
30
- identity?: { account_id?: string }
31
- review?: { status?: string }
32
- }
33
- created_at: string
34
- account?: { display_name?: string; slug?: string; data?: any }
35
- }
36
-
37
- interface FunnelSignal {
38
- id: string
39
- data?: {
40
- classification?: { pipeline_slug?: string; stage?: string }
41
- action?: { action_type?: string }
42
- scoring_components?: { raw_score?: { rating?: number } }
43
- }
44
- created_at: string
45
- }
46
-
47
- // ─── HELPERS ──────────────────────────────────────────────────────────────────
48
-
49
- function nextAction(stage?: string, temperature?: string): string {
50
- if (temperature === 'hot') {
51
- if (stage === 'installed') return 'Create Claim Link'
52
- if (stage === 'identified') return 'Send Proposal'
53
- return 'Schedule Exec Call'
54
- }
55
- if (temperature === 'warm') return 'Schedule Discovery Call'
56
- return 'Review'
57
- }
58
-
59
- function temperatureBadge(temp?: string) {
60
- if (temp === 'hot') return <Badge className="bg-red-100 text-red-700 border-red-200">Hot</Badge>
61
- if (temp === 'warm') return <Badge className="bg-yellow-100 text-yellow-700 border-yellow-200">Warm</Badge>
62
- return <Badge variant="secondary">New</Badge>
63
- }
64
-
65
- function opportunityTypeBadge(type?: string) {
66
- const map: Record<string, string> = {
67
- advanced_portal: 'Paid Audit',
68
- managed_services: 'Managed Ops',
69
- expansion: 'Expansion',
70
- support_plan: 'Support Plan',
71
- implementation: 'Unclaimed Install',
72
- advocate: 'Partner',
73
- }
74
- return <Badge variant="outline">{map[type || ''] || type || '—'}</Badge>
75
- }
76
-
77
- function scoreBar(rating?: number) {
78
- const val = Math.round(((rating || 0) / 5) * 100)
79
- const color = val >= 80 ? 'bg-green-500' : val >= 50 ? 'bg-yellow-500' : 'bg-gray-300'
80
- return (
81
- <div className="flex items-center gap-2">
82
- <div className="w-20 h-1.5 bg-muted rounded-full overflow-hidden">
83
- <div className={`h-full rounded-full ${color}`} style={{ width: `${val}%` }} />
84
- </div>
85
- <span className="text-xs font-medium">{val}</span>
86
- </div>
87
- )
88
- }
89
-
90
- // ─── FUNNEL HEALTH ────────────────────────────────────────────────────────────
91
-
92
- function FunnelHealth({ signals, pipelineSlug, stages, label }: {
93
- signals: FunnelSignal[]
94
- pipelineSlug: string
95
- stages: string[]
96
- label: string
97
- }) {
98
- const filtered = signals.filter(s => s.data?.classification?.pipeline_slug === pipelineSlug)
99
- const counts = stages.map(stage => ({
100
- stage,
101
- count: filtered.filter(s => s.data?.classification?.stage === stage).length
102
- }))
103
-
104
- return (
105
- <div className="border rounded-lg p-4 space-y-3">
106
- <div className="flex items-center gap-2">
107
- {pipelineSlug.includes('audit') ? <ScanSearch className="h-4 w-4 text-muted-foreground" /> : <Download className="h-4 w-4 text-muted-foreground" />}
108
- <span className="font-medium text-sm">{label} Health</span>
109
- </div>
110
- <div className="flex items-center gap-1">
111
- {counts.map((c, i) => (
112
- <div key={c.stage} className="flex items-center gap-1">
113
- <div className="text-center">
114
- <div className="text-lg font-bold">{c.count}</div>
115
- <div className="text-xs text-muted-foreground capitalize">{c.stage}</div>
116
- </div>
117
- {i < counts.length - 1 && <ChevronRight className="h-3 w-3 text-muted-foreground mx-1" />}
118
- </div>
119
- ))}
120
- </div>
121
- </div>
122
- )
123
- }
124
-
125
- // ─── MAIN PAGE ────────────────────────────────────────────────────────────────
126
-
127
- export default function CommandCenterPage() {
128
- const navigate = useNavigate()
129
- const [kpis, setKpis] = useState<KpiData>({ launchPipeline: 0, managedOpsArr: 0, paidAuditRequests: 0, claimedInstalls: 0, loading: true })
130
- const [opportunities, setOpportunities] = useState<Opportunity[]>([])
131
- const [accounts, setAccounts] = useState<any[]>([])
132
- const [signals, setSignals] = useState<FunnelSignal[]>([])
133
- const [loading, setLoading] = useState(true)
134
-
135
- useEffect(() => {
136
- Promise.all([
137
- apiFetch('/api/admin-data?action=list&entity=items&type_slug=deal&limit=500').then(r => r.json()),
138
- apiFetch('/api/admin-data?action=list&entity=items&type_slug=opportunity_queue&limit=100').then(r => r.json()),
139
- apiFetch('/api/admin-data?action=list&entity=accounts&limit=500').then(r => r.json()),
140
- apiFetch('/api/admin-data?action=list&entity=items&type_slug=funnel_signal&limit=500').then(r => r.json()),
141
- ]).then(([dealsRes, oppsRes, accsRes, sigsRes]) => {
142
- const deals: any[] = dealsRes?.data ?? dealsRes ?? []
143
- const opps: Opportunity[] = oppsRes?.data ?? oppsRes ?? []
144
- const accs: any[] = accsRes?.data ?? accsRes ?? []
145
- const sigs: FunnelSignal[] = sigsRes?.data ?? sigsRes ?? []
146
-
147
- // KPIs from deals
148
- const launchPipeline = deals
149
- .filter((d: any) => d.data?.stage === 'launch_sprint')
150
- .reduce((s: number, d: any) => s + (d.data?.value || 0), 0)
151
- const managedOpsArr = deals
152
- .filter((d: any) => d.data?.stage === 'managed_ops')
153
- .reduce((s: number, d: any) => s + (d.data?.value || 0), 0)
154
- const paidAuditRequests = opps.filter(o => o.data?.recommendation?.opportunity_type === 'advanced_portal').length
155
- const claimedInstalls = accs.filter((a: any) => a.data?.claim_status === 'claimed').length
156
-
157
- setKpis({ launchPipeline, managedOpsArr, paidAuditRequests, claimedInstalls, loading: false })
158
-
159
- // Enrich opportunities with account data
160
- const accById = Object.fromEntries(accs.map((a: any) => [a.id, a]))
161
- const enriched = opps.map(o => ({
162
- ...o,
163
- account: accById[o.data?.identity?.account_id || o.account_id || '']
164
- }))
165
- setOpportunities(enriched)
166
- setAccounts(accs)
167
- setSignals(sigs)
168
- }).catch(() => {}).finally(() => setLoading(false))
169
- }, [])
170
-
171
- const hotOpps = opportunities
172
- .filter(o => o.data?.review?.status === 'pending')
173
- .sort((a, b) => (b.data?.trigger?.trigger_rating || 0) - (a.data?.trigger?.trigger_rating || 0))
174
- .slice(0, 10)
175
-
176
- const auditAccounts = accounts.filter(a => a.data?.ratings?.['funnel-ai-audit'])
177
- const installAccounts = accounts.filter(a => a.data?.ratings?.['funnel-technical'] || a.data?.lifecycle_stage === 'installed')
178
-
179
- return (
180
- <div className="p-6 space-y-6">
181
- {/* Header */}
182
- <div className="flex items-center justify-between">
183
- <div>
184
- <h1 className="text-2xl font-bold">Opportunity Command Center</h1>
185
- <p className="text-sm text-muted-foreground mt-0.5">Signal-driven revenue operations. Prioritize what's hot, act with confidence, close faster.</p>
186
- </div>
187
- <div className="flex gap-2">
188
- <Button variant="outline" size="sm"><Download className="h-4 w-4 mr-1.5" />Export Signals</Button>
189
- <Button size="sm"><TrendingUp className="h-4 w-4 mr-1.5" />Create Opportunity</Button>
190
- </div>
191
- </div>
192
-
193
- {/* KPI Strip */}
194
- <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
195
- {[
196
- { label: 'Launch Pipeline', value: kpis.loading ? '…' : `$${(kpis.launchPipeline / 1000).toFixed(0)}k`, icon: TrendingUp, delta: null },
197
- { label: 'Managed Ops ARR', value: kpis.loading ? '…' : `$${(kpis.managedOpsArr / 1000).toFixed(0)}k`, icon: TrendingUp, delta: null },
198
- { label: 'Paid Audit Requests', value: kpis.loading ? '…' : kpis.paidAuditRequests, icon: ScanSearch, delta: null },
199
- { label: 'Claimed Active Installs', value: kpis.loading ? '…' : kpis.claimedInstalls, icon: Download, delta: null },
200
- ].map(kpi => (
201
- <div key={kpi.label} className="border rounded-lg p-4 space-y-2">
202
- <div className="flex items-center justify-between">
203
- <span className="text-xs text-muted-foreground">{kpi.label}</span>
204
- <kpi.icon className="h-4 w-4 text-muted-foreground" />
205
- </div>
206
- {kpis.loading
207
- ? <Skeleton className="h-7 w-20" />
208
- : <div className="text-2xl font-bold">{kpi.value}</div>
209
- }
210
- </div>
211
- ))}
212
- </div>
213
-
214
- {/* Hot Opportunities */}
215
- <div className="border rounded-lg">
216
- <div className="flex items-center justify-between px-4 py-3 border-b">
217
- <div className="flex items-center gap-2">
218
- <Flame className="h-4 w-4 text-orange-500" />
219
- <span className="font-semibold">Hot Opportunities</span>
220
- </div>
221
- <Button variant="ghost" size="sm" className="text-xs gap-1">
222
- View all <ArrowUpRight className="h-3 w-3" />
223
- </Button>
224
- </div>
225
-
226
- {loading ? (
227
- <div className="p-4 space-y-3">
228
- {Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}
229
- </div>
230
- ) : hotOpps.length === 0 ? (
231
- <div className="p-8 text-center text-sm text-muted-foreground">
232
- <Circle className="h-8 w-8 mx-auto mb-2 opacity-20" />
233
- No pending opportunities yet. Signals will appear here once they score above threshold.
234
- </div>
235
- ) : (
236
- <div className="divide-y">
237
- {/* Table header */}
238
- <div className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_2fr_1fr] gap-4 px-4 py-2 text-xs text-muted-foreground font-medium">
239
- <span>Opportunity</span><span>Type</span><span>Stage</span>
240
- <span>Signal Score</span><span>Last Signal</span>
241
- <span>Next Action</span><span>Status</span>
242
- </div>
243
- {hotOpps.map(opp => {
244
- const acc = opp.account
245
- const temp = acc?.data?.temperature
246
- const stage = opp.data?.trigger?.trigger_stage
247
- const rating = opp.data?.trigger?.trigger_rating
248
- const oppType = opp.data?.recommendation?.opportunity_type
249
- return (
250
- <div
251
- key={opp.id}
252
- className="grid grid-cols-[2fr_1fr_1fr_1fr_1fr_2fr_1fr] gap-4 px-4 py-3 items-center hover:bg-accent/50 cursor-pointer transition-colors text-sm"
253
- onClick={() => acc?.id && navigate(`/cortex/crm/accounts/${acc.id}`)}
254
- >
255
- <div className="min-w-0">
256
- <div className="font-medium truncate">{acc?.display_name || acc?.slug || opp.title}</div>
257
- <div className="text-xs text-muted-foreground truncate">{acc?.slug || '—'}</div>
258
- </div>
259
- <div>{opportunityTypeBadge(oppType)}</div>
260
- <div><Badge variant="secondary" className="capitalize">{stage || '—'}</Badge></div>
261
- <div>{scoreBar(rating)}</div>
262
- <div className="text-xs text-muted-foreground">{new Date(opp.created_at).toLocaleDateString()}</div>
263
- <div className="text-xs">{nextAction(stage, temp)}</div>
264
- <div>{temperatureBadge(temp)}</div>
265
- </div>
266
- )
267
- })}
268
- </div>
269
- )}
270
- </div>
271
-
272
- {/* Bottom: Tabs + Health Panels */}
273
- <div className="grid grid-cols-1 lg:grid-cols-[1fr_300px_300px] gap-6">
274
- <div className="border rounded-lg">
275
- <Tabs defaultValue="audit">
276
- <div className="px-4 pt-3 border-b">
277
- <TabsList className="h-8 bg-transparent p-0 gap-4">
278
- {['audit', 'install', 'partners', 'forecast'].map(t => (
279
- <TabsTrigger key={t} value={t}
280
- className="capitalize rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent h-8 px-1 text-xs">
281
- {t === 'audit' ? 'Audit Funnel' : t === 'install' ? 'Install Funnel' : t === 'partners' ? 'Partner Signals' : 'Revenue Forecast'}
282
- </TabsTrigger>
283
- ))}
284
- </TabsList>
285
- </div>
286
-
287
- <TabsContent value="audit" className="mt-0 p-4">
288
- <p className="text-xs text-muted-foreground mb-3 font-medium">Audit-Led Opportunities</p>
289
- {auditAccounts.length === 0 ? (
290
- <p className="text-sm text-muted-foreground text-center py-6">No audit funnel activity yet.</p>
291
- ) : (
292
- <div className="divide-y">
293
- <div className="grid grid-cols-[2fr_1fr_2fr_1fr] gap-3 pb-2 text-xs text-muted-foreground font-medium">
294
- <span>Account</span><span>Score</span><span>Recommended Offer</span><span>Status</span>
295
- </div>
296
- {auditAccounts.slice(0, 6).map(a => {
297
- const r = a.data?.ratings?.['funnel-ai-audit']?.rating || 0
298
- const offer = r >= 5 ? 'Paid Audit $5k' : r >= 4 ? 'Discovery Call' : r >= 3 ? 'Self-Audit' : 'Content Nurture'
299
- const status = a.data?.temperature === 'hot' ? 'Hot' : a.data?.temperature === 'warm' ? 'Warm' : 'New'
300
- return (
301
- <div key={a.id}
302
- className="grid grid-cols-[2fr_1fr_2fr_1fr] gap-3 py-2 items-center text-sm cursor-pointer hover:bg-accent/50"
303
- onClick={() => navigate(`/cortex/crm/accounts/${a.id}`)}>
304
- <span className="font-medium truncate">{a.display_name || a.slug}</span>
305
- {scoreBar(r)}
306
- <span className="text-xs text-muted-foreground">{offer}</span>
307
- <Badge variant={status === 'Hot' ? 'destructive' : 'secondary'} className="text-xs">{status}</Badge>
308
- </div>
309
- )
310
- })}
311
- </div>
312
- )}
313
- <Button variant="ghost" size="sm" className="mt-2 text-xs" onClick={() => navigate('/cortex/ops/audit-funnel')}>
314
- View all audit opportunities <ChevronRight className="h-3 w-3 ml-1" />
315
- </Button>
316
- </TabsContent>
317
-
318
- <TabsContent value="install" className="mt-0 p-4">
319
- <p className="text-xs text-muted-foreground mb-3 font-medium">Install-Led Opportunities</p>
320
- {installAccounts.length === 0 ? (
321
- <p className="text-sm text-muted-foreground text-center py-6">No install funnel activity yet.</p>
322
- ) : (
323
- <div className="divide-y">
324
- <div className="grid grid-cols-[2fr_1fr_1fr_1fr] gap-3 pb-2 text-xs text-muted-foreground font-medium">
325
- <span>Account</span><span>Score</span><span>Stage</span><span>Claim</span>
326
- </div>
327
- {installAccounts.slice(0, 6).map(a => {
328
- const r = a.data?.ratings?.['funnel-technical']?.rating || a.data?.ratings?.installed?.rating || 0
329
- return (
330
- <div key={a.id}
331
- className="grid grid-cols-[2fr_1fr_1fr_1fr] gap-3 py-2 items-center text-sm cursor-pointer hover:bg-accent/50"
332
- onClick={() => navigate(`/cortex/crm/accounts/${a.id}`)}>
333
- <span className="font-medium truncate">{a.display_name || a.slug}</span>
334
- {scoreBar(r)}
335
- <Badge variant="secondary" className="text-xs capitalize">{a.data?.lifecycle_stage || '—'}</Badge>
336
- <Badge variant={a.data?.claim_status === 'claimed' ? 'default' : 'outline'} className="text-xs">
337
- {a.data?.claim_status === 'claimed' ? 'Claimed' : 'Unclaimed'}
338
- </Badge>
339
- </div>
340
- )
341
- })}
342
- </div>
343
- )}
344
- <Button variant="ghost" size="sm" className="mt-2 text-xs" onClick={() => navigate('/cortex/ops/install-funnel')}>
345
- View install funnel <ChevronRight className="h-3 w-3 ml-1" />
346
- </Button>
347
- </TabsContent>
348
-
349
- <TabsContent value="partners" className="mt-0 p-8 text-center text-sm text-muted-foreground">
350
- <Users className="h-8 w-8 mx-auto mb-2 opacity-20" />
351
- Partner signal tracking coming soon.
352
- </TabsContent>
353
-
354
- <TabsContent value="forecast" className="mt-0 p-8 text-center text-sm text-muted-foreground">
355
- <TrendingUp className="h-8 w-8 mx-auto mb-2 opacity-20" />
356
- Revenue forecast coming soon.
357
- </TabsContent>
358
- </Tabs>
359
- </div>
360
-
361
- {/* Funnel Health Panels */}
362
- <FunnelHealth
363
- signals={signals}
364
- pipelineSlug="funnel-technical"
365
- stages={['anonymous', 'identified', 'installed']}
366
- label="Install Funnel"
367
- />
368
- <FunnelHealth
369
- signals={signals}
370
- pipelineSlug="funnel-ai-audit"
371
- stages={['anonymous', 'identified', 'installed']}
372
- label="Audit Funnel"
373
- />
374
- </div>
375
- </div>
376
- )
377
- }