spine-framework-cortex 0.1.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 (40) hide show
  1. package/components/CortexSidebar.tsx +130 -0
  2. package/functions/custom_anonymous-sessions.ts +356 -0
  3. package/functions/custom_case_analysis.ts +507 -0
  4. package/functions/custom_community-escalation.ts +234 -0
  5. package/functions/custom_cortex-chunks.ts +52 -0
  6. package/functions/custom_cortex-handler.ts +35 -0
  7. package/functions/custom_funnel-scoring.ts +256 -0
  8. package/functions/custom_funnel-signal.ts +678 -0
  9. package/functions/custom_funnel-timers.ts +449 -0
  10. package/functions/custom_kb-chunker-test.ts +364 -0
  11. package/functions/custom_kb-chunker.ts +576 -0
  12. package/functions/custom_kb-embeddings.ts +481 -0
  13. package/functions/custom_kb-ingestion.ts +448 -0
  14. package/functions/custom_support-triage.ts +649 -0
  15. package/functions/custom_tag_management.ts +314 -0
  16. package/index.tsx +103 -0
  17. package/manifest.json +82 -0
  18. package/package.json +29 -0
  19. package/pages/CortexDashboard.tsx +97 -0
  20. package/pages/community/CommunityPage.tsx +159 -0
  21. package/pages/courses/CoursesPage.tsx +231 -0
  22. package/pages/crm/AccountDetailPage.tsx +393 -0
  23. package/pages/crm/AccountsPage.tsx +164 -0
  24. package/pages/crm/ActivityPage.tsx +82 -0
  25. package/pages/crm/ContactDetailPage.tsx +184 -0
  26. package/pages/crm/ContactsPage.tsx +87 -0
  27. package/pages/crm/DealDetailPage.tsx +191 -0
  28. package/pages/crm/DealsPage.tsx +169 -0
  29. package/pages/crm/HealthPage.tsx +109 -0
  30. package/pages/intelligence/IntelligencePage.tsx +314 -0
  31. package/pages/kb/KBEditorPage.tsx +328 -0
  32. package/pages/kb/KBIngestionPage.tsx +409 -0
  33. package/pages/kb/KBPage.tsx +258 -0
  34. package/pages/support/RedactionReview.tsx +562 -0
  35. package/pages/support/SupportPage.tsx +395 -0
  36. package/pages/support/TicketDetailPage.tsx +919 -0
  37. package/seed/accounts.json +9 -0
  38. package/seed/link-types.json +44 -0
  39. package/seed/triggers.json +80 -0
  40. package/seed/types.json +352 -0
