spine-framework-cortex 0.1.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.
Files changed (40) hide show
  1. package/components/CortexSidebar.tsx +130 -0
  2. package/functions/custom_anonymous-sessions.ts +356 -0
  3. package/functions/custom_case_analysis.ts +507 -0
  4. package/functions/custom_community-escalation.ts +234 -0
  5. package/functions/custom_cortex-chunks.ts +52 -0
  6. package/functions/custom_cortex-handler.ts +35 -0
  7. package/functions/custom_funnel-scoring.ts +256 -0
  8. package/functions/custom_funnel-signal.ts +678 -0
  9. package/functions/custom_funnel-timers.ts +449 -0
  10. package/functions/custom_kb-chunker-test.ts +364 -0
  11. package/functions/custom_kb-chunker.ts +576 -0
  12. package/functions/custom_kb-embeddings.ts +481 -0
  13. package/functions/custom_kb-ingestion.ts +448 -0
  14. package/functions/custom_support-triage.ts +649 -0
  15. package/functions/custom_tag_management.ts +314 -0
  16. package/index.tsx +103 -0
  17. package/manifest.json +82 -0
  18. package/package.json +29 -0
  19. package/pages/CortexDashboard.tsx +97 -0
  20. package/pages/community/CommunityPage.tsx +159 -0
  21. package/pages/courses/CoursesPage.tsx +231 -0
  22. package/pages/crm/AccountDetailPage.tsx +393 -0
  23. package/pages/crm/AccountsPage.tsx +164 -0
  24. package/pages/crm/ActivityPage.tsx +82 -0
  25. package/pages/crm/ContactDetailPage.tsx +184 -0
  26. package/pages/crm/ContactsPage.tsx +87 -0
  27. package/pages/crm/DealDetailPage.tsx +191 -0
  28. package/pages/crm/DealsPage.tsx +169 -0
  29. package/pages/crm/HealthPage.tsx +109 -0
  30. package/pages/intelligence/IntelligencePage.tsx +314 -0
  31. package/pages/kb/KBEditorPage.tsx +328 -0
  32. package/pages/kb/KBIngestionPage.tsx +409 -0
  33. package/pages/kb/KBPage.tsx +258 -0
  34. package/pages/support/RedactionReview.tsx +562 -0
  35. package/pages/support/SupportPage.tsx +395 -0
  36. package/pages/support/TicketDetailPage.tsx +919 -0
  37. package/seed/accounts.json +9 -0
  38. package/seed/link-types.json +44 -0
  39. package/seed/triggers.json +80 -0
  40. package/seed/types.json +352 -0
