spine-framework-portal 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.
@@ -0,0 +1,445 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { Plus, Send, X, ChevronRight, ThumbsUp, ThumbsDown, Bot, AlertCircle, CheckCircle } from 'lucide-react'
3
+ import { useTickets, useTicket, useNewTicketTriage, useTriageReply, useSubmitFeedback, useUpdateTicket } from '../hooks/useTickets'
4
+ import { usePortalThread } from '../hooks/usePortalThreads'
5
+ import { usePortalSignal } from '../hooks/usePortalSignal'
6
+ import { SearchFilterBar } from '../components/SearchFilterBar'
7
+ import { StatusBadge } from '../components/StatusBadge'
8
+ import { Button } from '@core/components/ui/button'
9
+ import { Input } from '@core/components/ui/input'
10
+ import { Skeleton } from '@core/components/ui/skeleton'
11
+ import { ScrollArea } from '@core/components/ui/scroll-area'
12
+ import { Separator } from '@core/components/ui/separator'
13
+
14
+ interface TicketThreadProps {
15
+ ticketId: string
16
+ ticketStatus?: string
17
+ onStatusChange?: (status: string) => void
18
+ }
19
+
20
+ function AIResponseCard({ message, ticketId, messageId, onFeedback }: {
21
+ message: any
22
+ ticketId: string
23
+ messageId: string
24
+ onFeedback: () => void
25
+ }) {
26
+ const { submitFeedback, loading } = useSubmitFeedback()
27
+ const [feedback, setFeedback] = useState<'up' | 'down' | null>(message.data?.feedback || null)
28
+
29
+ const handleFeedback = async (type: 'up' | 'down') => {
30
+ if (loading || feedback) return
31
+ await submitFeedback(ticketId, messageId, type)
32
+ setFeedback(type)
33
+ onFeedback()
34
+ }
35
+
36
+ return (
37
+ <div className="flex justify-start">
38
+ <div className="max-w-md rounded-lg px-4 py-3 text-sm bg-muted/80 text-foreground border border-border">
39
+ <div className="flex items-center gap-2 mb-2">
40
+ <Bot size={14} className="text-primary" />
41
+ <span className="text-xs font-medium text-muted-foreground">Spine Assistant</span>
42
+ </div>
43
+ <div className="leading-relaxed">{message.content}</div>
44
+
45
+ {!feedback && (
46
+ <div className="flex items-center gap-2 mt-3 pt-3 border-t border-border/50">
47
+ <span className="text-xs text-muted-foreground">Was this helpful?</span>
48
+ <Button
49
+ variant="ghost"
50
+ size="icon"
51
+ className="h-6 w-6"
52
+ onClick={() => handleFeedback('up')}
53
+ disabled={loading}
54
+ >
55
+ <ThumbsUp size={12} />
56
+ </Button>
57
+ <Button
58
+ variant="ghost"
59
+ size="icon"
60
+ className="h-6 w-6"
61
+ onClick={() => handleFeedback('down')}
62
+ disabled={loading}
63
+ >
64
+ <ThumbsDown size={12} />
65
+ </Button>
66
+ </div>
67
+ )}
68
+
69
+ {feedback === 'up' && (
70
+ <div className="flex items-center gap-2 mt-3 pt-3 border-t border-border/50 text-xs text-green-600">
71
+ <CheckCircle size={12} />
72
+ <span>Thanks for the feedback!</span>
73
+ </div>
74
+ )}
75
+
76
+ {feedback === 'down' && (
77
+ <div className="flex items-center gap-2 mt-3 pt-3 border-t border-border/50 text-xs text-amber-600">
78
+ <AlertCircle size={12} />
79
+ <span>Escalated to our team. We&apos;ll follow up shortly.</span>
80
+ </div>
81
+ )}
82
+ </div>
83
+ </div>
84
+ )
85
+ }
86
+
87
+ function TicketThread({ ticketId, ticketStatus, onStatusChange }: TicketThreadProps) {
88
+ const { messages, loading, thread, refetch } = usePortalThread('items', ticketId)
89
+ const [replyText, setReplyText] = useState('')
90
+ const [aiState, setAiState] = useState<'idle' | 'analyzing' | 'responded' | 'escalated'>('idle')
91
+ const { sendReply, loading: replying } = useTriageReply()
92
+ const { sendSignal } = usePortalSignal()
93
+
94
+ const publicMessages = messages.filter((m: any) => m.visibility !== 'internal')
95
+
96
+ const handleSend = async () => {
97
+ if (!replyText.trim() || !thread?.id) return
98
+ const text = replyText.trim()
99
+ setReplyText('')
100
+ setAiState('analyzing')
101
+ try {
102
+ const result: any = await sendReply(text, thread.id, ticketId)
103
+ if (result.escalated) {
104
+ setAiState('escalated')
105
+ onStatusChange?.('human_assigned')
106
+ } else {
107
+ setAiState('responded')
108
+ }
109
+ await refetch()
110
+ sendSignal('ticket_reply', 'Replied to support ticket')
111
+ } catch (e: any) {
112
+ console.error(e)
113
+ setAiState('escalated')
114
+ }
115
+ }
116
+
117
+ const handleFeedback = async () => {
118
+ await refetch()
119
+ }
120
+
121
+ return (
122
+ <div className="flex flex-col h-full">
123
+ <ScrollArea className="flex-1 p-4">
124
+ <div className="space-y-3">
125
+ {/* AI State Indicator */}
126
+ {aiState === 'analyzing' && (
127
+ <div className="flex justify-center py-4">
128
+ <div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 px-3 py-2 rounded-full">
129
+ <Bot size={14} className="animate-pulse" />
130
+ <span>Analyzing your question…</span>
131
+ </div>
132
+ </div>
133
+ )}
134
+
135
+ {aiState === 'escalated' && (
136
+ <div className="flex justify-center py-4">
137
+ <div className="flex items-center gap-2 text-xs text-amber-600 bg-amber-50 px-3 py-2 rounded-full">
138
+ <AlertCircle size={14} />
139
+ <span>We&apos;re looking into this and will have a human response shortly</span>
140
+ </div>
141
+ </div>
142
+ )}
143
+
144
+ {loading && <div className="space-y-2"><Skeleton className="h-10 w-2/3" /><Skeleton className="h-10 w-1/2 ml-auto" /></div>}
145
+
146
+ {!loading && publicMessages.length === 0 && aiState === 'idle' && (
147
+ <p className="text-sm text-muted-foreground italic text-center py-8">No messages yet. Start the conversation.</p>
148
+ )}
149
+
150
+ {publicMessages.map((msg: any) => {
151
+ if (msg.data?.message_type === 'agent') {
152
+ return (
153
+ <AIResponseCard
154
+ key={msg.id}
155
+ message={msg}
156
+ ticketId={ticketId}
157
+ messageId={msg.id}
158
+ onFeedback={handleFeedback}
159
+ />
160
+ )
161
+ }
162
+ return (
163
+ <div key={msg.id} className={`flex ${msg.direction === 'inbound' ? 'justify-end' : 'justify-start'}`}>
164
+ <div className={`max-w-xs rounded-lg px-3 py-2 text-sm ${
165
+ msg.direction === 'inbound'
166
+ ? 'bg-primary text-primary-foreground'
167
+ : 'bg-muted text-foreground border border-border'
168
+ }`}>{msg.content}</div>
169
+ </div>
170
+ )
171
+ })}
172
+ </div>
173
+ </ScrollArea>
174
+ <div className="border-t p-3 flex gap-2 shrink-0">
175
+ <Input placeholder="Reply…" value={replyText} onChange={(e) => setReplyText(e.target.value)}
176
+ onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()} className="flex-1"
177
+ disabled={aiState === 'analyzing' || replying} />
178
+ <Button size="icon" onClick={handleSend} disabled={replying || aiState === 'analyzing' || !replyText.trim()}>
179
+ <Send size={14} />
180
+ </Button>
181
+ </div>
182
+ </div>
183
+ )
184
+ }
185
+
186
+ // Draft ticket view: blank ticket UI, customer types first message, AI creates ticket + replies
187
+ function DraftTicketView({ onClose, onCreated }: { onClose: () => void; onCreated: (ticketId: string) => void }) {
188
+ const { startTriage, loading } = useNewTicketTriage()
189
+ const [message, setMessage] = useState('')
190
+ const [aiState, setAiState] = useState<'idle' | 'analyzing' | 'done'>('idle')
191
+ const [draftMessages, setDraftMessages] = useState<Array<{ role: 'customer' | 'ai'; content: string; escalated?: boolean }>>([]
192
+ )
193
+ const { sendSignal } = usePortalSignal()
194
+
195
+ const handleSend = async () => {
196
+ if (!message.trim() || loading || aiState === 'analyzing') return
197
+ const text = message.trim()
198
+ setMessage('')
199
+ setDraftMessages([{ role: 'customer', content: text }])
200
+ setAiState('analyzing')
201
+ try {
202
+ const result: any = await startTriage(text)
203
+ const publicResponse = result.escalated
204
+ ? "We're looking into this and will have a human response shortly."
205
+ : result.public_response || 'Your ticket has been created and our team will be in touch.'
206
+ setDraftMessages([
207
+ { role: 'customer', content: text },
208
+ { role: 'ai', content: publicResponse, escalated: result.escalated },
209
+ ])
210
+ setAiState('done')
211
+ sendSignal('ticket_create', `New AI-triaged support ticket`)
212
+ // Transition to real ticket
213
+ setTimeout(() => onCreated(result.ticketId), 800)
214
+ } catch (e: any) {
215
+ setDraftMessages((prev) => [
216
+ ...prev,
217
+ { role: 'ai', content: "We're looking into this and will have a human response shortly.", escalated: true },
218
+ ])
219
+ setAiState('done')
220
+ }
221
+ }
222
+
223
+ return (
224
+ <div className="flex flex-col h-full">
225
+ {/* Header — mirrors existing ticket header style */}
226
+ <div className="px-6 py-3 border-b border-border flex items-center justify-between shrink-0">
227
+ <div>
228
+ <h2 className="text-sm font-semibold text-muted-foreground italic">New Ticket</h2>
229
+ <p className="text-xs text-muted-foreground mt-0.5">Describe your issue below</p>
230
+ </div>
231
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onClose}><X size={15} /></Button>
232
+ </div>
233
+
234
+ {/* Thread area */}
235
+ <ScrollArea className="flex-1 p-4">
236
+ <div className="space-y-3">
237
+ {draftMessages.length === 0 && (
238
+ <p className="text-sm text-muted-foreground italic text-center py-8">Describe your issue and our AI will respond immediately.</p>
239
+ )}
240
+
241
+ {draftMessages.map((m, i) => {
242
+ if (m.role === 'customer') {
243
+ return (
244
+ <div key={i} className="flex justify-end">
245
+ <div className="max-w-xs rounded-lg px-3 py-2 text-sm bg-primary text-primary-foreground">{m.content}</div>
246
+ </div>
247
+ )
248
+ }
249
+ if (m.escalated) {
250
+ return (
251
+ <div key={i} className="flex justify-center py-2">
252
+ <div className="flex items-center gap-2 text-xs text-amber-600 bg-amber-50 px-3 py-2 rounded-full border border-amber-200">
253
+ <AlertCircle size={14} />
254
+ <span>{m.content}</span>
255
+ </div>
256
+ </div>
257
+ )
258
+ }
259
+ return (
260
+ <div key={i} className="flex justify-start">
261
+ <div className="max-w-md rounded-lg px-4 py-3 text-sm bg-muted/80 text-foreground border border-border">
262
+ <div className="flex items-center gap-2 mb-2">
263
+ <Bot size={14} className="text-primary" />
264
+ <span className="text-xs font-medium text-muted-foreground">Spine Assistant</span>
265
+ </div>
266
+ <div className="leading-relaxed">{m.content}</div>
267
+ </div>
268
+ </div>
269
+ )
270
+ })}
271
+
272
+ {aiState === 'analyzing' && (
273
+ <div className="flex justify-center py-4">
274
+ <div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 px-3 py-2 rounded-full">
275
+ <Bot size={14} className="animate-pulse" />
276
+ <span>Analyzing your question…</span>
277
+ </div>
278
+ </div>
279
+ )}
280
+ </div>
281
+ </ScrollArea>
282
+
283
+ {/* Reply box */}
284
+ <div className="border-t p-3 flex gap-2 shrink-0">
285
+ <Input
286
+ placeholder="Describe your issue…"
287
+ value={message}
288
+ onChange={(e) => setMessage(e.target.value)}
289
+ onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
290
+ disabled={loading || aiState === 'analyzing' || aiState === 'done'}
291
+ className="flex-1"
292
+ autoFocus
293
+ />
294
+ <Button size="icon" onClick={handleSend} disabled={loading || !message.trim() || aiState !== 'idle'}>
295
+ <Send size={14} />
296
+ </Button>
297
+ </div>
298
+ </div>
299
+ )
300
+ }
301
+
302
+ export function TicketsPage() {
303
+ const [search, setSearch] = useState('')
304
+ const [selectedId, setSelectedId] = useState<string | null>(null)
305
+ const [showNew, setShowNew] = useState(false)
306
+
307
+ const { tickets, loading, error, refetch } = useTickets()
308
+ const { ticket: selectedTicket } = useTicket(selectedId)
309
+ const { updateTicket } = useUpdateTicket()
310
+
311
+ const filtered = tickets.filter((t) => (t.title ?? '').toLowerCase().includes(search.toLowerCase()))
312
+
313
+ const { sendSignal } = usePortalSignal()
314
+
315
+ const handleNewTicket = () => { setSelectedId(null); setShowNew(true) }
316
+
317
+ const handleDraftCreated = useCallback((ticketId: string) => {
318
+ setShowNew(false)
319
+ setSelectedId(ticketId)
320
+ refetch()
321
+ }, [refetch])
322
+
323
+ const handleSelectTicket = (id: string) => {
324
+ setSelectedId(id === selectedId ? null : id)
325
+ setShowNew(false)
326
+ if (id !== selectedId) sendSignal('ticket_view', 'Viewed support ticket')
327
+ }
328
+
329
+ const handleStatusChange = useCallback(async (newStatus: string) => {
330
+ if (!selectedId) return
331
+ await updateTicket(selectedId, {
332
+ data: {
333
+ status: newStatus,
334
+ aim_escalation_at: new Date().toISOString()
335
+ }
336
+ })
337
+ refetch()
338
+ }, [selectedId, selectedTicket, updateTicket, refetch])
339
+
340
+ return (
341
+ <div className="flex flex-col h-full">
342
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border bg-background shrink-0">
343
+ <SearchFilterBar placeholder="Search tickets…" value={search} onChange={setSearch} />
344
+ <Button size="sm" className="gap-1.5 shrink-0" onClick={handleNewTicket}>
345
+ <Plus size={14} /> New Ticket
346
+ </Button>
347
+ </div>
348
+
349
+ {error && <div className="px-4 py-2 text-sm text-destructive border-b border-border shrink-0">{error}</div>}
350
+
351
+ <div className="flex flex-1 min-h-0">
352
+ {/* Col 1 — ticket list */}
353
+ <div className="w-80 shrink-0 border-r border-border flex flex-col min-h-0">
354
+ <div className="flex-1 overflow-y-auto">
355
+ {loading ? (
356
+ <div className="p-4 space-y-3">{Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-14 w-full" />)}</div>
357
+ ) : filtered.length === 0 ? (
358
+ <div className="p-6 text-sm text-muted-foreground text-center">No tickets found.</div>
359
+ ) : (
360
+ filtered.map((ticket) => (
361
+ <button key={ticket.id}
362
+ onClick={() => handleSelectTicket(ticket.id)}
363
+ className={`w-full text-left px-4 py-3 border-b border-border hover:bg-accent/50 flex items-center gap-3 transition-colors ${
364
+ selectedId === ticket.id && !showNew ? 'bg-accent border-l-2 border-l-primary' : ''
365
+ }`}
366
+ >
367
+ <div className="flex-1 min-w-0">
368
+ <p className="text-sm font-medium truncate">{ticket.title}</p>
369
+ <p className="text-xs text-muted-foreground mt-0.5">{ticket.created_at ? new Date(ticket.created_at).toLocaleDateString() : '—'}</p>
370
+ </div>
371
+ <StatusBadge status={ticket.data?.status || ticket.status} />
372
+ <ChevronRight size={14} className="text-muted-foreground/50 shrink-0" />
373
+ </button>
374
+ ))
375
+ )}
376
+ </div>
377
+ </div>
378
+
379
+ {/* Col 2 — thread or new ticket */}
380
+ <div className="flex-1 flex flex-col min-h-0">
381
+ {showNew ? (
382
+ <DraftTicketView onClose={() => setShowNew(false)} onCreated={handleDraftCreated} />
383
+ ) : selectedId && selectedTicket ? (
384
+ <>
385
+ <div className="px-6 py-3 border-b border-border flex items-center justify-between shrink-0">
386
+ <div>
387
+ <h2 className="text-sm font-semibold">{selectedTicket.title}</h2>
388
+ <p className="text-xs text-muted-foreground mt-0.5 font-mono">{selectedTicket.id.slice(0, 8)}…</p>
389
+ </div>
390
+ <StatusBadge status={selectedTicket.data?.status || selectedTicket.status} />
391
+ </div>
392
+ <div className="flex-1 min-h-0">
393
+ <TicketThread
394
+ ticketId={selectedId}
395
+ ticketStatus={selectedTicket.data?.status || selectedTicket.status}
396
+ onStatusChange={handleStatusChange}
397
+ />
398
+ </div>
399
+ </>
400
+ ) : (
401
+ <div className="flex-1 flex items-center justify-center text-sm text-muted-foreground">
402
+ Select a ticket to view the conversation
403
+ </div>
404
+ )}
405
+ </div>
406
+
407
+ {/* Col 3 — ticket details (also shown during draft once AI responds) */}
408
+ {selectedId && selectedTicket && !showNew && (
409
+ <div className="w-72 shrink-0 border-l border-border flex flex-col min-h-0">
410
+ <div className="flex items-center px-4 py-2 h-9 border-b border-border shrink-0">
411
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Details</p>
412
+ </div>
413
+ <ScrollArea className="flex-1">
414
+ <div className="p-4 space-y-4 text-sm">
415
+ <div>
416
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">Subject</p>
417
+ <p className="font-medium">{selectedTicket.title}</p>
418
+ </div>
419
+ <Separator />
420
+ <div>
421
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">Status</p>
422
+ <StatusBadge status={selectedTicket.data?.status || selectedTicket.status} />
423
+ </div>
424
+ <Separator />
425
+ <div>
426
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">Created</p>
427
+ <p className="text-muted-foreground">{selectedTicket.created_at ? new Date(selectedTicket.created_at).toLocaleString() : '—'}</p>
428
+ </div>
429
+ {selectedTicket.description && (
430
+ <>
431
+ <Separator />
432
+ <div>
433
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">Description</p>
434
+ <p className="text-muted-foreground leading-relaxed">{selectedTicket.description}</p>
435
+ </div>
436
+ </>
437
+ )}
438
+ </div>
439
+ </ScrollArea>
440
+ </div>
441
+ )}
442
+ </div>
443
+ </div>
444
+ )
445
+ }
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "name": "Community: Unanswered to Ticket",
4
+ "description": "Check for community posts >24h without answers and escalate to support tickets",
5
+ "trigger_type": "cron",
6
+ "event_type": null,
7
+ "config": {
8
+ "function": "custom_community-escalation.checkUnanswered",
9
+ "schedule": "0 */4 * * *",
10
+ "timezone": "UTC",
11
+ "description": "Check for community posts >24h without answers, create tickets"
12
+ },
13
+ "ownership": "tenant",
14
+ "is_system": false,
15
+ "is_active": true
16
+ }
17
+ ]
@@ -0,0 +1,136 @@
1
+ [
2
+ {
3
+ "kind": "item",
4
+ "slug": "community_post",
5
+ "name": "Community Post",
6
+ "description": "Community discussion post with voting and accepted answers",
7
+ "icon": "message-circle",
8
+ "color": "#6366F1",
9
+ "ownership": "tenant",
10
+ "is_active": true,
11
+ "design_schema": {
12
+ "scope": "platform",
13
+ "fields": {
14
+ "title": { "label": "Title", "required": true, "data_type": "text" },
15
+ "content": { "label": "Content", "required": true, "data_type": "text" },
16
+ "context": { "label": "Context", "required": true, "data_type": "text", "options": ["support", "community"] },
17
+ "channel": { "label": "Channel", "required": true, "data_type": "text", "default": "general", "options": ["general", "announcements", "help", "show-and-tell"] },
18
+ "moderation_status": { "label": "Moderation Status", "data_type": "text", "default": "pending", "options": ["pending", "approved", "flagged"] },
19
+ "helpful_count": { "label": "Helpful Votes", "data_type": "integer", "default": 0 },
20
+ "not_helpful_count": { "label": "Not Helpful Votes", "data_type": "integer", "default": 0 },
21
+ "accepted_answer_id": { "label": "Accepted Answer", "data_type": "uuid" }
22
+ },
23
+ "record_permissions": {
24
+ "member": ["create", "read", "update"],
25
+ "system_admin": ["create", "read", "update", "delete"]
26
+ }
27
+ },
28
+ "validation_schema": {}
29
+ },
30
+ {
31
+ "kind": "item",
32
+ "slug": "course_lesson",
33
+ "name": "Course Lesson",
34
+ "description": "Learning content unit — part of a course sequence",
35
+ "icon": "graduation-cap",
36
+ "color": "#F97316",
37
+ "ownership": "tenant",
38
+ "is_active": true,
39
+ "design_schema": {
40
+ "scope": "platform",
41
+ "fields": {
42
+ "title": { "label": "Title", "required": true, "data_type": "text" },
43
+ "content": { "label": "Content", "required": true, "data_type": "text" },
44
+ "context": { "label": "Context", "required": true, "data_type": "text", "options": ["kb", "course"] },
45
+ "sequence": { "label": "Lesson Sequence", "data_type": "integer" },
46
+ "video_url": { "label": "Video URL", "data_type": "url" },
47
+ "estimated_duration": { "label": "Estimated Duration (minutes)", "data_type": "integer" },
48
+ "progress_required": { "label": "Progress Required", "data_type": "boolean", "default": true },
49
+ "prerequisites": { "label": "Prerequisite Lessons", "data_type": "uuid[]" }
50
+ },
51
+ "record_permissions": {
52
+ "all": ["read"],
53
+ "support": ["read", "create", "update"]
54
+ }
55
+ },
56
+ "validation_schema": {}
57
+ },
58
+ {
59
+ "kind": "item",
60
+ "slug": "integrity_report",
61
+ "name": "Integrity Report",
62
+ "description": "Spine core integrity check result from a customer deploy",
63
+ "icon": "shield-check",
64
+ "color": "#84CC16",
65
+ "ownership": "tenant",
66
+ "is_active": true,
67
+ "design_schema": {
68
+ "scope": "account",
69
+ "fields": {
70
+ "title": { "label": "Title", "system": true, "required": true, "data_type": "text" },
71
+ "status": { "label": "Status", "system": true, "required": false, "data_type": "text" },
72
+ "is_active": { "label": "Active", "system": true, "required": true, "data_type": "boolean" },
73
+ "core_hash": { "label": "Core Hash", "system": false, "required": true, "data_type": "text" },
74
+ "manifest_hash": { "label": "Manifest Hash", "system": false, "required": true, "data_type": "text" },
75
+ "deploy_id": { "label": "Deploy ID", "system": false, "required": false, "data_type": "text" },
76
+ "deploy_url": { "label": "Deploy URL", "system": false, "required": false, "data_type": "text" },
77
+ "created_at": { "label": "Created", "system": true, "readonly": true, "required": false, "data_type": "datetime" },
78
+ "updated_at": { "label": "Updated", "system": true, "readonly": true, "required": false, "data_type": "datetime" }
79
+ },
80
+ "record_permissions": { "system_admin": ["create", "read", "update", "delete"] }
81
+ },
82
+ "validation_schema": {}
83
+ },
84
+ {
85
+ "kind": "item",
86
+ "slug": "support_ticket",
87
+ "name": "Support Ticket",
88
+ "description": "Customer support request with AI-first escalation chain",
89
+ "icon": "ticket",
90
+ "color": "#EF4444",
91
+ "ownership": "tenant",
92
+ "is_active": true,
93
+ "design_schema": {
94
+ "scope": "customer",
95
+ "fields": {
96
+ "title": { "label": "Title", "system": true, "required": true, "data_type": "text" },
97
+ "description": { "label": "Description", "system": true, "required": true, "data_type": "text" },
98
+ "status": { "label": "Status", "system": true, "data_type": "text", "default": "open", "options": ["open", "ai_responding", "human_assigned", "in_progress", "resolved", "closed"] },
99
+ "context": { "label": "Context", "required": true, "data_type": "text", "options": ["support", "community"] },
100
+ "priority": { "label": "Priority", "data_type": "text", "default": "medium", "options": ["low", "medium", "high", "urgent"] },
101
+ "escalated": { "label": "Escalated to Human", "data_type": "boolean", "default": false },
102
+ "ai_response": { "label": "AI Response", "data_type": "text" },
103
+ "ai_confidence": { "label": "AI Confidence", "data_type": "float" },
104
+ "aim_triage_agent_id": { "label": "Triage Agent ID", "data_type": "text" },
105
+ "aim_escalation_reason": { "label": "Escalation Reason", "data_type": "text", "options": ["low_confidence", "thumbs_down", "customer_request", "none"] },
106
+ "aim_human_assignee_id": { "label": "Human Assignee", "data_type": "text" },
107
+ "aim_confidence_threshold": { "label": "Confidence Threshold", "data_type": "number", "default": 0.75 },
108
+ "aim_confidence_at_response": { "label": "Confidence at Response", "data_type": "number" },
109
+ "ca_true_problem": { "label": "True Problem Identified", "data_type": "text" },
110
+ "ca_reported_issue": { "label": "Reported Issue", "data_type": "text" },
111
+ "ca_final_solution": { "label": "Final Solution Summary", "data_type": "text" },
112
+ "ca_solution_steps": { "label": "Steps to Solve", "data_type": "json" },
113
+ "ca_diagnostic_steps": { "label": "Steps to Diagnose", "data_type": "json" },
114
+ "ca_analysis_tags": { "label": "Analysis Tags", "data_type": "json" },
115
+ "ca_escalation_required": { "label": "Was Escalated to Human Agent", "data_type": "boolean" },
116
+ "ca_customer_temperature": { "label": "Customer Temperature", "data_type": "text", "options": ["positive", "neutral", "negative", "frustrated"] },
117
+ "ca_time_to_resolution": { "label": "Time to Resolution (minutes)", "data_type": "number" },
118
+ "ca_back_and_forth_count": { "label": "Number of Back and Forths", "data_type": "number" },
119
+ "ca_automation_potential": { "label": "Automation Potential", "data_type": "text", "options": ["high", "medium", "low"] },
120
+ "ca_sentiment_progression": { "label": "Customer Sentiment Progression", "data_type": "json" },
121
+ "ca_kb_candidate": { "label": "KB Candidate", "data_type": "boolean" },
122
+ "kb_proposed_kb_id": { "label": "Proposed KB Article ID", "data_type": "text" },
123
+ "kb_redacted_draft": { "label": "Redacted Draft", "data_type": "text" },
124
+ "kb_approved_at": { "label": "Approved At", "data_type": "datetime" },
125
+ "kb_approved_by": { "label": "Approved By", "data_type": "text" },
126
+ "kb_human_edits": { "label": "Human Edits", "data_type": "text" }
127
+ },
128
+ "record_permissions": {
129
+ "member": ["create", "read", "update"],
130
+ "support": ["create", "read", "update"],
131
+ "system_admin": ["create", "read", "update", "delete"]
132
+ }
133
+ },
134
+ "validation_schema": {}
135
+ }
136
+ ]