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.
- package/{functions/custom_cortex-handler.ts → api/cortex-handler.ts} +9 -9
- package/components/CliInstancesCard.tsx +144 -0
- package/components/CortexSidebar.tsx +27 -53
- package/hooks/useTypeRegistry.ts +74 -0
- package/index.tsx +13 -24
- package/manifest.json +1 -13
- package/package.json +11 -20
- package/pages/courses/CoursesPage.tsx +14 -4
- package/pages/crm/AccountDetailPage.tsx +149 -194
- package/pages/crm/ContactsPage.tsx +7 -7
- package/pages/intelligence/IntelligencePage.tsx +24 -31
- package/pages/kb/KBEditorPage.tsx +9 -2
- package/pages/operations/AuditFunnelPage.tsx +378 -0
- package/pages/operations/InstallFunnelPage.tsx +410 -0
- package/pages/operations/OperationsDashboard.tsx +275 -0
- package/pages/support/RedactionReview.tsx +11 -2
- package/seed/link-types.json +4 -42
- package/seed/package.json +27 -0
- package/seed/roles.json +1 -1
- package/seed/types.json +2711 -596
- package/CHANGELOG.md +0 -46
- package/LICENSE.md +0 -223
- package/README.md +0 -69
- package/functions/custom_anonymous-sessions.ts +0 -356
- package/functions/custom_case_analysis.ts +0 -507
- package/functions/custom_community-escalation.ts +0 -234
- package/functions/custom_cortex-chunks.ts +0 -52
- package/functions/custom_funnel-scoring.ts +0 -256
- package/functions/custom_funnel-signal.ts +0 -446
- package/functions/custom_funnel-timers.ts +0 -449
- package/functions/custom_kb-chunker-test.ts +0 -364
- package/functions/custom_kb-chunker.ts +0 -576
- package/functions/custom_kb-embeddings.ts +0 -481
- package/functions/custom_kb-ingestion.ts +0 -448
- package/functions/custom_support-triage.ts +0 -649
- package/functions/custom_tag_management.ts +0 -314
- package/functions/webhook-handlers.ts +0 -29
- package/lib/resolveTypeId.ts +0 -16
- package/pages/crm/ContactDetailPage.tsx +0 -184
- package/pages/ops/AuditFunnelPage.tsx +0 -191
- package/pages/ops/CommandCenterPage.tsx +0 -377
- package/pages/ops/InstallFunnelPage.tsx +0 -226
- package/seed/accounts.json +0 -9
- package/seed/integrations.json +0 -24
- package/seed/pipelines.json +0 -59
- package/seed/triggers.json +0 -125
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cortex Webhook Handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* and
|
|
6
|
-
*
|
|
7
|
-
* Receives: (sanitizedData, context, event)
|
|
8
|
-
* Returns: plain text or object
|
|
4
|
+
* Returns "Serenity" for valid requests
|
|
5
|
+
* Receives sanitized data and full context from integration-routes
|
|
9
6
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
|
|
8
|
+
export default async function handler(
|
|
9
|
+
data: any,
|
|
12
10
|
ctx: {
|
|
13
11
|
integrationId: string
|
|
14
12
|
accountId: string
|
|
15
13
|
slug: string
|
|
16
|
-
principal:
|
|
14
|
+
principal: any
|
|
17
15
|
requestId: string
|
|
18
16
|
headers: Record<string, string>
|
|
19
17
|
},
|
|
@@ -25,11 +23,13 @@ export default async function cortexHandler(
|
|
|
25
23
|
queryStringParameters: Record<string, string>
|
|
26
24
|
}
|
|
27
25
|
): Promise<string> {
|
|
26
|
+
// Log the received data for debugging
|
|
28
27
|
console.log(`[${ctx.requestId}] Cortex handler received:`, {
|
|
29
28
|
testText: data['test-text'],
|
|
30
29
|
integrationId: ctx.integrationId,
|
|
31
30
|
accountId: ctx.accountId
|
|
32
31
|
})
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
// Return Serenity as plain text
|
|
34
|
+
return 'Serenity'
|
|
35
35
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card'
|
|
3
|
+
import { Badge } from '@core/components/ui/badge'
|
|
4
|
+
import { Skeleton } from '@core/components/ui/skeleton'
|
|
5
|
+
import { Terminal, CheckCircle } from 'lucide-react'
|
|
6
|
+
import { apiFetch } from '@core/lib/api'
|
|
7
|
+
|
|
8
|
+
interface CliInstance {
|
|
9
|
+
id: string
|
|
10
|
+
serial: string
|
|
11
|
+
app_slug: string
|
|
12
|
+
version: string
|
|
13
|
+
first_seen_at: string
|
|
14
|
+
last_signal_at: string
|
|
15
|
+
claimed_at?: string
|
|
16
|
+
claimed_by_person_id?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CliInstancesCardProps {
|
|
20
|
+
accountId?: string
|
|
21
|
+
personId?: string
|
|
22
|
+
title?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function CliInstancesCard({
|
|
26
|
+
accountId,
|
|
27
|
+
personId,
|
|
28
|
+
title = "CLI Installations"
|
|
29
|
+
}: CliInstancesCardProps) {
|
|
30
|
+
const [instances, setInstances] = useState<CliInstance[]>([])
|
|
31
|
+
const [loading, setLoading] = useState(true)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
async function loadInstances() {
|
|
35
|
+
if (!accountId && !personId) return
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
setLoading(true)
|
|
39
|
+
const endpoint = accountId
|
|
40
|
+
? `/custom_api/account-cli-instances?account_id=${accountId}`
|
|
41
|
+
: `/custom_api/person-cli-instances?person_id=${personId}`
|
|
42
|
+
|
|
43
|
+
const { data } = await apiFetch(endpoint)
|
|
44
|
+
setInstances(data || [])
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error('Failed to load CLI instances:', e)
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
loadInstances()
|
|
53
|
+
}, [accountId, personId])
|
|
54
|
+
|
|
55
|
+
// Truncate serial for display
|
|
56
|
+
const truncateSerial = (serial: string) => {
|
|
57
|
+
if (!serial || serial.length < 20) return serial
|
|
58
|
+
return `${serial.slice(0, 8)}***${serial.slice(-6)}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (loading) {
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader>
|
|
65
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
66
|
+
<Terminal className="w-4 h-4" />
|
|
67
|
+
{title}
|
|
68
|
+
</CardTitle>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent>
|
|
71
|
+
<Skeleton className="h-12" />
|
|
72
|
+
</CardContent>
|
|
73
|
+
</Card>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (instances.length === 0) {
|
|
78
|
+
return (
|
|
79
|
+
<Card>
|
|
80
|
+
<CardHeader>
|
|
81
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
82
|
+
<Terminal className="w-4 h-4" />
|
|
83
|
+
{title}
|
|
84
|
+
</CardTitle>
|
|
85
|
+
</CardHeader>
|
|
86
|
+
<CardContent>
|
|
87
|
+
<p className="text-sm text-muted-foreground">
|
|
88
|
+
No CLI installations claimed yet
|
|
89
|
+
</p>
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Card>
|
|
97
|
+
<CardHeader>
|
|
98
|
+
<div className="flex items-center justify-between">
|
|
99
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
100
|
+
<Terminal className="w-4 h-4" />
|
|
101
|
+
{title}
|
|
102
|
+
</CardTitle>
|
|
103
|
+
<Badge variant="secondary" className="text-xs">
|
|
104
|
+
{instances.length}
|
|
105
|
+
</Badge>
|
|
106
|
+
</div>
|
|
107
|
+
</CardHeader>
|
|
108
|
+
<CardContent>
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
{instances.map((instance) => (
|
|
111
|
+
<div
|
|
112
|
+
key={instance.id}
|
|
113
|
+
className="flex items-center justify-between p-2 rounded-md border hover:bg-muted/50 transition-colors"
|
|
114
|
+
>
|
|
115
|
+
<div className="space-y-1">
|
|
116
|
+
<div className="flex items-center gap-2">
|
|
117
|
+
<code className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">
|
|
118
|
+
{truncateSerial(instance.serial)}
|
|
119
|
+
</code>
|
|
120
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">
|
|
121
|
+
{instance.app_slug}
|
|
122
|
+
</Badge>
|
|
123
|
+
<span className="text-xs text-muted-foreground">
|
|
124
|
+
v{instance.version}
|
|
125
|
+
</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
128
|
+
<span>First seen {new Date(instance.first_seen_at).toLocaleDateString()}</span>
|
|
129
|
+
{instance.last_signal_at && (
|
|
130
|
+
<>
|
|
131
|
+
<span>•</span>
|
|
132
|
+
<span>Active {new Date(instance.last_signal_at).toLocaleDateString()}</span>
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<CheckCircle className="w-4 h-4 text-green-500" />
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</CardContent>
|
|
142
|
+
</Card>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as React from "react"
|
|
11
|
-
import {
|
|
11
|
+
import { useNavigate, useLocation } from "react-router-dom"
|
|
12
12
|
import { useAuth } from "@core/contexts/AuthContext"
|
|
13
|
-
import { useCurrentApp } from "@core/contexts/AppContext"
|
|
14
13
|
import {
|
|
15
14
|
LayoutDashboard,
|
|
16
15
|
Building2,
|
|
@@ -21,8 +20,8 @@ import {
|
|
|
21
20
|
Handshake,
|
|
22
21
|
Activity,
|
|
23
22
|
Heart,
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
BarChart3,
|
|
24
|
+
Search,
|
|
26
25
|
Download,
|
|
27
26
|
} from "lucide-react"
|
|
28
27
|
import {
|
|
@@ -39,36 +38,29 @@ import {
|
|
|
39
38
|
SidebarRail,
|
|
40
39
|
} from "@core/components/ui/sidebar"
|
|
41
40
|
|
|
41
|
+
const crmItems = [
|
|
42
|
+
{ title: "Dashboard", url: "/cortex/dashboard", icon: LayoutDashboard },
|
|
43
|
+
{ title: "Accounts", url: "/cortex/crm/accounts", icon: Building2 },
|
|
44
|
+
{ title: "Contacts", url: "/cortex/crm/contacts", icon: Users },
|
|
45
|
+
{ title: "Deals", url: "/cortex/crm/deals", icon: Handshake },
|
|
46
|
+
{ title: "Health", url: "/cortex/crm/health", icon: Heart },
|
|
47
|
+
{ title: "Activity", url: "/cortex/crm/activity", icon: Activity },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const opsItems = [
|
|
51
|
+
{ title: "Operations Dashboard", url: "/cortex/operations", icon: BarChart3 },
|
|
52
|
+
{ title: "Audit Funnel", url: "/cortex/operations/audit-funnel", icon: Search },
|
|
53
|
+
{ title: "Install Funnel", url: "/cortex/operations/install-funnel", icon: Download },
|
|
54
|
+
{ title: "Support", url: "/cortex/support", icon: Headphones },
|
|
55
|
+
{ title: "Community", url: "/cortex/community", icon: Users },
|
|
56
|
+
{ title: "Knowledge Base", url: "/cortex/kb", icon: BookOpen },
|
|
57
|
+
{ title: "Courses", url: "/cortex/courses", icon: GraduationCap },
|
|
58
|
+
]
|
|
59
|
+
|
|
42
60
|
export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
43
61
|
const navigate = useNavigate()
|
|
44
62
|
const location = useLocation()
|
|
45
63
|
const { user } = useAuth()
|
|
46
|
-
const app = useCurrentApp()
|
|
47
|
-
|
|
48
|
-
// Normalize route_prefix: '/' becomes '' so all paths are /dashboard, /crm/accounts, etc.
|
|
49
|
-
const base = app.route_prefix === '/' ? '' : (app.route_prefix || '')
|
|
50
|
-
|
|
51
|
-
const crmItems = [
|
|
52
|
-
{ title: "Dashboard", url: `${base}/dashboard`, icon: LayoutDashboard },
|
|
53
|
-
{ title: "Accounts", url: `${base}/crm/accounts`, icon: Building2 },
|
|
54
|
-
{ title: "Contacts", url: `${base}/crm/contacts`, icon: Users },
|
|
55
|
-
{ title: "Deals", url: `${base}/crm/deals`, icon: Handshake },
|
|
56
|
-
{ title: "Health", url: `${base}/crm/health`, icon: Heart },
|
|
57
|
-
{ title: "Activity", url: `${base}/crm/activity`, icon: Activity },
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
const revenueItems = [
|
|
61
|
-
{ title: "Command Center", url: `${base}/ops/command-center`, icon: Flame },
|
|
62
|
-
{ title: "Audit Funnel", url: `${base}/ops/audit-funnel`, icon: ScanSearch },
|
|
63
|
-
{ title: "Install Funnel", url: `${base}/ops/install-funnel`, icon: Download },
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
const opsItems = [
|
|
67
|
-
{ title: "Support", url: `${base}/support`, icon: Headphones },
|
|
68
|
-
{ title: "Community", url: `${base}/community`, icon: Users },
|
|
69
|
-
{ title: "Knowledge Base", url: `${base}/kb`, icon: BookOpen },
|
|
70
|
-
{ title: "Courses", url: `${base}/courses`, icon: GraduationCap },
|
|
71
|
-
]
|
|
72
64
|
|
|
73
65
|
const isActive = (url: string) => location.pathname.startsWith(url)
|
|
74
66
|
|
|
@@ -77,7 +69,7 @@ export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>
|
|
|
77
69
|
<SidebarHeader>
|
|
78
70
|
<SidebarMenu>
|
|
79
71
|
<SidebarMenuItem>
|
|
80
|
-
<SidebarMenuButton size="lg" onClick={() => navigate(
|
|
72
|
+
<SidebarMenuButton size="lg" onClick={() => navigate("/cortex/dashboard")}>
|
|
81
73
|
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
|
82
74
|
<span className="text-sm font-bold">Cx</span>
|
|
83
75
|
</div>
|
|
@@ -101,28 +93,10 @@ export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>
|
|
|
101
93
|
asChild
|
|
102
94
|
isActive={isActive(item.url)}
|
|
103
95
|
>
|
|
104
|
-
<
|
|
105
|
-
<item.icon className="h-4 w-4" />
|
|
106
|
-
<span>{item.title}</span>
|
|
107
|
-
</Link>
|
|
108
|
-
</SidebarMenuButton>
|
|
109
|
-
</SidebarMenuItem>
|
|
110
|
-
))}
|
|
111
|
-
</SidebarMenu>
|
|
112
|
-
</SidebarGroupContent>
|
|
113
|
-
</SidebarGroup>
|
|
114
|
-
|
|
115
|
-
<SidebarGroup>
|
|
116
|
-
<SidebarGroupLabel>Revenue</SidebarGroupLabel>
|
|
117
|
-
<SidebarGroupContent>
|
|
118
|
-
<SidebarMenu>
|
|
119
|
-
{revenueItems.map((item) => (
|
|
120
|
-
<SidebarMenuItem key={item.title}>
|
|
121
|
-
<SidebarMenuButton asChild isActive={isActive(item.url)}>
|
|
122
|
-
<Link to={item.url}>
|
|
96
|
+
<a href={item.url}>
|
|
123
97
|
<item.icon className="h-4 w-4" />
|
|
124
98
|
<span>{item.title}</span>
|
|
125
|
-
</
|
|
99
|
+
</a>
|
|
126
100
|
</SidebarMenuButton>
|
|
127
101
|
</SidebarMenuItem>
|
|
128
102
|
))}
|
|
@@ -140,10 +114,10 @@ export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>
|
|
|
140
114
|
asChild
|
|
141
115
|
isActive={isActive(item.url)}
|
|
142
116
|
>
|
|
143
|
-
<
|
|
117
|
+
<a href={item.url}>
|
|
144
118
|
<item.icon className="h-4 w-4" />
|
|
145
119
|
<span>{item.title}</span>
|
|
146
|
-
</
|
|
120
|
+
</a>
|
|
147
121
|
</SidebarMenuButton>
|
|
148
122
|
</SidebarMenuItem>
|
|
149
123
|
))}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { supabase } from '@core/lib/supabase'
|
|
3
|
+
|
|
4
|
+
interface TypeRecord {
|
|
5
|
+
id: string
|
|
6
|
+
slug: string
|
|
7
|
+
name: string
|
|
8
|
+
description?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Module-level cache - persists across renders, shared across hooks
|
|
12
|
+
let typeCache: Map<string, TypeRecord> | null = null
|
|
13
|
+
let loadPromise: Promise<Map<string, TypeRecord>> | null = null
|
|
14
|
+
|
|
15
|
+
async function fetchTypes(): Promise<Map<string, TypeRecord>> {
|
|
16
|
+
if (typeCache) return typeCache
|
|
17
|
+
if (loadPromise) return loadPromise
|
|
18
|
+
|
|
19
|
+
loadPromise = (async () => {
|
|
20
|
+
try {
|
|
21
|
+
// Query Supabase directly - types is a config table, not exposed via admin-data
|
|
22
|
+
const { data: types, error } = await supabase
|
|
23
|
+
.from('types')
|
|
24
|
+
.select('id, slug, name, description')
|
|
25
|
+
.eq('is_active', true)
|
|
26
|
+
.limit(100)
|
|
27
|
+
|
|
28
|
+
if (error) throw error
|
|
29
|
+
|
|
30
|
+
typeCache = new Map((types || []).map(t => [t.slug, t]))
|
|
31
|
+
return typeCache
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error('Failed to load types:', e)
|
|
34
|
+
typeCache = new Map() // Empty cache on error
|
|
35
|
+
return typeCache
|
|
36
|
+
} finally {
|
|
37
|
+
loadPromise = null
|
|
38
|
+
}
|
|
39
|
+
})()
|
|
40
|
+
|
|
41
|
+
return loadPromise
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useTypeRegistry() {
|
|
45
|
+
const [types, setTypes] = useState<Map<string, TypeRecord>>(typeCache || new Map())
|
|
46
|
+
const [loading, setLoading] = useState(!typeCache)
|
|
47
|
+
const [error, setError] = useState<string | null>(null)
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (typeCache) return
|
|
51
|
+
|
|
52
|
+
fetchTypes()
|
|
53
|
+
.then(cache => {
|
|
54
|
+
setTypes(cache)
|
|
55
|
+
setLoading(false)
|
|
56
|
+
})
|
|
57
|
+
.catch(e => {
|
|
58
|
+
setError(e.message)
|
|
59
|
+
setLoading(false)
|
|
60
|
+
})
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const getTypeId = (slug: string): string | null => {
|
|
64
|
+
return types.get(slug)?.id || null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { types, loading, error, getTypeId }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Synchronous version for use inside async functions
|
|
71
|
+
export async function getTypeIdAsync(slug: string): Promise<string | null> {
|
|
72
|
+
const cache = await fetchTypes()
|
|
73
|
+
return cache.get(slug)?.id || null
|
|
74
|
+
}
|
package/index.tsx
CHANGED
|
@@ -4,7 +4,6 @@ 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'
|
|
8
7
|
|
|
9
8
|
const CortexDashboard = lazy(() => import('./pages/CortexDashboard'))
|
|
10
9
|
|
|
@@ -12,7 +11,6 @@ const CortexDashboard = lazy(() => import('./pages/CortexDashboard'))
|
|
|
12
11
|
const AccountsPage = lazy(() => import('./pages/crm/AccountsPage'))
|
|
13
12
|
const AccountDetailPage = lazy(() => import('./pages/crm/AccountDetailPage'))
|
|
14
13
|
const ContactsPage = lazy(() => import('./pages/crm/ContactsPage'))
|
|
15
|
-
const ContactDetailPage = lazy(() => import('./pages/crm/ContactDetailPage'))
|
|
16
14
|
const DealsPage = lazy(() => import('./pages/crm/DealsPage'))
|
|
17
15
|
const DealDetailPage = lazy(() => import('./pages/crm/DealDetailPage'))
|
|
18
16
|
const HealthPage = lazy(() => import('./pages/crm/HealthPage'))
|
|
@@ -37,30 +35,22 @@ const CoursesPage = lazy(() => import('./pages/courses/CoursesPage'))
|
|
|
37
35
|
// Intelligence
|
|
38
36
|
const IntelligencePage = lazy(() => import('./pages/intelligence/IntelligencePage'))
|
|
39
37
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const AuditFunnelPage = lazy(() => import('./pages/
|
|
43
|
-
const InstallFunnelPage = lazy(() => import('./pages/
|
|
38
|
+
// Operations
|
|
39
|
+
const OperationsDashboard = lazy(() => import('./pages/operations/OperationsDashboard'))
|
|
40
|
+
const AuditFunnelPage = lazy(() => import('./pages/operations/AuditFunnelPage'))
|
|
41
|
+
const InstallFunnelPage = lazy(() => import('./pages/operations/InstallFunnelPage'))
|
|
44
42
|
|
|
45
43
|
const Fallback = <div className="min-h-[400px] flex items-center justify-center"><LoadingSpinner /></div>
|
|
46
44
|
|
|
47
45
|
function CortexLayout() {
|
|
48
46
|
const location = useLocation()
|
|
49
|
-
const app = useCurrentApp()
|
|
50
|
-
|
|
51
|
-
// Normalize route_prefix: '/' → '' so paths are /dashboard, /crm/accounts, etc.
|
|
52
|
-
const base = app.route_prefix === '/' ? '' : (app.route_prefix || '')
|
|
53
|
-
const prefixDepth = base.split('/').filter(Boolean).length
|
|
54
|
-
|
|
55
47
|
const segments = location.pathname.split('/').filter(Boolean)
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (appSegments[0] && appSegments[0] !== 'dashboard') {
|
|
60
|
-
breadcrumbs.push({ title: appSegments[0].charAt(0).toUpperCase() + appSegments[0].slice(1) })
|
|
48
|
+
const breadcrumbs: { title: string; url?: string }[] = [{ title: 'Cortex', url: '/cortex/dashboard' }]
|
|
49
|
+
if (segments[1] && segments[1] !== 'dashboard') {
|
|
50
|
+
breadcrumbs.push({ title: segments[1].charAt(0).toUpperCase() + segments[1].slice(1) })
|
|
61
51
|
}
|
|
62
|
-
if (
|
|
63
|
-
breadcrumbs.push({ title:
|
|
52
|
+
if (segments[2]) {
|
|
53
|
+
breadcrumbs.push({ title: segments[2].charAt(0).toUpperCase() + segments[2].slice(1) })
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
return (
|
|
@@ -73,7 +63,6 @@ function CortexLayout() {
|
|
|
73
63
|
{/* CRM */}
|
|
74
64
|
<Route path="crm/accounts/:id" element={<AccountDetailPage />} />
|
|
75
65
|
<Route path="crm/accounts" element={<AccountsPage />} />
|
|
76
|
-
<Route path="crm/contacts/:id" element={<ContactDetailPage />} />
|
|
77
66
|
<Route path="crm/contacts" element={<ContactsPage />} />
|
|
78
67
|
<Route path="crm/deals/new" element={<DealDetailPage />} />
|
|
79
68
|
<Route path="crm/deals/:id" element={<DealDetailPage />} />
|
|
@@ -101,10 +90,10 @@ function CortexLayout() {
|
|
|
101
90
|
{/* Intelligence */}
|
|
102
91
|
<Route path="intelligence" element={<IntelligencePage />} />
|
|
103
92
|
|
|
104
|
-
{/*
|
|
105
|
-
<Route path="
|
|
106
|
-
<Route path="
|
|
107
|
-
<Route path="
|
|
93
|
+
{/* Operations */}
|
|
94
|
+
<Route path="operations" element={<OperationsDashboard />} />
|
|
95
|
+
<Route path="operations/audit-funnel" element={<AuditFunnelPage />} />
|
|
96
|
+
<Route path="operations/install-funnel" element={<InstallFunnelPage />} />
|
|
108
97
|
|
|
109
98
|
<Route path="*" element={<Navigate to="dashboard" replace />} />
|
|
110
99
|
</Routes>
|
package/manifest.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"required_roles": ["support"],
|
|
7
7
|
"routes": [
|
|
8
|
+
"/cortex",
|
|
8
9
|
"/cortex/dashboard",
|
|
9
10
|
"/cortex/crm",
|
|
10
11
|
"/cortex/crm/accounts",
|
|
@@ -74,21 +75,8 @@
|
|
|
74
75
|
"order": 7
|
|
75
76
|
}
|
|
76
77
|
],
|
|
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
|
-
],
|
|
89
78
|
"features": ["crm", "support", "community", "kb", "courses", "intelligence"],
|
|
90
79
|
"dependencies": ["items", "accounts", "pipelines", "integrations"],
|
|
91
80
|
"entry_point": "./index.tsx",
|
|
92
|
-
"directories": ["pages", "components", "hooks", "config", "seed", "functions", "migrations", "tests"],
|
|
93
81
|
"sidebar_component": "./components/CortexSidebar.tsx"
|
|
94
82
|
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-framework-cortex",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"private": false,
|
|
4
5
|
"description": "Cortex — AI-powered support, CRM, and knowledge base app for Spine Framework",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "https://github.com/spine-framework/cortex",
|
|
10
|
-
"directory": "custom/apps/cortex"
|
|
11
|
-
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"publish:next": "npm publish --tag next --access public",
|
|
14
|
-
"publish:promote": "node -e \"const v=require('./package.json').version;require('child_process').execSync('npm dist-tag add spine-framework-cortex@'+v+' latest',{stdio:'inherit'})\""
|
|
15
|
-
},
|
|
6
|
+
"keywords": ["spine-framework", "crm", "support", "knowledge-base", "community"],
|
|
7
|
+
"author": "Spine Team",
|
|
8
|
+
"license": "MIT",
|
|
16
9
|
"peerDependencies": {
|
|
17
10
|
"spine-framework": ">=0.1.0"
|
|
18
11
|
},
|
|
@@ -22,15 +15,13 @@
|
|
|
22
15
|
"seed/",
|
|
23
16
|
"pages/",
|
|
24
17
|
"components/",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"README.md"
|
|
28
|
-
"LICENSE.md",
|
|
29
|
-
"CHANGELOG.md"
|
|
18
|
+
"api/",
|
|
19
|
+
"hooks/",
|
|
20
|
+
"README.md"
|
|
30
21
|
],
|
|
31
22
|
"spine": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
23
|
+
"app_slug": "cortex",
|
|
24
|
+
"entry_point": "./index.tsx",
|
|
25
|
+
"manifest": "./manifest.json"
|
|
35
26
|
}
|
|
36
27
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
import { resolveTypeId } from '../../lib/resolveTypeId'
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
3
2
|
import { useNavigate } from 'react-router-dom'
|
|
4
3
|
import { apiFetch } from '@core/lib/api'
|
|
4
|
+
import { getTypeIdAsync } from '../hooks/useTypeRegistry'
|
|
5
5
|
import { Button } from '@core/components/ui/button'
|
|
6
6
|
import { Input } from '@core/components/ui/input'
|
|
7
7
|
import { Badge } from '@core/components/ui/badge'
|
|
@@ -31,6 +31,13 @@ function DiscussionPanel({ lesson }: { lesson: Lesson }) {
|
|
|
31
31
|
const [loading, setLoading] = useState(true)
|
|
32
32
|
const [newMessage, setNewMessage] = useState('')
|
|
33
33
|
const [submitting, setSubmitting] = useState(false)
|
|
34
|
+
const messageTypeIdRef = useRef<string>('')
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
getTypeIdAsync('message').then(id => {
|
|
38
|
+
if (id) messageTypeIdRef.current = id
|
|
39
|
+
})
|
|
40
|
+
}, [])
|
|
34
41
|
|
|
35
42
|
useEffect(() => {
|
|
36
43
|
setLoading(true)
|
|
@@ -46,6 +53,10 @@ function DiscussionPanel({ lesson }: { lesson: Lesson }) {
|
|
|
46
53
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
47
54
|
e.preventDefault()
|
|
48
55
|
if (!newMessage.trim() || submitting) return
|
|
56
|
+
if (!messageTypeIdRef.current) {
|
|
57
|
+
console.error('Message type not loaded')
|
|
58
|
+
return
|
|
59
|
+
}
|
|
49
60
|
|
|
50
61
|
setSubmitting(true)
|
|
51
62
|
try {
|
|
@@ -68,12 +79,11 @@ function DiscussionPanel({ lesson }: { lesson: Lesson }) {
|
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
// Create message
|
|
71
|
-
const messageTypeId = await resolveTypeId('message')
|
|
72
82
|
const messageResponse = await apiFetch('/api/admin-data?action=create&entity=messages', {
|
|
73
83
|
method: 'POST',
|
|
74
84
|
headers: { 'Content-Type': 'application/json' },
|
|
75
85
|
body: JSON.stringify({
|
|
76
|
-
type_id:
|
|
86
|
+
type_id: messageTypeIdRef.current,
|
|
77
87
|
thread_id: currentThread.id,
|
|
78
88
|
content: newMessage.trim(),
|
|
79
89
|
direction: 'inbound',
|