spine-framework-cortex 0.2.24 → 0.2.26
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 +193 -0
- package/README.md +46 -0
- package/functions/custom_case_analysis.ts +1 -1
- package/functions/custom_cortex-handler.ts +35 -0
- package/functions/custom_kb-chunker-test.ts +364 -0
- package/functions/custom_kb-ingestion.ts +447 -0
- package/functions/custom_support-redaction.ts +1 -1
- package/functions/custom_support-solution.ts +1 -1
- package/functions/custom_tag_management.ts +314 -0
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/pages/community/CommunityPage.tsx +29 -10
- package/pages/courses/CoursesPage.tsx +2 -14
- package/pages/kb/KBEditorPage.tsx +1 -10
- package/pages/support/RedactionReview.tsx +1 -12
- package/pages/support/TicketDetailPage.tsx +34 -46
- package/seed/types.json +2 -2
- package/hooks/useTypeRegistry.ts +0 -74
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useAppPath } from '@core/hooks/useAppPath'
|
|
4
4
|
import { apiFetch } from '@core/lib/api'
|
|
@@ -78,14 +78,16 @@ const STATUS_BADGE: Record<string, string> = {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Merged Thread Panel - combines internal and external messages
|
|
81
|
-
function MergedThreadPanel({
|
|
82
|
-
ticketId,
|
|
83
|
-
externalThread,
|
|
84
|
-
internalThread
|
|
85
|
-
|
|
81
|
+
function MergedThreadPanel({
|
|
82
|
+
ticketId,
|
|
83
|
+
externalThread,
|
|
84
|
+
internalThread,
|
|
85
|
+
refreshThreads,
|
|
86
|
+
}: {
|
|
86
87
|
ticketId: string
|
|
87
88
|
externalThread: Thread | null
|
|
88
89
|
internalThread: Thread | null
|
|
90
|
+
refreshThreads: () => Promise<void>
|
|
89
91
|
}) {
|
|
90
92
|
const [messages, setMessages] = useState<Message[]>([])
|
|
91
93
|
const [loading, setLoading] = useState(true)
|
|
@@ -132,29 +134,19 @@ function MergedThreadPanel({
|
|
|
132
134
|
try {
|
|
133
135
|
// Create internal thread if it doesn't exist
|
|
134
136
|
if (!targetThread && replyType === 'internal') {
|
|
135
|
-
const threadTypesRes = await apiFetch('/api/types?kind=thread&limit=1').then(r => r.json())
|
|
136
|
-
const threadTypes = Array.isArray(threadTypesRes?.data) ? threadTypesRes.data : Array.isArray(threadTypesRes) ? threadTypesRes : []
|
|
137
|
-
if (!threadTypes.length) {
|
|
138
|
-
console.error('No thread type found')
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
137
|
const threadRes = await apiFetch('/api/admin-data?action=create&entity=threads', {
|
|
143
138
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
144
139
|
body: JSON.stringify({
|
|
145
140
|
target_type: 'item',
|
|
146
141
|
target_id: ticketId,
|
|
147
142
|
visibility: 'internal',
|
|
148
|
-
|
|
143
|
+
type_slug: 'support_thread'
|
|
149
144
|
}),
|
|
150
145
|
})
|
|
151
146
|
const newThread = await threadRes.json()
|
|
152
147
|
if (newThread?.id) {
|
|
153
148
|
targetThread = newThread
|
|
154
|
-
|
|
155
|
-
const thr = await apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${ticketId}&limit=10`).then(r => r.json())
|
|
156
|
-
const threadList = thr?.data ?? thr
|
|
157
|
-
setThreads(Array.isArray(threadList) ? threadList : [])
|
|
149
|
+
await refreshThreads()
|
|
158
150
|
}
|
|
159
151
|
}
|
|
160
152
|
|
|
@@ -163,14 +155,6 @@ function MergedThreadPanel({
|
|
|
163
155
|
return
|
|
164
156
|
}
|
|
165
157
|
|
|
166
|
-
// Resolve message type_id
|
|
167
|
-
const typesRes = await apiFetch('/api/types?kind=message&limit=1').then(r => r.json())
|
|
168
|
-
const types = Array.isArray(typesRes?.data) ? typesRes.data : Array.isArray(typesRes) ? typesRes : []
|
|
169
|
-
if (!types.length) {
|
|
170
|
-
console.error('No message type found')
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
|
|
174
158
|
// Get current message count for sequencing
|
|
175
159
|
const currentMsgs = await apiFetch(`/api/admin-data?action=list&entity=messages&thread_id=${targetThread.id}&limit=1000`).then(r => r.json())
|
|
176
160
|
const msgCount = Array.isArray(currentMsgs?.data) ? currentMsgs.data.length : Array.isArray(currentMsgs) ? currentMsgs.length : 0
|
|
@@ -178,11 +162,11 @@ function MergedThreadPanel({
|
|
|
178
162
|
|
|
179
163
|
const res = await apiFetch('/api/admin-data?action=create&entity=messages', {
|
|
180
164
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
181
|
-
body: JSON.stringify({
|
|
182
|
-
thread_id: targetThread.id,
|
|
183
|
-
content: reply.trim(),
|
|
184
|
-
direction: 'outbound',
|
|
185
|
-
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
thread_id: targetThread.id,
|
|
167
|
+
content: reply.trim(),
|
|
168
|
+
direction: 'outbound',
|
|
169
|
+
type_slug: 'support_reply',
|
|
186
170
|
sequence: nextSeq,
|
|
187
171
|
visibility: replyType
|
|
188
172
|
}),
|
|
@@ -740,18 +724,25 @@ export default function TicketDetailPage() {
|
|
|
740
724
|
const [watchLoading, setWatchLoading] = useState(false)
|
|
741
725
|
const [analysisLoading, setAnalysisLoading] = useState(false)
|
|
742
726
|
|
|
743
|
-
|
|
727
|
+
const loadTicketAndThreads = useCallback(async () => {
|
|
744
728
|
if (!id) return
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
729
|
+
setLoading(true)
|
|
730
|
+
try {
|
|
731
|
+
const [ir, thr] = await Promise.all([
|
|
732
|
+
apiFetch(`/api/admin-data?action=get&entity=items&id=${id}`).then(r => r.json()),
|
|
733
|
+
apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${id}&type_slug=support_thread&limit=10`).then(r => r.json()),
|
|
734
|
+
])
|
|
749
735
|
setTicket(ir?.data ?? ir ?? null)
|
|
750
736
|
const threadList = thr?.data ?? thr
|
|
751
737
|
setThreads(Array.isArray(threadList) ? threadList : [])
|
|
752
|
-
}
|
|
738
|
+
} catch {
|
|
739
|
+
} finally {
|
|
740
|
+
setLoading(false)
|
|
741
|
+
}
|
|
753
742
|
}, [id])
|
|
754
743
|
|
|
744
|
+
useEffect(() => { loadTicketAndThreads() }, [loadTicketAndThreads])
|
|
745
|
+
|
|
755
746
|
// Check if current user is watching this ticket
|
|
756
747
|
useEffect(() => {
|
|
757
748
|
if (!id || !user?.id) return
|
|
@@ -776,16 +767,12 @@ export default function TicketDetailPage() {
|
|
|
776
767
|
setIsWatching(false)
|
|
777
768
|
setWatcherId(null)
|
|
778
769
|
} else {
|
|
779
|
-
// Resolve watcher type_id via types API
|
|
780
|
-
const typeRes = await apiFetch('/api/types?kind=watcher&limit=1').then(r => r.json())
|
|
781
|
-
const types = Array.isArray(typeRes?.data) ? typeRes.data : Array.isArray(typeRes) ? typeRes : []
|
|
782
|
-
if (!types.length) { console.error('No watcher type found'); return }
|
|
783
770
|
const res = await apiFetch('/api/admin-data?action=create&entity=watchers', {
|
|
784
771
|
method: 'POST',
|
|
785
772
|
headers: { 'Content-Type': 'application/json' },
|
|
786
773
|
body: JSON.stringify({
|
|
787
774
|
entity: 'watchers',
|
|
788
|
-
|
|
775
|
+
type_slug: 'watcher',
|
|
789
776
|
target_type: 'item',
|
|
790
777
|
target_id: id,
|
|
791
778
|
person_id: user.id,
|
|
@@ -887,10 +874,11 @@ export default function TicketDetailPage() {
|
|
|
887
874
|
<div className="flex flex-1 min-h-0">
|
|
888
875
|
{/* Main Conversation Panel */}
|
|
889
876
|
<div className="flex-1 min-w-0 border-r border-border">
|
|
890
|
-
<MergedThreadPanel
|
|
891
|
-
ticketId={id || ''}
|
|
892
|
-
externalThread={externalThread}
|
|
893
|
-
internalThread={internalThread}
|
|
877
|
+
<MergedThreadPanel
|
|
878
|
+
ticketId={id || ''}
|
|
879
|
+
externalThread={externalThread}
|
|
880
|
+
internalThread={internalThread}
|
|
881
|
+
refreshThreads={loadTicketAndThreads}
|
|
894
882
|
/>
|
|
895
883
|
</div>
|
|
896
884
|
|
package/seed/types.json
CHANGED
|
@@ -1868,7 +1868,7 @@
|
|
|
1868
1868
|
"kind": "item",
|
|
1869
1869
|
"slug": "course_lesson",
|
|
1870
1870
|
"name": "Course Lesson",
|
|
1871
|
-
"description": "Learning content unit
|
|
1871
|
+
"description": "Learning content unit \u2014 part of a course sequence",
|
|
1872
1872
|
"icon": "graduation-cap",
|
|
1873
1873
|
"color": "#F97316",
|
|
1874
1874
|
"ownership": "tenant",
|
|
@@ -1986,4 +1986,4 @@
|
|
|
1986
1986
|
},
|
|
1987
1987
|
"validation_schema": {}
|
|
1988
1988
|
}
|
|
1989
|
-
]
|
|
1989
|
+
]
|
package/hooks/useTypeRegistry.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|