spine-framework-cortex 0.1.19 → 0.2.1

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 +4 -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
@@ -6,7 +6,8 @@ import { Badge } from '@core/components/ui/badge'
6
6
  import { Skeleton } from '@core/components/ui/skeleton'
7
7
  import { Button } from '@core/components/ui/button'
8
8
  import { ScrollArea } from '@core/components/ui/scroll-area'
9
- import { ArrowLeft, User, Ticket, Handshake, Activity, Heart, Filter, TrendingUp, Target, Clock, Eye, Download, FileText, CheckCircle, Zap, Link2 } from 'lucide-react'
9
+ import { Separator } from '@core/components/ui/separator'
10
+ import { ArrowLeft, User, Ticket, Handshake, Activity, Heart, Funnel, TrendingUp, Target, Clock } from 'lucide-react'
10
11
 
11
12
  interface Account {
12
13
  id: string
@@ -38,7 +39,8 @@ interface Account {
38
39
  interface Person {
39
40
  id: string
40
41
  email: string
41
- full_name?: string
42
+ first_name?: string
43
+ last_name?: string
42
44
  created_at: string
43
45
  }
44
46
 
@@ -67,7 +69,7 @@ function PeopleTab({ accountId }: { accountId: string }) {
67
69
  <User className="h-4 w-4 text-muted-foreground" />
68
70
  </div>
69
71
  <div>
70
- <p className="text-sm font-medium">{p.full_name || '—'}</p>
72
+ <p className="text-sm font-medium">{[p.first_name, p.last_name].filter(Boolean).join(' ') || '—'}</p>
71
73
  <p className="text-xs text-muted-foreground">{p.email}</p>
72
74
  </div>
73
75
  <p className="ml-auto text-xs text-muted-foreground">{new Date(p.created_at).toLocaleDateString()}</p>
@@ -139,231 +141,184 @@ function ActivityTab({ accountId }: { accountId: string }) {
139
141
  )
140
142
  }
141
143
 
142
- // ─── ACTION TYPE ICON ────────────────────────────────────────────────────────
143
- function ActionIcon({ actionType }: { actionType?: string }) {
144
- if (!actionType) return <Zap className="h-3.5 w-3.5 text-muted-foreground" />
145
- if (actionType.includes('page_view') || actionType.includes('visit')) return <Eye className="h-3.5 w-3.5 text-muted-foreground" />
146
- if (actionType.includes('install') || actionType.includes('npm')) return <Download className="h-3.5 w-3.5 text-blue-500" />
147
- if (actionType.includes('claim')) return <CheckCircle className="h-3.5 w-3.5 text-green-500" />
148
- if (actionType.includes('audit') || actionType.includes('self')) return <FileText className="h-3.5 w-3.5 text-purple-500" />
149
- if (actionType.includes('signup') || actionType.includes('register')) return <User className="h-3.5 w-3.5 text-orange-500" />
150
- return <Zap className="h-3.5 w-3.5 text-muted-foreground" />
151
- }
152
-
153
- // ─── SIGNAL TIMELINE ─────────────────────────────────────────────────────────
154
- function SignalTimeline({ accountId }: { accountId: string }) {
155
- const [signals, setSignals] = useState<any[]>([])
156
- const [loading, setLoading] = useState(true)
157
- const [showAll, setShowAll] = useState(false)
158
-
159
- useEffect(() => {
160
- apiFetch(`/api/admin-data?action=list&entity=items&type_slug=funnel_signal&account_id=${accountId}&limit=50`)
161
- .then(r => r.json())
162
- .then(j => {
163
- const items = Array.isArray(j?.data) ? j.data : Array.isArray(j) ? j : []
164
- items.sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
165
- setSignals(items)
166
- })
167
- .catch(() => setSignals([]))
168
- .finally(() => setLoading(false))
169
- }, [accountId])
170
-
171
- if (loading) return <div className="space-y-2">{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}</div>
172
- if (signals.length === 0) return (
173
- <div className="text-center py-8 text-muted-foreground">
174
- <Zap className="h-7 w-7 mx-auto mb-2 opacity-20" />
175
- <p className="text-sm">No signals received yet.</p>
176
- </div>
177
- )
178
-
179
- const visible = showAll ? signals : signals.slice(0, 8)
180
-
181
- return (
182
- <div className="space-y-1">
183
- {visible.map(sig => {
184
- const actionType = sig.data?.action?.action_type
185
- const pipelineSlug = sig.data?.classification?.pipeline_slug
186
- const rating = sig.data?.scoring_components?.raw_score?.rating
187
- return (
188
- <div key={sig.id} className="flex items-start gap-3 py-2 px-1 rounded hover:bg-muted/40 transition-colors">
189
- <div className="mt-0.5 shrink-0"><ActionIcon actionType={actionType} /></div>
190
- <div className="flex-1 min-w-0">
191
- <div className="flex items-center gap-2 flex-wrap">
192
- <span className="text-sm font-medium capitalize">{actionType?.replace(/_/g, ' ') || sig.title}</span>
193
- {pipelineSlug && (
194
- <Badge variant="outline" className="text-xs py-0 h-4 font-mono">{pipelineSlug.replace('funnel-', '')}</Badge>
195
- )}
196
- {rating && (
197
- <span className={`text-xs font-medium ${
198
- rating >= 4 ? 'text-red-600' : rating >= 3 ? 'text-yellow-600' : 'text-muted-foreground'
199
- }`}>{rating}/5</span>
200
- )}
201
- </div>
202
- <p className="text-xs text-muted-foreground">
203
- {new Date(sig.created_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
204
- </p>
205
- </div>
206
- </div>
207
- )
208
- })}
209
- {signals.length > 8 && !showAll && (
210
- <button className="text-xs text-primary hover:underline pt-1" onClick={() => setShowAll(true)}>
211
- View all {signals.length} signals
212
- </button>
213
- )}
214
- </div>
215
- )
216
- }
217
-
218
- // ─── SCORE BREAKDOWN ─────────────────────────────────────────────────────────
219
- const PIPELINE_LABELS: Record<string, string> = {
220
- 'funnel-ai-audit': 'Audit Intent',
221
- 'funnel-ai-audit-portal-signup': 'Portal Signup',
222
- 'funnel-ai-audit-hot': 'Audit Hot',
223
- 'funnel-technical': 'Technical Intent',
224
- 'funnel-technical-installed': 'Install Activity',
225
- 'funnel-technical-claimed': 'Claim Intent',
226
- 'funnel-general': 'General',
227
- anonymous: 'Anonymous',
228
- identified: 'Identified',
229
- installed: 'Installed',
230
- }
231
-
232
- function ScoreBreakdown({ ratings }: { ratings?: Record<string, any> }) {
233
- if (!ratings || Object.keys(ratings).length === 0) return (
234
- <p className="text-xs text-muted-foreground">No scoring data yet.</p>
235
- )
236
-
237
- return (
238
- <div className="space-y-2.5">
239
- {Object.entries(ratings).map(([key, val]) => {
240
- if (!val || typeof val.rating !== 'number') return null
241
- const displayPct = Math.round((val.raw_score / 25) * 100)
242
- const label = PIPELINE_LABELS[key] || key
243
- const color = displayPct >= 80 ? 'bg-green-500' : displayPct >= 50 ? 'bg-yellow-500' : displayPct >= 25 ? 'bg-blue-400' : 'bg-gray-300'
244
- return (
245
- <div key={key} className="flex items-center gap-3">
246
- <span className="text-sm text-muted-foreground w-36 shrink-0 truncate">{label}</span>
247
- <div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
248
- <div className={`h-full rounded-full ${color}`} style={{ width: `${displayPct}%` }} />
249
- </div>
250
- <span className="text-xs font-semibold w-8 text-right tabular-nums">{displayPct}</span>
251
- </div>
252
- )
253
- })}
254
- </div>
255
- )
256
- }
257
-
258
- // ─── FUNNEL TAB (replaces old FilterTab) ─────────────────────────────────────
259
144
  function FunnelTab({ account }: { account: Account }) {
260
- const temp = account.data?.temperature
261
145
  const stage = account.data?.lifecycle_stage
146
+ const score = account.data?.lead_score ?? 0
147
+ const temp = account.data?.temperature
262
148
  const ratings = account.data?.ratings
149
+ const attr = account.data?.attribution
263
150
  const queue = account.data?.queue
264
- const claimStatus = (account.data as any)?.claim_status
265
- const instanceId = (account.data as any)?.instance_id
266
151
 
267
152
  return (
268
153
  <div className="p-6 space-y-6">
269
- {/* Top stat row */}
154
+ {/* Header Stats */}
270
155
  <div className="grid grid-cols-3 gap-4">
271
156
  <div className="border rounded-lg p-4">
272
- <div className="flex items-center gap-2 mb-1.5">
273
- <Target className="h-3.5 w-3.5 text-muted-foreground" />
274
- <span className="text-xs text-muted-foreground uppercase tracking-wider">Stage</span>
157
+ <div className="flex items-center gap-2 mb-2">
158
+ <Target className="h-4 w-4 text-muted-foreground" />
159
+ <span className="text-xs text-muted-foreground uppercase tracking-wider">Lifecycle Stage</span>
275
160
  </div>
276
- {stage
277
- ? <Badge className="capitalize" variant={stage === 'installed' ? 'default' : 'secondary'}>{stage}</Badge>
278
- : <span className="text-sm text-muted-foreground">Not set</span>
279
- }
161
+ {stage ? (
162
+ <Badge className="capitalize text-sm" variant={stage === 'customer' ? 'default' : 'secondary'}>
163
+ {stage.replace(/_/g, ' ')}
164
+ </Badge>
165
+ ) : (
166
+ <span className="text-muted-foreground text-sm">Not set</span>
167
+ )}
280
168
  </div>
169
+
281
170
  <div className="border rounded-lg p-4">
282
- <div className="flex items-center gap-2 mb-1.5">
283
- <TrendingUp className="h-3.5 w-3.5 text-muted-foreground" />
284
- <span className="text-xs text-muted-foreground uppercase tracking-wider">Temperature</span>
171
+ <div className="flex items-center gap-2 mb-2">
172
+ <TrendingUp className="h-4 w-4 text-muted-foreground" />
173
+ <span className="text-xs text-muted-foreground uppercase tracking-wider">Lead Score</span>
174
+ </div>
175
+ <div className="flex items-center gap-3">
176
+ <div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
177
+ <div
178
+ className={`h-full rounded-full ${score >= 70 ? 'bg-green-500' : score >= 40 ? 'bg-yellow-500' : 'bg-gray-400'}`}
179
+ style={{ width: `${Math.min(score, 100)}%` }}
180
+ />
181
+ </div>
182
+ <span className="font-semibold">{score}</span>
285
183
  </div>
286
- {temp ? (
287
- <Badge variant="outline" className={`capitalize ${
288
- temp === 'hot' ? 'border-red-400 text-red-600 bg-red-50' :
289
- temp === 'warm' ? 'border-yellow-400 text-yellow-600 bg-yellow-50' :
290
- 'border-blue-400 text-blue-600 bg-blue-50'
291
- }`}>{temp}</Badge>
292
- ) : <span className="text-sm text-muted-foreground">—</span>}
293
184
  </div>
185
+
294
186
  <div className="border rounded-lg p-4">
295
- <div className="flex items-center gap-2 mb-1.5">
296
- <Clock className="h-3.5 w-3.5 text-muted-foreground" />
187
+ <div className="flex items-center gap-2 mb-2">
188
+ <Clock className="h-4 w-4 text-muted-foreground" />
297
189
  <span className="text-xs text-muted-foreground uppercase tracking-wider">Last Signal</span>
298
190
  </div>
299
191
  <span className="text-sm">
300
- {account.data?.last_signal_at ? new Date(account.data.last_signal_at).toLocaleDateString() : 'Never'}
192
+ {account.data?.last_signal_at
193
+ ? new Date(account.data.last_signal_at).toLocaleDateString()
194
+ : 'Never'
195
+ }
301
196
  </span>
302
197
  </div>
303
198
  </div>
304
199
 
305
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
306
- {/* Left: Signal Timeline */}
307
- <div className="space-y-3">
308
- <h3 className="font-medium text-sm flex items-center gap-2">
309
- <Zap className="h-4 w-4 text-muted-foreground" /> Signal Timeline
310
- </h3>
311
- <SignalTimeline accountId={account.id} />
200
+ {/* Temperature Badge */}
201
+ {temp && (
202
+ <div className="flex items-center gap-2">
203
+ <span className="text-sm text-muted-foreground">Temperature:</span>
204
+ <Badge
205
+ variant="outline"
206
+ className={`capitalize ${
207
+ temp === 'hot' ? 'border-red-400 text-red-600 bg-red-50' :
208
+ temp === 'warm' ? 'border-yellow-400 text-yellow-600 bg-yellow-50' :
209
+ 'border-blue-400 text-blue-600 bg-blue-50'
210
+ }`}
211
+ >
212
+ {temp}
213
+ </Badge>
312
214
  </div>
215
+ )}
313
216
 
314
- {/* Right: Score Breakdown + Claim Status */}
315
- <div className="space-y-4">
316
- <div className="border rounded-lg p-4 space-y-3">
317
- <h3 className="font-medium text-sm flex items-center gap-2">
318
- <TrendingUp className="h-4 w-4 text-muted-foreground" /> Signal Score Breakdown
319
- </h3>
320
- <ScoreBreakdown ratings={ratings} />
217
+ {/* Queue Status */}
218
+ {queue?.pending_opportunity_id && (
219
+ <div className="border rounded-lg p-4 bg-yellow-50 border-yellow-200">
220
+ <div className="flex items-center gap-2">
221
+ <Funnel className="h-4 w-4 text-yellow-600" />
222
+ <span className="font-medium text-sm">Opportunity in Queue</span>
321
223
  </div>
224
+ <p className="text-xs text-muted-foreground mt-1">
225
+ Type: {queue.primary_opportunity_type?.replace(/_/g, ' ') || 'Unknown'}
226
+ </p>
227
+ </div>
228
+ )}
322
229
 
323
- {/* Opportunity queue alert */}
324
- {queue?.pending_opportunity_id && (
325
- <div className="border rounded-lg p-4 bg-yellow-50 border-yellow-200">
326
- <div className="flex items-center gap-2">
327
- <Filter className="h-4 w-4 text-yellow-600" />
328
- <span className="font-medium text-sm">Opportunity in Queue</span>
230
+ {/* Stage Ratings */}
231
+ {ratings && (
232
+ <div className="border rounded-lg p-4">
233
+ <h3 className="font-medium mb-4">Stage Ratings</h3>
234
+ <div className="space-y-3">
235
+ {ratings.anonymous && (
236
+ <div className="flex items-center justify-between">
237
+ <span className="text-sm text-muted-foreground">Anonymous</span>
238
+ <div className="flex items-center gap-2">
239
+ <div className="flex gap-0.5">
240
+ {Array.from({ length: 5 }).map((_, i) => (
241
+ <div
242
+ key={i}
243
+ className={`w-2 h-2 rounded-full ${i < ratings.anonymous!.rating ? 'bg-blue-500' : 'bg-gray-200'}`}
244
+ />
245
+ ))}
246
+ </div>
247
+ <span className="text-xs font-medium w-12 text-right">{ratings.anonymous.raw_score}</span>
248
+ </div>
329
249
  </div>
330
- <p className="text-xs text-muted-foreground mt-1">
331
- {queue.primary_opportunity_type?.replace(/_/g, ' ') || 'Pending review'}
332
- </p>
333
- </div>
334
- )}
250
+ )}
251
+ {ratings.identified && (
252
+ <div className="flex items-center justify-between">
253
+ <span className="text-sm text-muted-foreground">Identified</span>
254
+ <div className="flex items-center gap-2">
255
+ <div className="flex gap-0.5">
256
+ {Array.from({ length: 5 }).map((_, i) => (
257
+ <div
258
+ key={i}
259
+ className={`w-2 h-2 rounded-full ${i < ratings.identified!.rating ? 'bg-green-500' : 'bg-gray-200'}`}
260
+ />
261
+ ))}
262
+ </div>
263
+ <span className="text-xs font-medium w-12 text-right">{ratings.identified.raw_score}</span>
264
+ </div>
265
+ </div>
266
+ )}
267
+ {ratings.engaged && (
268
+ <div className="flex items-center justify-between">
269
+ <span className="text-sm text-muted-foreground">Engaged</span>
270
+ <div className="flex items-center gap-2">
271
+ <div className="flex gap-0.5">
272
+ {Array.from({ length: 5 }).map((_, i) => (
273
+ <div
274
+ key={i}
275
+ className={`w-2 h-2 rounded-full ${i < ratings.engaged!.rating ? 'bg-orange-500' : 'bg-gray-200'}`}
276
+ />
277
+ ))}
278
+ </div>
279
+ <span className="text-xs font-medium w-12 text-right">{ratings.engaged.raw_score}</span>
280
+ </div>
281
+ </div>
282
+ )}
283
+ </div>
284
+ </div>
285
+ )}
335
286
 
336
- {/* Identity / Claim Status */}
337
- <div className="border rounded-lg p-4 space-y-3">
338
- <h3 className="font-medium text-sm flex items-center gap-2">
339
- <Link2 className="h-4 w-4 text-muted-foreground" /> Identity / Claim Status
340
- </h3>
341
- <div className="space-y-2 text-sm">
287
+ {/* Attribution */}
288
+ {(attr?.anonymous_first_touch || attr?.identified_first_touch) && (
289
+ <div className="border rounded-lg p-4">
290
+ <h3 className="font-medium mb-4">Attribution</h3>
291
+ <div className="space-y-3 text-sm">
292
+ {attr.anonymous_first_touch && (
342
293
  <div className="flex justify-between">
343
- <span className="text-muted-foreground">Instance ID</span>
344
- <span className="font-mono text-xs">{instanceId ? instanceId.slice(0, 14) + '…' : '—'}</span>
294
+ <span className="text-muted-foreground">Anonymous First Touch</span>
295
+ <span className="text-right">
296
+ <span className="font-medium">{attr.anonymous_first_touch.referrer || 'Direct'}</span>
297
+ <span className="text-xs text-muted-foreground block">
298
+ {attr.anonymous_first_touch.at ? new Date(attr.anonymous_first_touch.at).toLocaleDateString() : ''}
299
+ </span>
300
+ </span>
345
301
  </div>
346
- <div className="flex justify-between items-center">
347
- <span className="text-muted-foreground">Claim Status</span>
348
- {claimStatus === 'claimed'
349
- ? <Badge className="text-xs bg-green-100 text-green-700 border-green-200">Claimed</Badge>
350
- : <Badge variant="outline" className="text-xs">Unclaimed</Badge>
351
- }
302
+ )}
303
+ {attr.identified_first_touch && (
304
+ <div className="flex justify-between">
305
+ <span className="text-muted-foreground">Identified First Touch</span>
306
+ <span className="text-right">
307
+ <span className="font-medium">{attr.identified_first_touch.referrer || 'Direct'}</span>
308
+ <span className="text-xs text-muted-foreground block">
309
+ {attr.identified_first_touch.at ? new Date(attr.identified_first_touch.at).toLocaleDateString() : ''}
310
+ </span>
311
+ </span>
352
312
  </div>
353
- {!claimStatus && (
354
- <Button variant="outline" size="sm" className="w-full mt-1 text-xs gap-1.5">
355
- <Link2 className="h-3.5 w-3.5" /> Create Claim Link
356
- </Button>
357
- )}
358
- </div>
313
+ )}
359
314
  </div>
360
315
  </div>
361
- </div>
316
+ )}
362
317
 
363
- {/* Empty state */}
364
- {!stage && !temp && !ratings && !queue?.pending_opportunity_id && (
318
+ {/* Empty State */}
319
+ {!stage && !score && !temp && !ratings && !queue?.pending_opportunity_id && (
365
320
  <div className="text-center py-12 text-muted-foreground">
366
- <Filter className="h-10 w-10 mx-auto mb-3 opacity-30" />
321
+ <Funnel className="h-10 w-10 mx-auto mb-3 opacity-30" />
367
322
  <p className="text-sm">No funnel data yet.</p>
368
323
  <p className="text-xs mt-1">Signals will appear here as they are processed.</p>
369
324
  </div>
@@ -410,7 +365,7 @@ export default function AccountDetailPage() {
410
365
  <TabsList className="h-9 bg-transparent p-0 gap-4">
411
366
  {[
412
367
  { value: 'people', label: 'People', icon: User },
413
- { value: 'funnel', label: 'Funnel', icon: Filter },
368
+ { value: 'funnel', label: 'Funnel', icon: Funnel },
414
369
  { value: 'tickets', label: 'Tickets', icon: Ticket },
415
370
  { value: 'deals', label: 'Deals', icon: Handshake },
416
371
  { value: 'health', label: 'Health', icon: Heart },
@@ -1,17 +1,16 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { useNavigate } from 'react-router-dom'
3
2
  import { apiFetch } from '@core/lib/api'
4
3
 
5
4
  interface Person {
6
5
  id: string
7
6
  email: string
8
- full_name?: string
7
+ first_name?: string
8
+ last_name?: string
9
9
  account_id?: string
10
10
  created_at: string
11
11
  }
12
12
 
13
13
  export default function ContactsPage() {
14
- const navigate = useNavigate()
15
14
  const [people, setPeople] = useState<Person[]>([])
16
15
  const [loading, setLoading] = useState(true)
17
16
  const [search, setSearch] = useState('')
@@ -26,8 +25,9 @@ export default function ContactsPage() {
26
25
 
27
26
  const filtered = people.filter(p => {
28
27
  const q = search.toLowerCase()
29
- return !q || (p.email || '').toLowerCase().includes(q) ||
30
- (p.full_name || '').toLowerCase().includes(q)
28
+ return !q || p.email.toLowerCase().includes(q) ||
29
+ (p.first_name || '').toLowerCase().includes(q) ||
30
+ (p.last_name || '').toLowerCase().includes(q)
31
31
  })
32
32
 
33
33
  return (
@@ -68,9 +68,9 @@ export default function ContactsPage() {
68
68
  </td>
69
69
  </tr>
70
70
  ) : filtered.map(person => (
71
- <tr key={person.id} className="hover:bg-slate-50 cursor-pointer" onClick={() => navigate(`/cortex/crm/contacts/${person.id}`)}>
71
+ <tr key={person.id} className="hover:bg-slate-50">
72
72
  <td className="px-5 py-3 font-medium text-slate-900">
73
- {person.full_name || '—'}
73
+ {[person.first_name, person.last_name].filter(Boolean).join(' ') || '—'}
74
74
  </td>
75
75
  <td className="px-5 py-3 text-slate-600">{person.email}</td>
76
76
  <td className="px-5 py-3 text-right text-slate-400 text-xs">
@@ -11,11 +11,11 @@ interface FunnelSignal {
11
11
  id: string
12
12
  title: string
13
13
  data: {
14
- action?: { action_type?: string; action_value?: number }
15
- identity?: { account_id?: string; person_id?: string; anonymous_id?: string }
16
- classification?: { stage?: string; source?: string }
17
- scoring_components?: { raw_score?: { rating?: number; calculated?: number } }
18
- processing?: { received_at?: string; scored_at?: string }
14
+ signal_type: string
15
+ score_delta: number
16
+ account_id?: string
17
+ person_id?: string
18
+ occurred_at: string
19
19
  }
20
20
  created_at: string
21
21
  }
@@ -95,7 +95,7 @@ export default function IntelligencePage() {
95
95
 
96
96
  useEffect(() => {
97
97
  if (accountsData) {
98
- setAccounts(accountsData.filter((acc: any) => acc.data?.lead_score !== undefined || acc.data?.lifecycle_stage))
98
+ setAccounts(accountsData.filter((acc: any) => acc.data?.lead_score !== undefined))
99
99
  }
100
100
  }, [accountsData])
101
101
 
@@ -206,35 +206,28 @@ export default function IntelligencePage() {
206
206
  <CardContent>
207
207
  <div className="space-y-3">
208
208
  {signals
209
- .filter(signal => signal.data?.identity?.account_id === selectedAccountId)
209
+ .filter(signal => signal.data.account_id === selectedAccountId)
210
210
  .slice(0, 5)
211
- .map((signal) => {
212
- const actionType = signal.data?.action?.action_type
213
- const rating = signal.data?.scoring_components?.raw_score?.rating
214
- const receivedAt = signal.data?.processing?.received_at || signal.created_at
215
- return (
216
- <div key={signal.id} className="flex items-center justify-between p-3 border rounded">
217
- <div className="flex items-center space-x-3">
218
- <Badge className={getSignalTypeColor(actionType || '')}>
219
- {actionType || signal.title}
220
- </Badge>
221
- <div>
222
- <div className="font-medium">{signal.title}</div>
223
- <div className="text-sm text-gray-500">
224
- {new Date(receivedAt).toLocaleDateString()}
225
- </div>
211
+ .map((signal) => (
212
+ <div key={signal.id} className="flex items-center justify-between p-3 border rounded">
213
+ <div className="flex items-center space-x-3">
214
+ <Badge className={getSignalTypeColor(signal.data.signal_type)}>
215
+ {signal.data.signal_type}
216
+ </Badge>
217
+ <div>
218
+ <div className="font-medium">{signal.title}</div>
219
+ <div className="text-sm text-gray-500">
220
+ {new Date(signal.data.occurred_at).toLocaleDateString()}
226
221
  </div>
227
222
  </div>
228
- {rating !== undefined && (
229
- <div className={`font-bold ${
230
- rating >= 4 ? 'text-green-600' : rating >= 3 ? 'text-yellow-600' : 'text-gray-500'
231
- }`}>
232
- {rating}/5
233
- </div>
234
- )}
235
223
  </div>
236
- )
237
- })}
224
+ <div className={`font-bold ${
225
+ signal.data.score_delta > 0 ? 'text-green-600' : 'text-red-600'
226
+ }`}>
227
+ {signal.data.score_delta > 0 ? '+' : ''}{signal.data.score_delta}
228
+ </div>
229
+ </div>
230
+ ))}
238
231
  </div>
239
232
  </CardContent>
240
233
  </Card>
@@ -1,7 +1,7 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { resolveTypeId } from '../../lib/resolveTypeId'
3
2
  import { useParams, useNavigate } from 'react-router-dom'
4
3
  import { apiFetch } from '@core/lib/api'
4
+ import { getTypeIdAsync } from '../hooks/useTypeRegistry'
5
5
  import { RichTextEditor } from '@core/components/ui/RichTextEditor'
6
6
  import { Button } from '@core/components/ui/button'
7
7
  import { Input } from '@core/components/ui/input'
@@ -71,6 +71,7 @@ export default function KBEditorPage() {
71
71
  const [loading, setLoading] = useState(!isNew)
72
72
  const [saving, setSaving] = useState(false)
73
73
  const [error, setError] = useState<string | null>(null)
74
+ const [kbArticleTypeId, setKbArticleTypeId] = useState<string>('')
74
75
 
75
76
  useEffect(() => {
76
77
  if (isNew) return
@@ -94,13 +95,19 @@ export default function KBEditorPage() {
94
95
  .finally(() => setLoading(false))
95
96
  }, [id, isNew])
96
97
 
98
+ useEffect(() => {
99
+ getTypeIdAsync('kb_article').then(id => {
100
+ if (id) setKbArticleTypeId(id)
101
+ })
102
+ }, [])
103
+
97
104
  const handleSave = async (publish?: boolean) => {
98
105
  if (!form.title.trim()) { setError('Title is required'); return }
99
106
  if (!form.kb_type) { setError('KB Type is required'); return }
107
+ if (!kbArticleTypeId) { setError('Type not loaded'); return }
100
108
  setSaving(true)
101
109
  setError(null)
102
110
  try {
103
- const kbArticleTypeId = await resolveTypeId('kb_article')
104
111
  const payload = {
105
112
  title: form.title.trim(),
106
113
  description: form.description,