spine-framework-cortex 0.2.22 → 0.2.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework-cortex",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "private": false,
5
5
  "description": "Cortex — AI-powered support, CRM, and knowledge base app for Spine Framework",
6
6
  "keywords": [
@@ -1,6 +1,7 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { Send, Hash, MessageSquarePlus, AlertCircle } from 'lucide-react'
3
3
  import { apiFetch } from '@core/lib/api'
4
+ import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
4
5
  import { Button } from '@core/components/ui/button'
5
6
  import { Input } from '@core/components/ui/input'
6
7
  import { Badge } from '@core/components/ui/badge'
@@ -20,6 +21,11 @@ function ThreadPane({ post }: { post: Post }) {
20
21
  const [loading, setLoading] = useState(true)
21
22
  const [replyText, setReplyText] = useState('')
22
23
  const [sending, setSending] = useState(false)
24
+ const [messageTypeId, setMessageTypeId] = useState<string | null>(null)
25
+
26
+ useEffect(() => {
27
+ getTypeIdAsync('message').then(id => setMessageTypeId(id))
28
+ }, [])
23
29
 
24
30
  useEffect(() => {
25
31
  setLoading(true)
@@ -33,14 +39,15 @@ function ThreadPane({ post }: { post: Post }) {
33
39
  }, [post.id])
34
40
 
35
41
  const handleSend = async () => {
36
- if (!replyText.trim() || !thread) return
42
+ if (!replyText.trim() || !thread || !messageTypeId) return
37
43
  setSending(true)
38
44
  try {
39
45
  const res = await apiFetch('/api/admin-data?action=create&entity=messages', {
40
46
  method: 'POST', headers: { 'Content-Type': 'application/json' },
41
- body: JSON.stringify({ thread_id: thread.id, content: replyText.trim(), direction: 'outbound', entity: 'messages', type_slug: 'message' }),
47
+ body: JSON.stringify({ thread_id: thread.id, type_id: messageTypeId, content: replyText.trim(), direction: 'outbound', entity: 'messages' }),
42
48
  })
43
- const msg = await res.json()
49
+ const raw = await res.json()
50
+ const msg = raw?.data ?? raw
44
51
  if (msg?.id) setMessages(prev => [...prev, msg])
45
52
  setReplyText('')
46
53
  } finally { setSending(false) }
@@ -66,7 +73,7 @@ function ThreadPane({ post }: { post: Post }) {
66
73
  <div className="border-t p-3 flex gap-2 shrink-0">
67
74
  <Input placeholder="Reply as agent…" value={replyText} onChange={e => setReplyText(e.target.value)}
68
75
  onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()} className="flex-1" />
69
- <Button size="icon" onClick={handleSend} disabled={sending || !replyText.trim() || !thread}><Send size={14} /></Button>
76
+ <Button size="icon" onClick={handleSend} disabled={sending || !replyText.trim() || !thread || !messageTypeId}><Send size={14} /></Button>
70
77
  </div>
71
78
  </div>
72
79
  )
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { useParams, useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
  import { Tabs, TabsList, TabsTrigger, TabsContent } from '@core/components/ui/tabs'
5
6
  import { Badge } from '@core/components/ui/badge'
@@ -81,6 +82,7 @@ function PeopleTab({ accountId }: { accountId: string }) {
81
82
 
82
83
  function ItemsTab({ accountId, typeSlug, emptyText }: { accountId: string; typeSlug: string; emptyText: string }) {
83
84
  const navigate = useNavigate()
85
+ const appPath = useAppPath()
84
86
  const [items, setItems] = useState<Item[]>([])
85
87
  const [loading, setLoading] = useState(true)
86
88
  useEffect(() => {
@@ -94,7 +96,7 @@ function ItemsTab({ accountId, typeSlug, emptyText }: { accountId: string; typeS
94
96
  {items.map(item => (
95
97
  <div key={item.id}
96
98
  className="flex items-center gap-3 px-4 py-3 hover:bg-accent/50 cursor-pointer transition-colors"
97
- onClick={() => typeSlug === 'support_ticket' ? navigate(`/cortex/support/${item.id}`) : typeSlug === 'deal' ? navigate(`/cortex/crm/deals/${item.id}`) : undefined}
99
+ onClick={() => typeSlug === 'support_ticket' ? navigate(appPath(`/support/${item.id}`)) : typeSlug === 'deal' ? navigate(appPath(`/crm/deals/${item.id}`)) : undefined}
98
100
  >
99
101
  <div className="flex-1 min-w-0">
100
102
  <p className="text-sm font-medium truncate">{item.title}</p>
@@ -330,6 +332,7 @@ function FunnelTab({ account }: { account: Account }) {
330
332
  export default function AccountDetailPage() {
331
333
  const { id } = useParams<{ id: string }>()
332
334
  const navigate = useNavigate()
335
+ const appPath = useAppPath()
333
336
  const [account, setAccount] = useState<Account | null>(null)
334
337
  const [loading, setLoading] = useState(true)
335
338
 
@@ -345,7 +348,7 @@ export default function AccountDetailPage() {
345
348
  return (
346
349
  <div className="flex flex-col h-full">
347
350
  <div className="px-6 py-4 border-b border-border shrink-0">
348
- <Button variant="ghost" size="sm" className="mb-2 -ml-2 gap-1 text-muted-foreground" onClick={() => navigate('/cortex/crm/accounts')}>
351
+ <Button variant="ghost" size="sm" className="mb-2 -ml-2 gap-1 text-muted-foreground" onClick={() => navigate(appPath('/crm/accounts'))}>
349
352
  <ArrowLeft className="h-3.5 w-3.5" /> Accounts
350
353
  </Button>
351
354
  <div className="flex items-center gap-3">
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
  import { Input } from '@core/components/ui/input'
5
6
  import { Badge } from '@core/components/ui/badge'
@@ -26,6 +27,7 @@ type Filter = 'all'
26
27
 
27
28
  export default function AccountsPage() {
28
29
  const navigate = useNavigate()
30
+ const appPath = useAppPath()
29
31
  const [accounts, setAccounts] = useState<Account[]>([])
30
32
  const [loading, setLoading] = useState(true)
31
33
  const [search, setSearch] = useState('')
@@ -91,7 +93,7 @@ export default function AccountsPage() {
91
93
  {filtered.map(account => (
92
94
  <tr
93
95
  key={account.id}
94
- onClick={() => navigate(`/cortex/crm/accounts/${account.id}`)}
96
+ onClick={() => navigate(appPath(`/crm/accounts/${account.id}`))}
95
97
  className="hover:bg-accent/50 cursor-pointer transition-colors"
96
98
  >
97
99
  <td className="px-5 py-3 font-medium">
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { useNavigate, useParams } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
 
5
6
  const STAGES = ['prospecting', 'qualification', 'proposal', 'negotiation', 'closed_won', 'closed_lost']
@@ -19,6 +20,7 @@ const EMPTY: DealForm = { title: '', stage: 'prospecting', value: '', close_date
19
20
  export default function DealDetailPage() {
20
21
  const { id } = useParams<{ id: string }>()
21
22
  const navigate = useNavigate()
23
+ const appPath = useAppPath()
22
24
  const isNew = !id || id === 'new'
23
25
 
24
26
  const [form, setForm] = useState<DealForm>(EMPTY)
@@ -82,7 +84,7 @@ export default function DealDetailPage() {
82
84
  const handleDelete = async () => {
83
85
  if (!confirm('Delete this deal?')) return
84
86
  await apiFetch(`/api/admin-data?action=delete&entity=items&id=${id}`, { method: 'POST' })
85
- navigate('/cortex/crm/deals')
87
+ navigate(appPath('/crm/deals'))
86
88
  }
87
89
 
88
90
  const set = (field: keyof DealForm) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
@@ -93,7 +95,7 @@ export default function DealDetailPage() {
93
95
  return (
94
96
  <div className="p-6 max-w-2xl">
95
97
  <div className="flex items-center gap-3 mb-6">
96
- <button onClick={() => navigate('/cortex/crm/deals')} className="text-slate-400 hover:text-slate-700 text-sm">
98
+ <button onClick={() => navigate(appPath('/crm/deals'))} className="text-slate-400 hover:text-slate-700 text-sm">
97
99
  ← Deals
98
100
  </button>
99
101
  <h1 className="text-xl font-bold text-slate-900">{isNew ? 'New Deal' : 'Edit Deal'}</h1>
@@ -174,7 +176,7 @@ export default function DealDetailPage() {
174
176
  </button>
175
177
  ) : <div />}
176
178
  <div className="flex gap-3">
177
- <button onClick={() => navigate('/cortex/crm/deals')} className="px-4 py-2 text-sm text-slate-600 border border-slate-200 rounded-lg hover:bg-slate-50">
179
+ <button onClick={() => navigate(appPath('/crm/deals'))} className="px-4 py-2 text-sm text-slate-600 border border-slate-200 rounded-lg hover:bg-slate-50">
178
180
  Cancel
179
181
  </button>
180
182
  <button
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
 
5
6
  interface Deal {
@@ -40,6 +41,7 @@ function DealCard({ deal, onClick }: { deal: Deal; onClick: () => void }) {
40
41
 
41
42
  export default function DealsPage() {
42
43
  const navigate = useNavigate()
44
+ const appPath = useAppPath()
43
45
  const [deals, setDeals] = useState<Deal[]>([])
44
46
  const [loading, setLoading] = useState(true)
45
47
  const [view, setView] = useState<'kanban' | 'list'>('kanban')
@@ -77,7 +79,7 @@ export default function DealsPage() {
77
79
  </button>
78
80
  </div>
79
81
  <button
80
- onClick={() => navigate('/cortex/crm/deals/new')}
82
+ onClick={() => navigate(appPath('/crm/deals/new'))}
81
83
  className="bg-blue-600 text-white text-sm font-medium px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
82
84
  >
83
85
  + New Deal
@@ -133,7 +135,7 @@ export default function DealsPage() {
133
135
  <tr>
134
136
  <td colSpan={5} className="px-5 py-12 text-center text-slate-400">
135
137
  No deals yet.{' '}
136
- <button onClick={() => navigate('/cortex/crm/deals/new')} className="text-blue-600 hover:underline">
138
+ <button onClick={() => navigate(appPath('/crm/deals/new'))} className="text-blue-600 hover:underline">
137
139
  Create one →
138
140
  </button>
139
141
  </td>
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { useParams, useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
  import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
5
6
  import { RichTextEditor } from '@core/components/ui/RichTextEditor'
@@ -66,6 +67,7 @@ function SidebarSelect({ label, value, onChange, options }: {
66
67
  export default function KBEditorPage() {
67
68
  const { id } = useParams<{ id: string }>()
68
69
  const navigate = useNavigate()
70
+ const appPath = useAppPath()
69
71
  const isNew = !id || id === 'new'
70
72
  const [form, setForm] = useState<ArticleForm>(EMPTY)
71
73
  const [loading, setLoading] = useState(!isNew)
@@ -157,7 +159,7 @@ export default function KBEditorPage() {
157
159
  }
158
160
  }
159
161
 
160
- navigate('/cortex/kb')
162
+ navigate(appPath('/kb'))
161
163
  } catch (e: any) {
162
164
  setError(e.message)
163
165
  } finally {
@@ -184,7 +186,7 @@ export default function KBEditorPage() {
184
186
  {/* Header */}
185
187
  <div className="px-6 py-3 border-b border-border shrink-0 flex items-center justify-between">
186
188
  <div className="flex items-center gap-3">
187
- <Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" onClick={() => navigate('/cortex/kb')}>
189
+ <Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" onClick={() => navigate(appPath('/kb'))}>
188
190
  <ArrowLeft className="h-3.5 w-3.5" /> KB
189
191
  </Button>
190
192
  <h1 className="text-base font-semibold">{isNew ? 'New Article' : 'Edit Article'}</h1>
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useRef, useCallback } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
  import { RichTextEditor } from '@core/components/ui/RichTextEditor'
5
6
  import { Button } from '@core/components/ui/button'
@@ -49,6 +50,7 @@ const PRIORITY_COLORS: Record<string, string> = {
49
50
 
50
51
  export default function KBPage() {
51
52
  const navigate = useNavigate()
53
+ const appPath = useAppPath()
52
54
  const [articles, setArticles] = useState<Article[]>([])
53
55
  const [searchResults, setSearchResults] = useState<Article[] | null>(null)
54
56
  const [loading, setLoading] = useState(true)
@@ -120,7 +122,7 @@ export default function KBPage() {
120
122
  onChange={e => handleSearch(e.target.value)}
121
123
  className="flex-1 h-8"
122
124
  />
123
- <Button size="sm" className="gap-1 shrink-0" onClick={() => navigate('/cortex/kb/new')}>
125
+ <Button size="sm" className="gap-1 shrink-0" onClick={() => navigate(appPath('/kb/new'))}>
124
126
  <Plus size={14} /> New
125
127
  </Button>
126
128
  </div>
@@ -153,7 +155,7 @@ export default function KBPage() {
153
155
  <div className="p-8 text-center text-muted-foreground">
154
156
  <BookOpen className="h-10 w-10 mx-auto mb-3 opacity-30" />
155
157
  <p className="text-sm">{search ? 'No articles match.' : 'No articles yet.'}</p>
156
- <Button size="sm" variant="outline" className="mt-3 gap-1" onClick={() => navigate('/cortex/kb/new')}>
158
+ <Button size="sm" variant="outline" className="mt-3 gap-1" onClick={() => navigate(appPath('/kb/new'))}>
157
159
  <Plus size={13} /> Create article
158
160
  </Button>
159
161
  </div>
@@ -198,7 +200,7 @@ export default function KBPage() {
198
200
  <div className="flex-1 flex flex-col items-center justify-center gap-3 text-muted-foreground">
199
201
  <BookOpen size={32} className="opacity-30" />
200
202
  <p className="text-sm">Select an article to preview</p>
201
- <Button size="sm" variant="outline" onClick={() => navigate('/cortex/kb/new')} className="gap-1">
203
+ <Button size="sm" variant="outline" onClick={() => navigate(appPath('/kb/new'))} className="gap-1">
202
204
  <Plus size={13} /> New article
203
205
  </Button>
204
206
  </div>
@@ -236,7 +238,7 @@ export default function KBPage() {
236
238
  size="sm"
237
239
  variant="outline"
238
240
  className="gap-1 shrink-0"
239
- onClick={() => navigate(`/cortex/kb/${selected.id}/edit`)}
241
+ onClick={() => navigate(appPath(`/kb/${selected.id}/edit`))}
240
242
  >
241
243
  <Edit size={13} /> Edit
242
244
  </Button>
@@ -1,5 +1,6 @@
1
1
  import { useState, useEffect, useCallback } from 'react'
2
2
  import { useParams, useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
  import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
5
6
  import { Button } from '@core/components/ui/button'
@@ -94,6 +95,7 @@ function parseContentWithRedactions(content: string, suggestions: RedactionSugge
94
95
  export default function RedactionReview() {
95
96
  const { id } = useParams<{ id: string }>()
96
97
  const navigate = useNavigate()
98
+ const appPath = useAppPath()
97
99
  const [ticket, setTicket] = useState<Ticket | null>(null)
98
100
  const [analysis, setAnalysis] = useState<RedactionAnalysis | null>(null)
99
101
  const [loading, setLoading] = useState(true)
@@ -311,7 +313,7 @@ export default function RedactionReview() {
311
313
  }),
312
314
  })
313
315
 
314
- navigate(`/cortex/kb/${kbId}`)
316
+ navigate(appPath(`/kb/${kbId}`))
315
317
  }
316
318
  } finally {
317
319
  setProcessing(false)
@@ -334,7 +336,7 @@ export default function RedactionReview() {
334
336
  <AlertTriangle className="h-5 w-5" />
335
337
  <p>Failed to analyze content for redaction. Please try again.</p>
336
338
  </div>
337
- <Button onClick={() => navigate(`/cortex/support/${id}`)}>Back to Ticket</Button>
339
+ <Button onClick={() => navigate(appPath(`/support/${id}`))}>Back to Ticket</Button>
338
340
  </div>
339
341
  )
340
342
  }
@@ -345,7 +347,7 @@ export default function RedactionReview() {
345
347
  <div className="px-6 py-4 border-b border-border shrink-0">
346
348
  <div className="flex items-center justify-between">
347
349
  <div className="flex items-center gap-3">
348
- <Button variant="ghost" size="sm" onClick={() => navigate(`/cortex/support/${id}`)}>
350
+ <Button variant="ghost" size="sm" onClick={() => navigate(appPath(`/support/${id}`))}>
349
351
  <ArrowLeft className="h-4 w-4" />
350
352
  </Button>
351
353
  <div>
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState, useMemo } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
  import { useAuth } from '@core/contexts/AuthContext'
5
6
  import { Input } from '@core/components/ui/input'
@@ -105,6 +106,7 @@ const KANBAN_COLUMNS = [
105
106
 
106
107
  export default function SupportPage() {
107
108
  const navigate = useNavigate()
109
+ const appPath = useAppPath()
108
110
  const { user } = useAuth()
109
111
  const [tickets, setTickets] = useState<Ticket[]>([])
110
112
  const [loading, setLoading] = useState(true)
@@ -220,7 +222,7 @@ export default function SupportPage() {
220
222
  {sorted.map(ticket => (
221
223
  <tr
222
224
  key={ticket.id}
223
- onClick={() => navigate(`/cortex/support/${ticket.id}`)}
225
+ onClick={() => navigate(appPath(`/support/${ticket.id}`))}
224
226
  className="hover:bg-accent/50 cursor-pointer transition-colors"
225
227
  >
226
228
  <td className="px-5 py-3">
@@ -305,7 +307,7 @@ export default function SupportPage() {
305
307
  {tickets?.map(ticket => (
306
308
  <div
307
309
  key={ticket.id}
308
- onClick={() => navigate(`/cortex/support/${ticket.id}`)}
310
+ onClick={() => navigate(appPath(`/support/${ticket.id}`))}
309
311
  className="bg-background border rounded-lg p-3 cursor-pointer hover:shadow-sm transition-shadow"
310
312
  >
311
313
  <div className="flex items-start justify-between gap-2">
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
2
  import { useParams, useNavigate } from 'react-router-dom'
3
+ import { useAppPath } from '@core/hooks/useAppPath'
3
4
  import { apiFetch } from '@core/lib/api'
4
5
  import { Button } from '@core/components/ui/button'
5
6
  import { Input } from '@core/components/ui/input'
@@ -728,6 +729,7 @@ function Separator() {
728
729
  export default function TicketDetailPage() {
729
730
  const { id } = useParams<{ id: string }>()
730
731
  const navigate = useNavigate()
732
+ const appPath = useAppPath()
731
733
  const { user } = useAuth()
732
734
  const [ticket, setTicket] = useState<Ticket | null>(null)
733
735
  const [threads, setThreads] = useState<Thread[]>([])
@@ -837,7 +839,7 @@ export default function TicketDetailPage() {
837
839
  }
838
840
 
839
841
  const handleGenerateKB = () => {
840
- navigate(`/cortex/support/${id}/kb-review`)
842
+ navigate(appPath(`/support/${id}/kb-review`))
841
843
  }
842
844
 
843
845
  const externalThread = threads.find(t => !t.visibility || t.visibility === 'external') ?? null
@@ -850,7 +852,7 @@ export default function TicketDetailPage() {
850
852
  <div className="flex flex-col h-full">
851
853
  <div className="px-6 py-4 border-b border-border shrink-0 flex items-center justify-between">
852
854
  <div className="flex items-center gap-3">
853
- <Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" onClick={() => navigate('/cortex/support')}>
855
+ <Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" onClick={() => navigate(appPath('/support'))}>
854
856
  <ArrowLeft className="h-3.5 w-3.5" /> Support
855
857
  </Button>
856
858
  <div>