spine-framework-cortex 0.1.7 → 1.0.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.
- package/LICENSE.md +223 -0
- package/README.md +185 -13
- package/custom/apps/cortex/LICENSE.md +13 -0
- package/custom/apps/cortex/README.md +27 -0
- package/custom/apps/cortex/api/cortex-handler.ts +35 -0
- package/{components → custom/apps/cortex/components}/CortexSidebar.tsx +27 -22
- package/custom/apps/cortex/docs/configuration.md +222 -0
- package/{index.tsx → custom/apps/cortex/index.tsx} +14 -5
- package/custom/apps/cortex/kb-ingestion.tsx +217 -0
- package/{manifest.json → custom/apps/cortex/manifest.json} +12 -1
- package/custom/apps/cortex/package.json +31 -0
- package/{pages → custom/apps/cortex/pages}/crm/AccountDetailPage.tsx +6 -6
- package/package.json +82 -20
- /package/{functions → custom/apps/cortex/functions}/custom_anonymous-sessions.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_case_analysis.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_community-escalation.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_cortex-chunks.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_cortex-handler.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_funnel-scoring.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_funnel-signal.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_funnel-timers.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_kb-chunker-test.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_kb-chunker.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_kb-embeddings.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_kb-ingestion.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_support-triage.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/custom_tag_management.ts +0 -0
- /package/{functions → custom/apps/cortex/functions}/webhook-handlers.ts +0 -0
- /package/{lib → custom/apps/cortex/lib}/resolveTypeId.ts +0 -0
- /package/{pages → custom/apps/cortex/pages}/CortexDashboard.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/community/CommunityPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/courses/CoursesPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/crm/AccountsPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/crm/ActivityPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/crm/ContactDetailPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/crm/ContactsPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/crm/DealDetailPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/crm/DealsPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/crm/HealthPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/intelligence/IntelligencePage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/kb/KBEditorPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/kb/KBIngestionPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/kb/KBPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/support/RedactionReview.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/support/SupportPage.tsx +0 -0
- /package/{pages → custom/apps/cortex/pages}/support/TicketDetailPage.tsx +0 -0
- /package/{seed → custom/apps/cortex/seed}/accounts.json +0 -0
- /package/{seed → custom/apps/cortex/seed}/link-types.json +0 -0
- /package/{seed → custom/apps/cortex/seed}/pipelines.json +0 -0
- /package/{seed → custom/apps/cortex/seed}/roles.json +0 -0
- /package/{seed → custom/apps/cortex/seed}/triggers.json +0 -0
- /package/{seed → custom/apps/cortex/seed}/types.json +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Cortex App Configuration
|
|
2
|
+
|
|
3
|
+
This guide explains how to configure and deploy the Cortex app for customer support, CRM, and community management.
|
|
4
|
+
|
|
5
|
+
## Database Configuration
|
|
6
|
+
|
|
7
|
+
### 1. Apps Table Entry
|
|
8
|
+
|
|
9
|
+
Insert the Cortex app into the `public.apps` table:
|
|
10
|
+
|
|
11
|
+
```sql
|
|
12
|
+
INSERT INTO public.apps (
|
|
13
|
+
id,
|
|
14
|
+
slug,
|
|
15
|
+
name,
|
|
16
|
+
description,
|
|
17
|
+
version,
|
|
18
|
+
app_type,
|
|
19
|
+
source,
|
|
20
|
+
owner_account_id,
|
|
21
|
+
is_active,
|
|
22
|
+
is_system,
|
|
23
|
+
min_role,
|
|
24
|
+
config,
|
|
25
|
+
nav_items,
|
|
26
|
+
route_prefix,
|
|
27
|
+
renderer,
|
|
28
|
+
created_at
|
|
29
|
+
) VALUES (
|
|
30
|
+
gen_random_uuid(),
|
|
31
|
+
'cortex',
|
|
32
|
+
'Cortex',
|
|
33
|
+
'Unified workspace for CRM, Support, Community, and Knowledge Base',
|
|
34
|
+
'1.0.0',
|
|
35
|
+
'custom',
|
|
36
|
+
'spine-framework',
|
|
37
|
+
'd3ab4cf8-33de-4ca5-97a2-dbc288c94338', -- spine-system account
|
|
38
|
+
true,
|
|
39
|
+
false,
|
|
40
|
+
'support',
|
|
41
|
+
'{}',
|
|
42
|
+
'[
|
|
43
|
+
{
|
|
44
|
+
"title": "Dashboard",
|
|
45
|
+
"path": "/dashboard",
|
|
46
|
+
"icon": "LayoutDashboard",
|
|
47
|
+
"order": 1
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"title": "CRM",
|
|
51
|
+
"path": "/crm",
|
|
52
|
+
"icon": "Users",
|
|
53
|
+
"order": 2,
|
|
54
|
+
"children": [
|
|
55
|
+
{"title": "Accounts", "path": "/crm/accounts"},
|
|
56
|
+
{"title": "Contacts", "path": "/crm/contacts"},
|
|
57
|
+
{"title": "Deals", "path": "/crm/deals"},
|
|
58
|
+
{"title": "Health", "path": "/crm/health"},
|
|
59
|
+
{"title": "Activity", "path": "/crm/activity"}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"title": "Support",
|
|
64
|
+
"path": "/support",
|
|
65
|
+
"icon": "Headphones",
|
|
66
|
+
"order": 3
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"title": "Community",
|
|
70
|
+
"path": "/community",
|
|
71
|
+
"icon": "Users",
|
|
72
|
+
"order": 4
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"title": "Knowledge Base",
|
|
76
|
+
"path": "/kb",
|
|
77
|
+
"icon": "BookOpen",
|
|
78
|
+
"order": 5
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"title": "Courses",
|
|
82
|
+
"path": "/courses",
|
|
83
|
+
"icon": "GraduationCap",
|
|
84
|
+
"order": 6
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"title": "Intelligence",
|
|
88
|
+
"path": "/intelligence",
|
|
89
|
+
"icon": "Brain",
|
|
90
|
+
"order": 7
|
|
91
|
+
}
|
|
92
|
+
]',
|
|
93
|
+
'/cortex', -- Change to '/' for root serving
|
|
94
|
+
'custom',
|
|
95
|
+
now()
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2. App Installation
|
|
100
|
+
|
|
101
|
+
Install Cortex for your account:
|
|
102
|
+
|
|
103
|
+
```sql
|
|
104
|
+
INSERT INTO public.app_installations (
|
|
105
|
+
account_id,
|
|
106
|
+
app_slug,
|
|
107
|
+
is_enabled
|
|
108
|
+
) VALUES (
|
|
109
|
+
'your-account-id', -- Replace with your account ID
|
|
110
|
+
'cortex',
|
|
111
|
+
true
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3. Required Roles
|
|
116
|
+
|
|
117
|
+
Cortex requires users to have the `support` role. Create this role if it doesn't exist:
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
INSERT INTO public.roles (
|
|
121
|
+
slug,
|
|
122
|
+
name,
|
|
123
|
+
description,
|
|
124
|
+
is_system,
|
|
125
|
+
is_active
|
|
126
|
+
) VALUES (
|
|
127
|
+
'support',
|
|
128
|
+
'Support Agent',
|
|
129
|
+
'Can access Cortex support features',
|
|
130
|
+
false,
|
|
131
|
+
true
|
|
132
|
+
) ON CONFLICT (slug) DO NOTHING;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Assign the support role to users who need Cortex access:
|
|
136
|
+
|
|
137
|
+
```sql
|
|
138
|
+
INSERT INTO public.people (
|
|
139
|
+
account_id,
|
|
140
|
+
user_id,
|
|
141
|
+
role_id,
|
|
142
|
+
is_active
|
|
143
|
+
) VALUES (
|
|
144
|
+
'your-account-id',
|
|
145
|
+
'user-id',
|
|
146
|
+
(SELECT id FROM public.roles WHERE slug = 'support'),
|
|
147
|
+
true
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Manifest Configuration
|
|
152
|
+
|
|
153
|
+
The `manifest.json` file controls app behavior and routing:
|
|
154
|
+
|
|
155
|
+
### Key Settings
|
|
156
|
+
|
|
157
|
+
- **`required_roles`**: `["support"]` - Users must have support role
|
|
158
|
+
- **`routes`**: Define all available routes in the app
|
|
159
|
+
- **`nav_items`**: Navigation structure and icons
|
|
160
|
+
- **`route_prefix`**: Where the app is served from
|
|
161
|
+
|
|
162
|
+
### Route Prefix Configuration
|
|
163
|
+
|
|
164
|
+
#### Subdirectory Serving (Default)
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"route_prefix": "/cortex"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
- App accessible at: `http://domain.com/cortex`
|
|
171
|
+
- Safe for multi-app deployments
|
|
172
|
+
- Default configuration
|
|
173
|
+
|
|
174
|
+
#### Root Serving
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"route_prefix": "/"
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
- App accessible at: `http://domain.com/`
|
|
181
|
+
- Use for single-app deployments
|
|
182
|
+
- Requires updating database `route_prefix` field
|
|
183
|
+
|
|
184
|
+
### Prefix-Aware Routing
|
|
185
|
+
|
|
186
|
+
Cortex uses prefix-aware routing that automatically adapts to the `route_prefix`:
|
|
187
|
+
|
|
188
|
+
- Navigation links automatically include the base path
|
|
189
|
+
- Route definitions work regardless of serving location
|
|
190
|
+
- Breadcrumbs and redirects are prefix-aware
|
|
191
|
+
|
|
192
|
+
No code changes needed when switching between subdirectory and root serving.
|
|
193
|
+
|
|
194
|
+
## Deployment Options
|
|
195
|
+
|
|
196
|
+
### Option 1: Subdirectory Deployment (Recommended)
|
|
197
|
+
|
|
198
|
+
1. Set `route_prefix: "/cortex"` in database
|
|
199
|
+
2. App available at `/cortex/*` URLs
|
|
200
|
+
3. Multiple apps can coexist safely
|
|
201
|
+
|
|
202
|
+
### Option 2: Root Deployment
|
|
203
|
+
|
|
204
|
+
1. Set `route_prefix: "/"` in database
|
|
205
|
+
2. App available at root URLs (`/`, `/dashboard`, etc.)
|
|
206
|
+
3. Use for dedicated Cortex deployments
|
|
207
|
+
|
|
208
|
+
## Verification
|
|
209
|
+
|
|
210
|
+
Test the configuration:
|
|
211
|
+
|
|
212
|
+
1. Check `/api/apps?action=list` returns Cortex
|
|
213
|
+
2. Navigate to the configured route prefix
|
|
214
|
+
3. Verify navigation works correctly
|
|
215
|
+
4. Confirm all pages load without errors
|
|
216
|
+
|
|
217
|
+
## Troubleshooting
|
|
218
|
+
|
|
219
|
+
- **404 errors**: Check `route_prefix` matches database entry
|
|
220
|
+
- **Access denied**: Verify user has `support` role
|
|
221
|
+
- **Navigation broken**: Ensure prefix-aware routing is enabled
|
|
222
|
+
- **Missing pages**: Check all route components exist and export defaults
|
|
@@ -4,6 +4,7 @@ import { LoadingSpinner } from '@core/components/ui/LoadingSpinner'
|
|
|
4
4
|
import { AppShell } from '@core/components/layout/AppShell'
|
|
5
5
|
import { CortexSidebar } from './components/CortexSidebar'
|
|
6
6
|
import { TooltipProvider } from '@core/components/ui/tooltip'
|
|
7
|
+
import { useCurrentApp } from '@core/contexts/AppContext'
|
|
7
8
|
|
|
8
9
|
const CortexDashboard = lazy(() => import('./pages/CortexDashboard'))
|
|
9
10
|
|
|
@@ -40,13 +41,21 @@ const Fallback = <div className="min-h-[400px] flex items-center justify-center"
|
|
|
40
41
|
|
|
41
42
|
function CortexLayout() {
|
|
42
43
|
const location = useLocation()
|
|
44
|
+
const app = useCurrentApp()
|
|
45
|
+
|
|
46
|
+
// Normalize route_prefix: '/' → '' so paths are /dashboard, /crm/accounts, etc.
|
|
47
|
+
const base = app.route_prefix === '/' ? '' : (app.route_prefix || '')
|
|
48
|
+
const prefixDepth = base.split('/').filter(Boolean).length
|
|
49
|
+
|
|
43
50
|
const segments = location.pathname.split('/').filter(Boolean)
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const appSegments = segments.slice(prefixDepth) // strips the prefix segments
|
|
52
|
+
|
|
53
|
+
const breadcrumbs: { title: string; url?: string }[] = [{ title: 'Cortex', url: `${base}/dashboard` }]
|
|
54
|
+
if (appSegments[0] && appSegments[0] !== 'dashboard') {
|
|
55
|
+
breadcrumbs.push({ title: appSegments[0].charAt(0).toUpperCase() + appSegments[0].slice(1) })
|
|
47
56
|
}
|
|
48
|
-
if (
|
|
49
|
-
breadcrumbs.push({ title:
|
|
57
|
+
if (appSegments[1]) {
|
|
58
|
+
breadcrumbs.push({ title: appSegments[1].charAt(0).toUpperCase() + appSegments[1].slice(1) })
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
return (
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useApi } from '../../../v2-core/src/hooks/useApi'
|
|
3
|
+
import { Button } from '../../../v2-core/src/components/ui/button'
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../v2-core/src/components/ui/card'
|
|
5
|
+
import { Progress } from '../../../v2-core/src/components/ui/progress'
|
|
6
|
+
import { Alert, AlertDescription } from '../../../v2-core/src/components/ui/alert'
|
|
7
|
+
import { Badge } from '../../../v2-core/src/components/ui/badge'
|
|
8
|
+
|
|
9
|
+
interface IngestionResponse {
|
|
10
|
+
success: boolean
|
|
11
|
+
items_created: number
|
|
12
|
+
items_updated: number
|
|
13
|
+
embeddings_generated: number
|
|
14
|
+
errors: string[]
|
|
15
|
+
skipped: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface IngestionStats {
|
|
19
|
+
total: number
|
|
20
|
+
processed: number
|
|
21
|
+
created: number
|
|
22
|
+
updated: number
|
|
23
|
+
errors: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function KBIngestion() {
|
|
27
|
+
const [isIngesting, setIsIngesting] = useState(false)
|
|
28
|
+
const [stats, setStats] = useState<IngestionStats | null>(null)
|
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
|
30
|
+
const [logs, setLogs] = useState<string[]>([])
|
|
31
|
+
const apiFetch = useApi()
|
|
32
|
+
|
|
33
|
+
const addLog = (message: string) => {
|
|
34
|
+
const timestamp = new Date().toLocaleTimeString()
|
|
35
|
+
setLogs(prev => [...prev, `[${timestamp}] ${message}`])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const loadChunks = async (): Promise<any[]> => {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch('/chunks.json')
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error('Failed to load chunks file')
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json()
|
|
45
|
+
return data.chunks || []
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`Could not load chunks: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ingestBatch = async (chunks: any[], batchSize = 10): Promise<IngestionResponse> => {
|
|
52
|
+
const response = await apiFetch('/.netlify/functions/custom_kb-ingestion', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
chunks,
|
|
59
|
+
force_update: false
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
65
|
+
throw new Error(`Batch failed: ${errorData.error || response.statusText}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return await response.json()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const startIngestion = async () => {
|
|
72
|
+
setIsIngesting(true)
|
|
73
|
+
setError(null)
|
|
74
|
+
setStats(null)
|
|
75
|
+
setLogs([])
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
addLog('Loading chunks from file...')
|
|
79
|
+
const chunks = await loadChunks()
|
|
80
|
+
addLog(`Loaded ${chunks.length} chunks`)
|
|
81
|
+
|
|
82
|
+
if (chunks.length === 0) {
|
|
83
|
+
throw new Error('No chunks found to ingest')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const results: IngestionStats = {
|
|
87
|
+
total: chunks.length,
|
|
88
|
+
processed: 0,
|
|
89
|
+
created: 0,
|
|
90
|
+
updated: 0,
|
|
91
|
+
errors: 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const batchSize = 10
|
|
95
|
+
const totalBatches = Math.ceil(chunks.length / batchSize)
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
98
|
+
const batch = chunks.slice(i, i + batchSize)
|
|
99
|
+
const batchNum = Math.floor(i / batchSize) + 1
|
|
100
|
+
|
|
101
|
+
addLog(`Processing batch ${batchNum}/${totalBatches} (${batch.length} chunks)...`)
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const response = await ingestBatch(batch)
|
|
105
|
+
|
|
106
|
+
results.processed += batch.length
|
|
107
|
+
results.created += response.items_created || 0
|
|
108
|
+
results.updated += response.items_updated || 0
|
|
109
|
+
|
|
110
|
+
if (response.errors && response.errors.length > 0) {
|
|
111
|
+
results.errors += response.errors.length
|
|
112
|
+
addLog(`Batch ${batchNum} had ${response.errors.length} errors`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (response.skipped && response.skipped.length > 0) {
|
|
116
|
+
addLog(`Batch ${batchNum} skipped ${response.skipped.length} chunks`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addLog(`Batch ${batchNum} complete: ${response.items_created || 0} created, ${response.items_updated || 0} updated`)
|
|
120
|
+
|
|
121
|
+
// Update progress
|
|
122
|
+
setStats({ ...results })
|
|
123
|
+
|
|
124
|
+
// Small delay to prevent overwhelming the server
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
126
|
+
|
|
127
|
+
} catch (batchError) {
|
|
128
|
+
results.errors += batch.length
|
|
129
|
+
addLog(`Batch ${batchNum} failed: ${batchError instanceof Error ? batchError.message : 'Unknown error'}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
addLog(`Ingestion complete! Created: ${results.created}, Updated: ${results.updated}, Errors: ${results.errors}`)
|
|
134
|
+
setStats(results)
|
|
135
|
+
|
|
136
|
+
} catch (err) {
|
|
137
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
|
138
|
+
setError(errorMessage)
|
|
139
|
+
addLog(`ERROR: ${errorMessage}`)
|
|
140
|
+
} finally {
|
|
141
|
+
setIsIngesting(false)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const progress = stats ? (stats.processed / stats.total) * 100 : 0
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
|
149
|
+
<Card>
|
|
150
|
+
<CardHeader>
|
|
151
|
+
<CardTitle>KB Code Chunk Ingestion</CardTitle>
|
|
152
|
+
<CardDescription>
|
|
153
|
+
Ingest parsed code chunks from v2-core functions into the Knowledge Base system.
|
|
154
|
+
This creates KB articles and embeddings for each code chunk.
|
|
155
|
+
</CardDescription>
|
|
156
|
+
</CardHeader>
|
|
157
|
+
<CardContent className="space-y-4">
|
|
158
|
+
<div className="flex items-center gap-4">
|
|
159
|
+
<Button
|
|
160
|
+
onClick={startIngestion}
|
|
161
|
+
disabled={isIngesting}
|
|
162
|
+
size="lg"
|
|
163
|
+
>
|
|
164
|
+
{isIngesting ? 'Ingesting...' : 'Start Ingestion'}
|
|
165
|
+
</Button>
|
|
166
|
+
|
|
167
|
+
{stats && (
|
|
168
|
+
<div className="flex gap-2">
|
|
169
|
+
<Badge variant="outline">
|
|
170
|
+
Total: {stats.total}
|
|
171
|
+
</Badge>
|
|
172
|
+
<Badge variant="outline">
|
|
173
|
+
Created: {stats.created}
|
|
174
|
+
</Badge>
|
|
175
|
+
<Badge variant="outline">
|
|
176
|
+
Updated: {stats.updated}
|
|
177
|
+
</Badge>
|
|
178
|
+
{stats.errors > 0 && (
|
|
179
|
+
<Badge variant="destructive">
|
|
180
|
+
Errors: {stats.errors}
|
|
181
|
+
</Badge>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{isIngesting && (
|
|
188
|
+
<div className="space-y-2">
|
|
189
|
+
<div className="flex justify-between text-sm">
|
|
190
|
+
<span>Progress</span>
|
|
191
|
+
<span>{stats ? `${stats.processed}/${stats.total}` : 'Starting...'}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<Progress value={progress} className="w-full" />
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{error && (
|
|
198
|
+
<Alert variant="destructive">
|
|
199
|
+
<AlertDescription>{error}</AlertDescription>
|
|
200
|
+
</Alert>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{logs.length > 0 && (
|
|
204
|
+
<div className="space-y-2">
|
|
205
|
+
<h4 className="font-medium">Activity Log</h4>
|
|
206
|
+
<div className="bg-gray-50 dark:bg-gray-900 p-4 rounded-lg max-h-64 overflow-y-auto">
|
|
207
|
+
<pre className="text-xs font-mono whitespace-pre-wrap">
|
|
208
|
+
{logs.join('\n')}
|
|
209
|
+
</pre>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</CardContent>
|
|
214
|
+
</Card>
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"required_roles": ["support"],
|
|
7
7
|
"routes": [
|
|
8
|
-
"/cortex",
|
|
9
8
|
"/cortex/dashboard",
|
|
10
9
|
"/cortex/crm",
|
|
11
10
|
"/cortex/crm/accounts",
|
|
@@ -75,6 +74,18 @@
|
|
|
75
74
|
"order": 7
|
|
76
75
|
}
|
|
77
76
|
],
|
|
77
|
+
"changelog": [
|
|
78
|
+
{
|
|
79
|
+
"version": "1.0.0",
|
|
80
|
+
"notes": [
|
|
81
|
+
"Root-relative nav paths for subdomain deployment model",
|
|
82
|
+
"Prefix-aware sidebar navigation via useCurrentApp()",
|
|
83
|
+
"Prefix-aware breadcrumbs with Navigate index redirect",
|
|
84
|
+
"FilterTab replaces FunnelTab (lucide Filter icon)",
|
|
85
|
+
"Initial release"
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
],
|
|
78
89
|
"features": ["crm", "support", "community", "kb", "courses", "intelligence"],
|
|
79
90
|
"dependencies": ["items", "accounts", "pipelines", "integrations"],
|
|
80
91
|
"entry_point": "./index.tsx",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spine-framework-cortex",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Cortex — AI-powered support, CRM, and knowledge base app for Spine Framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/spine-framework/cortex",
|
|
10
|
+
"directory": "custom/apps/cortex"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"spine-framework": ">=0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.tsx",
|
|
17
|
+
"manifest.json",
|
|
18
|
+
"seed/",
|
|
19
|
+
"pages/",
|
|
20
|
+
"components/",
|
|
21
|
+
"functions/",
|
|
22
|
+
"lib/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE.md"
|
|
25
|
+
],
|
|
26
|
+
"spine": {
|
|
27
|
+
"type": "app",
|
|
28
|
+
"slug": "cortex",
|
|
29
|
+
"manifestPath": "manifest.json"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -7,7 +7,7 @@ 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
9
|
import { Separator } from '@core/components/ui/separator'
|
|
10
|
-
import { ArrowLeft, User, Ticket, Handshake, Activity, Heart,
|
|
10
|
+
import { ArrowLeft, User, Ticket, Handshake, Activity, Heart, Filter, TrendingUp, Target, Clock } from 'lucide-react'
|
|
11
11
|
|
|
12
12
|
interface Account {
|
|
13
13
|
id: string
|
|
@@ -140,7 +140,7 @@ function ActivityTab({ accountId }: { accountId: string }) {
|
|
|
140
140
|
)
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
function
|
|
143
|
+
function FilterTab({ account }: { account: Account }) {
|
|
144
144
|
const stage = account.data?.lifecycle_stage
|
|
145
145
|
const score = account.data?.lead_score ?? 0
|
|
146
146
|
const temp = account.data?.temperature
|
|
@@ -217,7 +217,7 @@ function FunnelTab({ account }: { account: Account }) {
|
|
|
217
217
|
{queue?.pending_opportunity_id && (
|
|
218
218
|
<div className="border rounded-lg p-4 bg-yellow-50 border-yellow-200">
|
|
219
219
|
<div className="flex items-center gap-2">
|
|
220
|
-
<
|
|
220
|
+
<Filter className="h-4 w-4 text-yellow-600" />
|
|
221
221
|
<span className="font-medium text-sm">Opportunity in Queue</span>
|
|
222
222
|
</div>
|
|
223
223
|
<p className="text-xs text-muted-foreground mt-1">
|
|
@@ -317,7 +317,7 @@ function FunnelTab({ account }: { account: Account }) {
|
|
|
317
317
|
{/* Empty State */}
|
|
318
318
|
{!stage && !score && !temp && !ratings && !queue?.pending_opportunity_id && (
|
|
319
319
|
<div className="text-center py-12 text-muted-foreground">
|
|
320
|
-
<
|
|
320
|
+
<Filter className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
|
321
321
|
<p className="text-sm">No funnel data yet.</p>
|
|
322
322
|
<p className="text-xs mt-1">Signals will appear here as they are processed.</p>
|
|
323
323
|
</div>
|
|
@@ -364,7 +364,7 @@ export default function AccountDetailPage() {
|
|
|
364
364
|
<TabsList className="h-9 bg-transparent p-0 gap-4">
|
|
365
365
|
{[
|
|
366
366
|
{ value: 'people', label: 'People', icon: User },
|
|
367
|
-
{ value: 'funnel', label: 'Funnel', icon:
|
|
367
|
+
{ value: 'funnel', label: 'Funnel', icon: Filter },
|
|
368
368
|
{ value: 'tickets', label: 'Tickets', icon: Ticket },
|
|
369
369
|
{ value: 'deals', label: 'Deals', icon: Handshake },
|
|
370
370
|
{ value: 'health', label: 'Health', icon: Heart },
|
|
@@ -381,7 +381,7 @@ export default function AccountDetailPage() {
|
|
|
381
381
|
|
|
382
382
|
<ScrollArea className="flex-1">
|
|
383
383
|
<TabsContent value="people" className="mt-0"><PeopleTab accountId={id!} /></TabsContent>
|
|
384
|
-
<TabsContent value="funnel" className="mt-0"><
|
|
384
|
+
<TabsContent value="funnel" className="mt-0"><FilterTab account={account} /></TabsContent>
|
|
385
385
|
<TabsContent value="tickets" className="mt-0"><ItemsTab accountId={id!} typeSlug="support_ticket" emptyText="No tickets." /></TabsContent>
|
|
386
386
|
<TabsContent value="deals" className="mt-0"><ItemsTab accountId={id!} typeSlug="deal" emptyText="No deals." /></TabsContent>
|
|
387
387
|
<TabsContent value="health" className="mt-0"><ItemsTab accountId={id!} typeSlug="csm_health" emptyText="No health records." /></TabsContent>
|