@@ -0,0 +1,109 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { apiFetch } from '@core/lib/api'
3
+
4
+ interface HealthRecord {
5
+ id: string
6
+ title: string
7
+ data: { temperature?: 'green' | 'yellow' | 'red'; churn_risk_score?: number; adoption_score?: number; nps_score?: number; notes?: string }
8
+ created_at: string
9
+ }
10
+
11
+ const TEMP_STYLE: Record<string, { bg: string; text: string; dot: string; label: string }> = {
12
+ green: { bg: 'bg-green-50', text: 'text-green-700', dot: 'bg-green-500', label: 'Healthy' },
13
+ yellow: { bg: 'bg-yellow-50', text: 'text-yellow-700', dot: 'bg-yellow-500', label: 'At Risk' },
14
+ red: { bg: 'bg-red-50', text: 'text-red-700', dot: 'bg-red-500', label: 'Critical' },
15
+ }
16
+
17
+ function ScoreBar({ label, value }: { label: string; value?: number }) {
18
+ if (value == null) return null
19
+ const pct = Math.min(100, Math.max(0, value))
20
+ const color = pct >= 70 ? 'bg-green-500' : pct >= 40 ? 'bg-yellow-500' : 'bg-red-500'
21
+ return (
22
+ <div className="flex items-center gap-2 text-xs">
23
+ <span className="w-20 text-slate-500 flex-shrink-0">{label}</span>
24
+ <div className="flex-1 bg-slate-100 rounded-full h-1.5">
25
+ <div className={`${color} h-1.5 rounded-full`} style={{ width: `${pct}%` }} />
26
+ </div>
27
+ <span className="w-8 text-right text-slate-600 font-medium">{value}</span>
28
+ </div>
29
+ )
30
+ }
31
+
32
+ export default function HealthPage() {
33
+ const [records, setRecords] = useState<HealthRecord[]>([])
34
+ const [loading, setLoading] = useState(true)
35
+ const [filter, setFilter] = useState<'all' | 'green' | 'yellow' | 'red'>('all')
36
+
37
+ useEffect(() => {
38
+ apiFetch('/api/admin-data?action=list&entity=items&type_slug=csm_health&limit=200')
39
+ .then(r => r.json())
40
+ .then(json => setRecords(Array.isArray(json?.data) ? json.data : json || []))
41
+ .catch(() => setRecords([]))
42
+ .finally(() => setLoading(false))
43
+ }, [])
44
+
45
+ const filtered = filter === 'all' ? records : records.filter(r => r.data?.temperature === filter)
46
+ const counts = { green: records.filter(r => r.data?.temperature === 'green').length, yellow: records.filter(r => r.data?.temperature === 'yellow').length, red: records.filter(r => r.data?.temperature === 'red').length }
47
+
48
+ return (
49
+ <div className="p-6 space-y-5">
50
+ <div>
51
+ <h1 className="text-2xl font-bold text-slate-900">Customer Health</h1>
52
+ <p className="text-slate-500 text-sm mt-1">CSM health records across accounts</p>
53
+ </div>
54
+
55
+ <div className="grid grid-cols-3 gap-4">
56
+ {(['green', 'yellow', 'red'] as const).map(temp => {
57
+ const s = TEMP_STYLE[temp]
58
+ return (
59
+ <button
60
+ key={temp}
61
+ onClick={() => setFilter(f => f === temp ? 'all' : temp)}
62
+ className={`${s.bg} rounded-lg p-4 text-left border-2 transition-colors ${filter === temp ? 'border-current' : 'border-transparent'}`}
63
+ >
64
+ <div className="flex items-center gap-2 mb-1">
65
+ <div className={`w-2.5 h-2.5 rounded-full ${s.dot}`} />
66
+ <span className={`text-xs font-semibold uppercase tracking-wide ${s.text}`}>{s.label}</span>
67
+ </div>
68
+ <div className={`text-3xl font-bold ${s.text}`}>{counts[temp]}</div>
69
+ </button>
70
+ )
71
+ })}
72
+ </div>
73
+
74
+ <div className="space-y-3">
75
+ {loading ? (
76
+ <div className="text-center py-12 text-slate-400">Loading health records…</div>
77
+ ) : filtered.length === 0 ? (
78
+ <div className="text-center py-12 text-slate-400 bg-white rounded-lg border border-slate-200">
79
+ No health records {filter !== 'all' ? `with ${filter} status` : 'yet'}.
80
+ </div>
81
+ ) : filtered.map(record => {
82
+ const temp = record.data?.temperature || 'green'
83
+ const s = TEMP_STYLE[temp]
84
+ return (
85
+ <div key={record.id} className={`${s.bg} rounded-lg border border-slate-200 p-4`}>
86
+ <div className="flex items-center justify-between mb-3">
87
+ <div className="font-semibold text-slate-900">{record.title}</div>
88
+ <div className={`flex items-center gap-1.5 text-xs font-medium ${s.text}`}>
89
+ <div className={`w-2 h-2 rounded-full ${s.dot}`} />
90
+ {s.label}
91
+ </div>
92
+ </div>
93
+ <div className="space-y-1.5">
94
+ <ScoreBar label="Adoption" value={record.data?.adoption_score} />
95
+ <ScoreBar label="Churn Risk" value={record.data?.churn_risk_score != null ? 100 - record.data.churn_risk_score : undefined} />
96
+ </div>
97
+ {record.data?.nps_score != null && (
98
+ <div className="mt-2 text-xs text-slate-500">NPS: <span className="font-semibold text-slate-700">{record.data.nps_score}</span></div>
99
+ )}
100
+ {record.data?.notes && (
101
+ <div className="mt-2 text-xs text-slate-600 italic">{record.data.notes}</div>
102
+ )}
103
+ </div>
104
+ )
105
+ })}
106
+ </div>
107
+ </div>
108
+ )
109
+ }
@@ -0,0 +1,314 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@core/components/ui/card'
3
+ import { Badge } from '@core/components/ui/badge'
4
+ import { Button } from '@core/components/ui/button'
5
+ import { LoadingSpinner } from '@core/components/ui/LoadingSpinner'
6
+ import { useApi } from '@core/hooks/useApi'
7
+ import { apiFetch } from '@core/lib/api'
8
+ import { Brain, TrendingUp, Users, Target, Clock, AlertCircle } from 'lucide-react'
9
+
10
+ interface FunnelSignal {
11
+ id: string
12
+ title: string
13
+ data: {
14
+ signal_type: string
15
+ score_delta: number
16
+ account_id?: string
17
+ person_id?: string
18
+ occurred_at: string
19
+ }
20
+ created_at: string
21
+ }
22
+
23
+ interface Account {
24
+ id: string
25
+ display_name: string
26
+ data: {
27
+ lead_score?: number
28
+ lifecycle_stage?: string
29
+ }
30
+ }
31
+
32
+ interface Task {
33
+ id: string
34
+ title: string
35
+ data: {
36
+ task_type: string
37
+ priority: string
38
+ account_id?: string
39
+ person_id?: string
40
+ description: string
41
+ due_date?: string
42
+ }
43
+ status: string
44
+ }
45
+
46
+ interface ActivityLog {
47
+ id: string
48
+ title: string
49
+ data: {
50
+ action: string
51
+ account_id?: string
52
+ signal_type?: string
53
+ score_delta?: number
54
+ new_score?: number
55
+ new_stage?: string
56
+ }
57
+ created_at: string
58
+ }
59
+
60
+ export default function IntelligencePage() {
61
+ const [selectedAccountId, setSelectedAccountId] = useState<string>('')
62
+ const [accounts, setAccounts] = useState<Account[]>([])
63
+ const [signals, setSignals] = useState<FunnelSignal[]>([])
64
+ const [tasks, setTasks] = useState<Task[]>([])
65
+ const [activities, setActivities] = useState<ActivityLog[]>([])
66
+ const [loading, setLoading] = useState(true)
67
+
68
+ // Fetch accounts with lead scores
69
+ const { data: accountsData, loading: accountsLoading } = useApi(async () => {
70
+ const res = await apiFetch('/api/admin-data?action=list&entity=accounts&limit=50')
71
+ const json = await res.json()
72
+ return json.data || []
73
+ })
74
+
75
+ // Fetch funnel signals
76
+ const { data: signalsData, loading: signalsLoading } = useApi(async () => {
77
+ const res = await apiFetch('/api/admin-data?action=list&entity=items&type_slug=funnel_signal&limit=20')
78
+ const json = await res.json()
79
+ return json.data || []
80
+ })
81
+
82
+ // Fetch tasks
83
+ const { data: tasksData, loading: tasksLoading } = useApi(async () => {
84
+ const res = await apiFetch('/api/admin-data?action=list&entity=items&type_slug=task&limit=20')
85
+ const json = await res.json()
86
+ return json.data || []
87
+ })
88
+
89
+ // Fetch activity logs
90
+ const { data: activitiesData, loading: activitiesLoading } = useApi(async () => {
91
+ const res = await apiFetch('/api/admin-data?action=list&entity=items&type_slug=activity_log&limit=20')
92
+ const json = await res.json()
93
+ return json.data || []
94
+ })
95
+
96
+ useEffect(() => {
97
+ if (accountsData) {
98
+ setAccounts(accountsData.filter((acc: any) => acc.data?.lead_score !== undefined))
99
+ }
100
+ }, [accountsData])
101
+
102
+ useEffect(() => {
103
+ if (signalsData) setSignals(signalsData)
104
+ if (tasksData) setTasks(tasksData)
105
+ if (activitiesData) setActivities(activitiesData)
106
+ setLoading(false)
107
+ }, [signalsData, tasksData, activitiesData])
108
+
109
+ const selectedAccount = accounts.find(acc => acc.id === selectedAccountId)
110
+
111
+ const getLifecycleStageColor = (stage?: string) => {
112
+ switch (stage) {
113
+ case 'product_qualified_lead': return 'bg-green-500'
114
+ case 'engaged_lead': return 'bg-blue-500'
115
+ case 'identified_lead': return 'bg-yellow-500'
116
+ case 'anonymous': return 'bg-gray-500'
117
+ default: return 'bg-gray-400'
118
+ }
119
+ }
120
+
121
+ const getSignalTypeColor = (type: string) => {
122
+ const colors: Record<string, string> = {
123
+ 'docs_view': 'bg-blue-100 text-blue-800',
124
+ 'pricing_visit': 'bg-purple-100 text-purple-800',
125
+ 'portal_account_created': 'bg-green-100 text-green-800',
126
+ 'spine_install_registered': 'bg-orange-100 text-orange-800',
127
+ 'marketplace_app_installed': 'bg-pink-100 text-pink-800'
128
+ }
129
+ return colors[type] || 'bg-gray-100 text-gray-800'
130
+ }
131
+
132
+ const getPriorityColor = (priority: string) => {
133
+ switch (priority) {
134
+ case 'high': return 'bg-red-100 text-red-800'
135
+ case 'medium': return 'bg-yellow-100 text-yellow-800'
136
+ case 'low': return 'bg-green-100 text-green-800'
137
+ default: return 'bg-gray-100 text-gray-800'
138
+ }
139
+ }
140
+
141
+ if (loading || accountsLoading || signalsLoading || tasksLoading || activitiesLoading) {
142
+ return <LoadingSpinner />
143
+ }
144
+
145
+ return (
146
+ <div className="space-y-6">
147
+ <div className="flex items-center justify-between">
148
+ <div>
149
+ <h1 className="text-3xl font-bold">Cortex Intelligence</h1>
150
+ <p className="text-muted-foreground">Funnel intelligence and lead scoring insights</p>
151
+ </div>
152
+ <Button onClick={() => window.location.reload()}>
153
+ <Brain className="w-4 h-4 mr-2" />
154
+ Refresh Data
155
+ </Button>
156
+ </div>
157
+
158
+ {/* Account Selector */}
159
+ <Card>
160
+ <CardHeader>
161
+ <CardTitle className="flex items-center">
162
+ <Users className="w-5 h-5 mr-2" />
163
+ Account Intelligence
164
+ </CardTitle>
165
+ <CardDescription>Select an account to view detailed intelligence</CardDescription>
166
+ </CardHeader>
167
+ <CardContent>
168
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
169
+ {accounts.map((account) => (
170
+ <div
171
+ key={account.id}
172
+ className={`p-4 border rounded-lg cursor-pointer transition-colors ${
173
+ selectedAccountId === account.id ? 'border-blue-500 bg-blue-50' : 'hover:border-gray-300'
174
+ }`}
175
+ onClick={() => setSelectedAccountId(account.id)}
176
+ >
177
+ <div className="flex items-center justify-between mb-2">
178
+ <h3 className="font-semibold">{account.display_name}</h3>
179
+ <Badge className={getLifecycleStageColor(account.data?.lifecycle_stage)}>
180
+ {account.data?.lifecycle_stage || 'unknown'}
181
+ </Badge>
182
+ </div>
183
+ <div className="flex items-center justify-between">
184
+ <div className="text-2xl font-bold text-blue-600">
185
+ {account.data?.lead_score || 0}
186
+ </div>
187
+ <div className="text-sm text-gray-500">Lead Score</div>
188
+ </div>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ </CardContent>
193
+ </Card>
194
+
195
+ {selectedAccount && (
196
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
197
+ {/* Recent Signals */}
198
+ <Card>
199
+ <CardHeader>
200
+ <CardTitle className="flex items-center">
201
+ <TrendingUp className="w-5 h-5 mr-2" />
202
+ Recent Funnel Signals
203
+ </CardTitle>
204
+ <CardDescription>Latest signals for {selectedAccount?.display_name}</CardDescription>
205
+ </CardHeader>
206
+ <CardContent>
207
+ <div className="space-y-3">
208
+ {signals
209
+ .filter(signal => signal.data.account_id === selectedAccountId)
210
+ .slice(0, 5)
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()}
221
+ </div>
222
+ </div>
223
+ </div>
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
+ ))}
231
+ </div>
232
+ </CardContent>
233
+ </Card>
234
+
235
+ {/* Tasks */}
236
+ <Card>
237
+ <CardHeader>
238
+ <CardTitle className="flex items-center">
239
+ <Target className="w-5 h-5 mr-2" />
240
+ Recommended Actions
241
+ </CardTitle>
242
+ <CardDescription>Tasks for {selectedAccount?.display_name}</CardDescription>
243
+ </CardHeader>
244
+ <CardContent>
245
+ <div className="space-y-3">
246
+ {tasks
247
+ .filter(task => task.data.account_id === selectedAccountId)
248
+ .slice(0, 5)
249
+ .map((task) => (
250
+ <div key={task.id} className="p-3 border rounded">
251
+ <div className="flex items-center justify-between mb-2">
252
+ <Badge className={getPriorityColor(task.data.priority)}>
253
+ {task.data.priority}
254
+ </Badge>
255
+ {task.data.due_date && (
256
+ <div className="flex items-center text-sm text-gray-500">
257
+ <Clock className="w-4 h-4 mr-1" />
258
+ {new Date(task.data.due_date).toLocaleDateString()}
259
+ </div>
260
+ )}
261
+ </div>
262
+ <h4 className="font-medium mb-1">{task.title}</h4>
263
+ <p className="text-sm text-gray-600">{task.data.description}</p>
264
+ </div>
265
+ ))}
266
+ </div>
267
+ </CardContent>
268
+ </Card>
269
+ </div>
270
+ )}
271
+
272
+ {/* Activity Timeline */}
273
+ <Card>
274
+ <CardHeader>
275
+ <CardTitle className="flex items-center">
276
+ <Clock className="w-5 h-5 mr-2" />
277
+ Activity Timeline
278
+ </CardTitle>
279
+ <CardDescription>Recent funnel intelligence activities</CardDescription>
280
+ </CardHeader>
281
+ <CardContent>
282
+ <div className="space-y-3">
283
+ {activities.slice(0, 10).map((activity) => (
284
+ <div key={activity.id} className="flex items-start space-x-3 p-3 border rounded">
285
+ <AlertCircle className="w-5 h-5 mt-0.5 text-blue-500" />
286
+ <div className="flex-1">
287
+ <div className="flex items-center space-x-2 mb-1">
288
+ <Badge variant="outline">{activity.data.action}</Badge>
289
+ {activity.data.signal_type && (
290
+ <Badge className={getSignalTypeColor(activity.data.signal_type)}>
291
+ {activity.data.signal_type}
292
+ </Badge>
293
+ )}
294
+ </div>
295
+ <p className="text-sm text-gray-600">
296
+ {activity.data.new_score !== undefined && (
297
+ <>Score updated to {activity.data.new_score}</>
298
+ )}
299
+ {activity.data.new_stage && (
300
+ <> · Stage: {activity.data.new_stage}</>
301
+ )}
302
+ </p>
303
+ <div className="text-xs text-gray-500 mt-1">
304
+ {new Date(activity.created_at).toLocaleString()}
305
+ </div>
306
+ </div>
307
+ </div>
308
+ ))}
309
+ </div>
310
+ </CardContent>
311
+ </Card>
312
+ </div>
313
+ )
314
+ }