spine-framework-cortex 0.1.18 → 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 -42
  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 -430
  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
@@ -0,0 +1,378 @@
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 { Input } from '@core/components/ui/input'
6
+ import { Label } from '@core/components/ui/label'
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@core/components/ui/select'
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@core/components/ui/table'
9
+ import { useApi } from '@core/hooks/useApi'
10
+ import { formatDistanceToNow } from 'date-fns'
11
+ import { Search, Filter, Download } from 'lucide-react'
12
+
13
+ interface SignalRecord {
14
+ id: string
15
+ action_type: string
16
+ action_value: number
17
+ stage: string
18
+ source: string
19
+ created_at: string
20
+ account_name?: string
21
+ person_email?: string
22
+ anonymous_id?: string
23
+ session_id?: string
24
+ rating: number
25
+ raw_score: number
26
+ referrer_domain?: string
27
+ utm_source?: string
28
+ utm_medium?: string
29
+ utm_campaign?: string
30
+ }
31
+
32
+ interface AuditFilters {
33
+ date_from?: string
34
+ date_to?: string
35
+ source?: string
36
+ stage?: string
37
+ action_type?: string
38
+ min_rating?: number
39
+ account_id?: string
40
+ search?: string
41
+ }
42
+
43
+ export default function AuditFunnelPage() {
44
+ const [signals, setSignals] = useState<SignalRecord[]>([])
45
+ const [loading, setLoading] = useState(false)
46
+ const [filters, setFilters] = useState<AuditFilters>({})
47
+ const [totalCount, setTotalCount] = useState(0)
48
+ const [page, setPage] = useState(1)
49
+ const { apiFetch } = useApi()
50
+
51
+ useEffect(() => {
52
+ loadSignals()
53
+ }, [page, filters])
54
+
55
+ const loadSignals = async () => {
56
+ setLoading(true)
57
+ try {
58
+ const params = new URLSearchParams({
59
+ page: page.toString(),
60
+ limit: '50',
61
+ ...Object.fromEntries(
62
+ Object.entries(filters).filter(([_, v]) => v != null && v !== '')
63
+ )
64
+ })
65
+
66
+ const response = await apiFetch(`/admin/signals/audit?${params}`)
67
+ setSignals(response.signals || [])
68
+ setTotalCount(response.total || 0)
69
+ } catch (error) {
70
+ console.error('Failed to load signals:', error)
71
+ } finally {
72
+ setLoading(false)
73
+ }
74
+ }
75
+
76
+ const exportData = async () => {
77
+ try {
78
+ const params = new URLSearchParams({
79
+ export: 'true',
80
+ ...Object.fromEntries(
81
+ Object.entries(filters).filter(([_, v]) => v != null && v !== '')
82
+ )
83
+ })
84
+
85
+ const response = await apiFetch(`/admin/signals/audit?${params}`)
86
+
87
+ // Create download link
88
+ const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' })
89
+ const url = window.URL.createObjectURL(blob)
90
+ const a = document.createElement('a')
91
+ a.href = url
92
+ a.download = `signals-audit-${new Date().toISOString().split('T')[0]}.json`
93
+ document.body.appendChild(a)
94
+ a.click()
95
+ window.URL.revokeObjectURL(url)
96
+ document.body.removeChild(a)
97
+ } catch (error) {
98
+ console.error('Failed to export data:', error)
99
+ }
100
+ }
101
+
102
+ const updateFilter = (key: keyof AuditFilters, value: any) => {
103
+ setFilters(prev => ({ ...prev, [key]: value }))
104
+ setPage(1) // Reset to first page when filtering
105
+ }
106
+
107
+ const getRatingColor = (rating: number) => {
108
+ if (rating >= 4) return 'bg-green-100 text-green-800'
109
+ if (rating >= 3) return 'bg-yellow-100 text-yellow-800'
110
+ return 'bg-red-100 text-red-800'
111
+ }
112
+
113
+ const getSourceColor = (source: string) => {
114
+ const colors = {
115
+ mar: 'bg-blue-100 text-blue-800',
116
+ int: 'bg-purple-100 text-purple-800',
117
+ use: 'bg-green-100 text-green-800',
118
+ manual: 'bg-gray-100 text-gray-800'
119
+ }
120
+ return colors[source as keyof typeof colors] || 'bg-gray-100 text-gray-800'
121
+ }
122
+
123
+ return (
124
+ <div className="p-6 space-y-6">
125
+ <div className="flex justify-between items-center">
126
+ <div>
127
+ <h1 className="text-3xl font-bold">Funnel Audit</h1>
128
+ <p className="text-muted-foreground">
129
+ Detailed audit trail of all funnel signals and conversions
130
+ </p>
131
+ </div>
132
+ <div className="flex space-x-2">
133
+ <Button variant="outline" onClick={exportData}>
134
+ <Download className="w-4 h-4 mr-2" />
135
+ Export
136
+ </Button>
137
+ <Button onClick={loadSignals}>Refresh</Button>
138
+ </div>
139
+ </div>
140
+
141
+ {/* Filters */}
142
+ <Card>
143
+ <CardHeader>
144
+ <CardTitle className="flex items-center">
145
+ <Filter className="w-4 h-4 mr-2" />
146
+ Filters
147
+ </CardTitle>
148
+ </CardHeader>
149
+ <CardContent>
150
+ <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4">
151
+ <div>
152
+ <Label htmlFor="search">Search</Label>
153
+ <div className="relative">
154
+ <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
155
+ <Input
156
+ id="search"
157
+ placeholder="Email, domain..."
158
+ className="pl-8"
159
+ value={filters.search || ''}
160
+ onChange={(e) => updateFilter('search', e.target.value)}
161
+ />
162
+ </div>
163
+ </div>
164
+
165
+ <div>
166
+ <Label htmlFor="date_from">From Date</Label>
167
+ <Input
168
+ id="date_from"
169
+ type="date"
170
+ value={filters.date_from || ''}
171
+ onChange={(e) => updateFilter('date_from', e.target.value)}
172
+ />
173
+ </div>
174
+
175
+ <div>
176
+ <Label htmlFor="date_to">To Date</Label>
177
+ <Input
178
+ id="date_to"
179
+ type="date"
180
+ value={filters.date_to || ''}
181
+ onChange={(e) => updateFilter('date_to', e.target.value)}
182
+ />
183
+ </div>
184
+
185
+ <div>
186
+ <Label htmlFor="source">Source</Label>
187
+ <Select value={filters.source || ''} onValueChange={(value) => updateFilter('source', value)}>
188
+ <SelectTrigger>
189
+ <SelectValue placeholder="All sources" />
190
+ </SelectTrigger>
191
+ <SelectContent>
192
+ <SelectItem value="">All sources</SelectItem>
193
+ <SelectItem value="mar">Marketing</SelectItem>
194
+ <SelectItem value="int">Internal</SelectItem>
195
+ <SelectItem value="use">Usage</SelectItem>
196
+ <SelectItem value="manual">Manual</SelectItem>
197
+ </SelectContent>
198
+ </Select>
199
+ </div>
200
+
201
+ <div>
202
+ <Label htmlFor="stage">Stage</Label>
203
+ <Select value={filters.stage || ''} onValueChange={(value) => updateFilter('stage', value)}>
204
+ <SelectTrigger>
205
+ <SelectValue placeholder="All stages" />
206
+ </SelectTrigger>
207
+ <SelectContent>
208
+ <SelectItem value="">All stages</SelectItem>
209
+ <SelectItem value="anonymous">Anonymous</SelectItem>
210
+ <SelectItem value="identified">Identified</SelectItem>
211
+ <SelectItem value="installed">Installed</SelectItem>
212
+ </SelectContent>
213
+ </Select>
214
+ </div>
215
+
216
+ <div>
217
+ <Label htmlFor="min_rating">Min Rating</Label>
218
+ <Select value={filters.min_rating?.toString() || ''} onValueChange={(value) => updateFilter('min_rating', value ? parseInt(value) : undefined)}>
219
+ <SelectTrigger>
220
+ <SelectValue placeholder="Any rating" />
221
+ </SelectTrigger>
222
+ <SelectContent>
223
+ <SelectItem value="">Any rating</SelectItem>
224
+ <SelectItem value="4">4+ (High)</SelectItem>
225
+ <SelectItem value="3">3+ (Medium)</SelectItem>
226
+ <SelectItem value="2">2+ (Low)</SelectItem>
227
+ <SelectItem value="1">1+ (All)</SelectItem>
228
+ </SelectContent>
229
+ </Select>
230
+ </div>
231
+ </div>
232
+ </CardContent>
233
+ </Card>
234
+
235
+ {/* Results Summary */}
236
+ <Card>
237
+ <CardHeader>
238
+ <CardTitle>Results</CardTitle>
239
+ <CardDescription>
240
+ Showing {signals.length} of {totalCount} signals
241
+ </CardDescription>
242
+ </CardHeader>
243
+ <CardContent>
244
+ <Table>
245
+ <TableHeader>
246
+ <TableRow>
247
+ <TableHead>Timestamp</TableHead>
248
+ <TableHead>Action</TableHead>
249
+ <TableHead>User</TableHead>
250
+ <TableHead>Source</TableHead>
251
+ <TableHead>Stage</TableHead>
252
+ <TableHead>Rating</TableHead>
253
+ <TableHead>Score</TableHead>
254
+ <TableHead>Referrer</TableHead>
255
+ <TableHead>UTM</TableHead>
256
+ </TableRow>
257
+ </TableHeader>
258
+ <TableBody>
259
+ {loading ? (
260
+ <TableRow>
261
+ <TableCell colSpan={9} className="text-center py-8">
262
+ Loading signals...
263
+ </TableCell>
264
+ </TableRow>
265
+ ) : signals.length === 0 ? (
266
+ <TableRow>
267
+ <TableCell colSpan={9} className="text-center py-8">
268
+ No signals found matching the current filters
269
+ </TableCell>
270
+ </TableRow>
271
+ ) : (
272
+ signals.map((signal) => (
273
+ <TableRow key={signal.id}>
274
+ <TableCell>
275
+ <div>
276
+ <div className="text-sm">
277
+ {formatDistanceToNow(new Date(signal.created_at), { addSuffix: true })}
278
+ </div>
279
+ <div className="text-xs text-muted-foreground">
280
+ {new Date(signal.created_at).toLocaleString()}
281
+ </div>
282
+ </div>
283
+ </TableCell>
284
+ <TableCell>
285
+ <div>
286
+ <div className="font-medium">{signal.action_type}</div>
287
+ <div className="text-sm text-muted-foreground">
288
+ Value: {signal.action_value}
289
+ </div>
290
+ </div>
291
+ </TableCell>
292
+ <TableCell>
293
+ <div>
294
+ {signal.account_name ? (
295
+ <div>
296
+ <div className="font-medium">{signal.account_name}</div>
297
+ <div className="text-sm text-muted-foreground">{signal.person_email}</div>
298
+ </div>
299
+ ) : signal.anonymous_id ? (
300
+ <div className="text-sm text-muted-foreground">
301
+ Anonymous: {signal.anonymous_id.slice(0, 8)}...
302
+ </div>
303
+ ) : (
304
+ <div className="text-sm text-muted-foreground">Unknown</div>
305
+ )}
306
+ </div>
307
+ </TableCell>
308
+ <TableCell>
309
+ <Badge className={getSourceColor(signal.source)}>
310
+ {signal.source}
311
+ </Badge>
312
+ </TableCell>
313
+ <TableCell>
314
+ <Badge variant="outline">{signal.stage}</Badge>
315
+ </TableCell>
316
+ <TableCell>
317
+ <Badge className={getRatingColor(signal.rating)}>
318
+ {signal.rating}
319
+ </Badge>
320
+ </TableCell>
321
+ <TableCell>
322
+ <span className="font-mono text-sm">{signal.raw_score}</span>
323
+ </TableCell>
324
+ <TableCell>
325
+ <div className="max-w-32 truncate text-sm">
326
+ {signal.referrer_domain || 'Direct'}
327
+ </div>
328
+ </TableCell>
329
+ <TableCell>
330
+ <div className="text-sm space-y-1">
331
+ {signal.utm_source && (
332
+ <div>Source: {signal.utm_source}</div>
333
+ )}
334
+ {signal.utm_medium && (
335
+ <div>Medium: {signal.utm_medium}</div>
336
+ )}
337
+ {signal.utm_campaign && (
338
+ <div>Campaign: {signal.utm_campaign}</div>
339
+ )}
340
+ </div>
341
+ </TableCell>
342
+ </TableRow>
343
+ ))
344
+ )}
345
+ </TableBody>
346
+ </Table>
347
+
348
+ {/* Pagination */}
349
+ {totalCount > 50 && (
350
+ <div className="flex justify-between items-center mt-4">
351
+ <div className="text-sm text-muted-foreground">
352
+ Page {page} of {Math.ceil(totalCount / 50)}
353
+ </div>
354
+ <div className="flex space-x-2">
355
+ <Button
356
+ variant="outline"
357
+ size="sm"
358
+ onClick={() => setPage(p => Math.max(1, p - 1))}
359
+ disabled={page === 1}
360
+ >
361
+ Previous
362
+ </Button>
363
+ <Button
364
+ variant="outline"
365
+ size="sm"
366
+ onClick={() => setPage(p => p + 1)}
367
+ disabled={page >= Math.ceil(totalCount / 50)}
368
+ >
369
+ Next
370
+ </Button>
371
+ </div>
372
+ </div>
373
+ )}
374
+ </CardContent>
375
+ </Card>
376
+ </div>
377
+ )
378
+ }