@@ -0,0 +1,328 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { resolveTypeId } from '../../lib/resolveTypeId'
3
+ import { useParams, useNavigate } from 'react-router-dom'
4
+ import { apiFetch } from '@core/lib/api'
5
+ import { RichTextEditor } from '@core/components/ui/RichTextEditor'
6
+ import { Button } from '@core/components/ui/button'
7
+ import { Input } from '@core/components/ui/input'
8
+ import { Label } from '@core/components/ui/label'
9
+ import { Skeleton } from '@core/components/ui/skeleton'
10
+ import { ArrowLeft, Save } from 'lucide-react'
11
+
12
+ interface ArticleForm {
13
+ title: string
14
+ description: string
15
+ status: string
16
+ kb_type: string
17
+ priority: string
18
+ category: string
19
+ security_level: string
20
+ tags: string
21
+ audience: string[]
22
+ }
23
+
24
+ const EMPTY: ArticleForm = {
25
+ title: '',
26
+ description: '',
27
+ status: 'draft',
28
+ kb_type: 'article',
29
+ priority: 'medium',
30
+ category: '',
31
+ security_level: 'internal',
32
+ tags: '',
33
+ audience: [],
34
+ }
35
+
36
+ const AUDIENCE_OPTIONS = [
37
+ { value: 'end_user', label: 'End User' },
38
+ { value: 'support_agent', label: 'Support Agent' },
39
+ { value: 'developer', label: 'Developer' },
40
+ { value: 'admin', label: 'Administrator' },
41
+ { value: 'manager', label: 'Manager' },
42
+ { value: 'ai_system', label: 'AI System' },
43
+ { value: 'internal_only', label: 'Internal Only' },
44
+ ]
45
+
46
+ function SidebarSelect({ label, value, onChange, options }: {
47
+ label: string
48
+ value: string
49
+ onChange: (v: string) => void
50
+ options: { value: string; label: string }[]
51
+ }) {
52
+ return (
53
+ <div className="space-y-1.5">
54
+ <Label className="text-xs">{label}</Label>
55
+ <select
56
+ value={value}
57
+ onChange={e => onChange(e.target.value)}
58
+ className="w-full border border-border rounded px-2.5 py-1.5 text-sm bg-background"
59
+ >
60
+ {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
61
+ </select>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ export default function KBEditorPage() {
67
+ const { id } = useParams<{ id: string }>()
68
+ const navigate = useNavigate()
69
+ const isNew = !id || id === 'new'
70
+ const [form, setForm] = useState<ArticleForm>(EMPTY)
71
+ const [loading, setLoading] = useState(!isNew)
72
+ const [saving, setSaving] = useState(false)
73
+ const [error, setError] = useState<string | null>(null)
74
+
75
+ useEffect(() => {
76
+ if (isNew) return
77
+ apiFetch(`/api/admin-data?action=get&entity=items&id=${id}`)
78
+ .then(r => r.json())
79
+ .then(raw => {
80
+ const j = raw?.data ?? raw
81
+ if (j) setForm({
82
+ title: j.title || '',
83
+ description: j.description || '',
84
+ status: j.status || 'draft',
85
+ kb_type: j.data?.kb_type || 'article',
86
+ priority: j.data?.priority || 'medium',
87
+ category: j.data?.category || '',
88
+ security_level: j.data?.security_level || 'internal',
89
+ tags: (() => { const t = j.data?.tags; if (Array.isArray(t)) return t.join(', '); if (typeof t === 'string') { try { const p = JSON.parse(t); return Array.isArray(p) ? p.join(', ') : t } catch { return t } } return '' })(),
90
+ audience: Array.isArray(j.data?.audience) ? j.data.audience : [],
91
+ })
92
+ })
93
+ .catch(() => setError('Failed to load article'))
94
+ .finally(() => setLoading(false))
95
+ }, [id, isNew])
96
+
97
+ const handleSave = async (publish?: boolean) => {
98
+ if (!form.title.trim()) { setError('Title is required'); return }
99
+ if (!form.kb_type) { setError('KB Type is required'); return }
100
+ setSaving(true)
101
+ setError(null)
102
+ try {
103
+ const kbArticleTypeId = await resolveTypeId('kb_article')
104
+ const payload = {
105
+ title: form.title.trim(),
106
+ description: form.description,
107
+ type_id: kbArticleTypeId,
108
+ status: publish ? 'published' : form.status,
109
+ data: {
110
+ kb_type: form.kb_type,
111
+ priority: form.priority,
112
+ category: form.category.trim() || null,
113
+ security_level: form.security_level,
114
+ tags: form.tags ? form.tags.split(',').map(t => t.trim()).filter(Boolean) : [],
115
+ audience: form.audience,
116
+ },
117
+ }
118
+ let res: Response
119
+ if (isNew) {
120
+ res = await apiFetch('/api/admin-data?action=create&entity=items', {
121
+ method: 'POST',
122
+ headers: { 'Content-Type': 'application/json' },
123
+ body: JSON.stringify(payload),
124
+ })
125
+ } else {
126
+ res = await apiFetch(`/api/admin-data?entity=items&id=${id}`, {
127
+ method: 'PATCH',
128
+ headers: { 'Content-Type': 'application/json' },
129
+ body: JSON.stringify(payload),
130
+ })
131
+ }
132
+ if (!res.ok) { const e = await res.clone().json().catch(() => ({})); throw new Error(e?.error || 'Save failed') }
133
+
134
+ // Auto-generate embeddings for the saved article
135
+ const saved = await res.json().catch(() => null)
136
+ const itemId = isNew ? (saved?.id || saved?.data?.id) : id
137
+ if (itemId) {
138
+ try {
139
+ await apiFetch('/api/custom_kb-embeddings?action=generate', {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({
143
+ item_ids: [itemId],
144
+ vector_types: ['semantic', 'structure'],
145
+ force_regenerate: true,
146
+ }),
147
+ })
148
+ } catch (embErr) {
149
+ console.warn('Embedding generation failed (article saved successfully):', embErr)
150
+ }
151
+ }
152
+
153
+ navigate('/cortex/kb')
154
+ } catch (e: any) {
155
+ setError(e.message)
156
+ } finally {
157
+ setSaving(false)
158
+ }
159
+ }
160
+
161
+ const toggleAudience = (val: string) => {
162
+ setForm(f => ({
163
+ ...f,
164
+ audience: f.audience.includes(val) ? f.audience.filter(a => a !== val) : [...f.audience, val],
165
+ }))
166
+ }
167
+
168
+ if (loading) return (
169
+ <div className="p-6 space-y-4">
170
+ <Skeleton className="h-8 w-64" />
171
+ <Skeleton className="h-64 w-full" />
172
+ </div>
173
+ )
174
+
175
+ return (
176
+ <div className="flex flex-col h-full">
177
+ {/* Header */}
178
+ <div className="px-6 py-3 border-b border-border shrink-0 flex items-center justify-between">
179
+ <div className="flex items-center gap-3">
180
+ <Button variant="ghost" size="sm" className="gap-1 text-muted-foreground" onClick={() => navigate('/cortex/kb')}>
181
+ <ArrowLeft className="h-3.5 w-3.5" /> KB
182
+ </Button>
183
+ <h1 className="text-base font-semibold">{isNew ? 'New Article' : 'Edit Article'}</h1>
184
+ </div>
185
+ <div className="flex items-center gap-2">
186
+ <Button variant="outline" size="sm" onClick={() => handleSave(false)} disabled={saving}>
187
+ <Save className="h-3.5 w-3.5 mr-1" /> Save
188
+ </Button>
189
+ <Button size="sm" onClick={() => handleSave(true)} disabled={saving}>
190
+ {saving ? 'Saving…' : 'Publish'}
191
+ </Button>
192
+ </div>
193
+ </div>
194
+
195
+ {error && (
196
+ <div className="mx-6 mt-4 px-4 py-3 bg-destructive/10 border border-destructive/20 text-destructive rounded text-sm">
197
+ {error}
198
+ </div>
199
+ )}
200
+
201
+ <div className="flex flex-1 min-h-0">
202
+ {/* Main content area */}
203
+ <div className="flex-1 p-6 space-y-4 overflow-y-auto">
204
+ <div className="space-y-1.5">
205
+ <Label>Title *</Label>
206
+ <Input
207
+ value={form.title}
208
+ onChange={e => setForm(f => ({ ...f, title: e.target.value }))}
209
+ placeholder="Article title"
210
+ className="text-lg font-medium"
211
+ />
212
+ </div>
213
+ <div className="space-y-1.5 flex-1">
214
+ <Label>Content *</Label>
215
+ <RichTextEditor
216
+ value={form.description}
217
+ onChange={html => setForm(f => ({ ...f, description: html }))}
218
+ placeholder="Write your article content here…"
219
+ minHeight="400px"
220
+ />
221
+ </div>
222
+ </div>
223
+
224
+ {/* Sidebar */}
225
+ <div className="w-64 shrink-0 border-l border-border p-4 space-y-4 overflow-y-auto">
226
+ <SidebarSelect
227
+ label="Status"
228
+ value={form.status}
229
+ onChange={v => setForm(f => ({ ...f, status: v }))}
230
+ options={[
231
+ { value: 'draft', label: 'Draft' },
232
+ { value: 'review', label: 'Under Review' },
233
+ { value: 'published', label: 'Published' },
234
+ { value: 'deprecated', label: 'Deprecated' },
235
+ { value: 'archived', label: 'Archived' },
236
+ { value: 'restricted', label: 'Restricted Access' },
237
+ ]}
238
+ />
239
+ <SidebarSelect
240
+ label="KB Type *"
241
+ value={form.kb_type}
242
+ onChange={v => setForm(f => ({ ...f, kb_type: v }))}
243
+ options={[
244
+ { value: 'article', label: 'Article' },
245
+ { value: 'care_guide', label: 'Care Guide' },
246
+ { value: 'process_guide', label: 'Process Guide' },
247
+ { value: 'code_chunk', label: 'Code Chunk' },
248
+ { value: 'api_reference', label: 'API Reference' },
249
+ { value: 'troubleshooting', label: 'Troubleshooting' },
250
+ { value: 'faq', label: 'FAQ' },
251
+ { value: 'policy', label: 'Policy' },
252
+ { value: 'tutorial', label: 'Tutorial' },
253
+ ]}
254
+ />
255
+ <SidebarSelect
256
+ label="Priority *"
257
+ value={form.priority}
258
+ onChange={v => setForm(f => ({ ...f, priority: v }))}
259
+ options={[
260
+ { value: 'low', label: 'Low' },
261
+ { value: 'medium', label: 'Medium' },
262
+ { value: 'high', label: 'High' },
263
+ { value: 'critical', label: 'Critical' },
264
+ { value: 'emergency', label: 'Emergency' },
265
+ ]}
266
+ />
267
+ <SidebarSelect
268
+ label="Category"
269
+ value={form.category}
270
+ onChange={v => setForm(f => ({ ...f, category: v }))}
271
+ options={[
272
+ { value: '', label: '— None —' },
273
+ { value: 'getting_started', label: 'Getting Started' },
274
+ { value: 'user_guide', label: 'User Guide' },
275
+ { value: 'technical', label: 'Technical' },
276
+ { value: 'billing', label: 'Billing' },
277
+ { value: 'security', label: 'Security' },
278
+ { value: 'integration', label: 'Integration' },
279
+ { value: 'troubleshooting', label: 'Troubleshooting' },
280
+ { value: 'reference', label: 'Reference' },
281
+ { value: 'best_practices', label: 'Best Practices' },
282
+ { value: 'policies', label: 'Policies' },
283
+ ]}
284
+ />
285
+ <SidebarSelect
286
+ label="Security Level"
287
+ value={form.security_level}
288
+ onChange={v => setForm(f => ({ ...f, security_level: v }))}
289
+ options={[
290
+ { value: 'public', label: 'Public' },
291
+ { value: 'internal', label: 'Internal' },
292
+ { value: 'confidential', label: 'Confidential' },
293
+ { value: 'restricted', label: 'Restricted' },
294
+ ]}
295
+ />
296
+
297
+ <div className="space-y-1.5">
298
+ <Label className="text-xs">Tags</Label>
299
+ <Input
300
+ value={form.tags}
301
+ onChange={e => setForm(f => ({ ...f, tags: e.target.value }))}
302
+ placeholder="tag1, tag2, tag3"
303
+ className="text-sm"
304
+ />
305
+ <p className="text-xs text-muted-foreground">Comma-separated</p>
306
+ </div>
307
+
308
+ <div className="space-y-1.5">
309
+ <Label className="text-xs">Audience</Label>
310
+ <div className="space-y-1">
311
+ {AUDIENCE_OPTIONS.map(opt => (
312
+ <label key={opt.value} className="flex items-center gap-2 cursor-pointer">
313
+ <input
314
+ type="checkbox"
315
+ checked={form.audience.includes(opt.value)}
316
+ onChange={() => toggleAudience(opt.value)}
317
+ className="rounded border-border"
318
+ />
319
+ <span className="text-xs">{opt.label}</span>
320
+ </label>
321
+ ))}
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ )
328
+ }