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
@@ -1,19 +1,17 @@
1
1
  /**
2
2
  * Cortex Webhook Handler
3
3
  *
4
- * Convention: custom_*.ts files are assembled into /functions/
5
- * and loaded by integration-routes via: import('./custom_cortex-handler')
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
- export default async function cortexHandler(
11
- data: Record<string, any>,
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: { id: string; type: string; accountId: string }
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
- return data['test-text']
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 { Link, useNavigate, useLocation } from "react-router-dom"
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
- Flame,
25
- ScanSearch,
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(`${base}/dashboard`)}>
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
- <Link to={item.url}>
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
- </Link>
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
- <Link to={item.url}>
117
+ <a href={item.url}>
144
118
  <item.icon className="h-4 w-4" />
145
119
  <span>{item.title}</span>
146
- </Link>
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
- // Ops / Revenue
41
- const CommandCenterPage = lazy(() => import('./pages/ops/CommandCenterPage'))
42
- const AuditFunnelPage = lazy(() => import('./pages/ops/AuditFunnelPage'))
43
- const InstallFunnelPage = lazy(() => import('./pages/ops/InstallFunnelPage'))
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 appSegments = segments.slice(prefixDepth) // strips the prefix segments
57
-
58
- const breadcrumbs: { title: string; url?: string }[] = [{ title: 'Cortex', url: `${base}/dashboard` }]
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 (appSegments[1]) {
63
- breadcrumbs.push({ title: appSegments[1].charAt(0).toUpperCase() + appSegments[1].slice(1) })
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
- {/* Ops / Revenue */}
105
- <Route path="ops/command-center" element={<CommandCenterPage />} />
106
- <Route path="ops/audit-funnel" element={<AuditFunnelPage />} />
107
- <Route path="ops/install-funnel" element={<InstallFunnelPage />} />
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.18",
3
+ "version": "0.2.0",
4
+ "private": false,
4
5
  "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
- "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
- "functions/",
26
- "lib/",
27
- "README.md",
28
- "LICENSE.md",
29
- "CHANGELOG.md"
18
+ "api/",
19
+ "hooks/",
20
+ "README.md"
30
21
  ],
31
22
  "spine": {
32
- "type": "app",
33
- "slug": "cortex",
34
- "manifestPath": "manifest.json"
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: messageTypeId,
86
+ type_id: messageTypeIdRef.current,
77
87
  thread_id: currentThread.id,
78
88
  content: newMessage.trim(),
79
89
  direction: 'inbound',