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,159 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Send, Hash, MessageSquarePlus, AlertCircle } from 'lucide-react'
3
+ import { apiFetch } from '@core/lib/api'
4
+ import { Button } from '@core/components/ui/button'
5
+ import { Input } from '@core/components/ui/input'
6
+ import { Badge } from '@core/components/ui/badge'
7
+ import { Skeleton } from '@core/components/ui/skeleton'
8
+ import { ScrollArea } from '@core/components/ui/scroll-area'
9
+
10
+ const CHANNELS = ['general', 'announcements', 'help', 'show-and-tell']
11
+ const CHANNEL_LABELS: Record<string, string> = { general: 'General', announcements: 'Announcements', help: 'Help', 'show-and-tell': 'Show & Tell' }
12
+
13
+ interface Post { id: string; title: string; description?: string; data?: Record<string, any>; created_at: string }
14
+ interface Message { id: string; content: string; direction: string; created_at: string }
15
+ interface Thread { id: string }
16
+
17
+ function ThreadPane({ post }: { post: Post }) {
18
+ const [messages, setMessages] = useState<Message[]>([])
19
+ const [thread, setThread] = useState<Thread | null>(null)
20
+ const [loading, setLoading] = useState(true)
21
+ const [replyText, setReplyText] = useState('')
22
+ const [sending, setSending] = useState(false)
23
+
24
+ useEffect(() => {
25
+ setLoading(true)
26
+ apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${post.id}&limit=5`)
27
+ .then(r => r.json()).then(j => {
28
+ const t = (j?.data ?? j)?.[0] ?? null
29
+ setThread(t)
30
+ if (t) return apiFetch(`/api/admin-data?action=list&entity=messages&thread_id=${t.id}&limit=100`).then(r => r.json())
31
+ return []
32
+ }).then(raw => { const msgs = raw?.data ?? raw; setMessages(Array.isArray(msgs) ? msgs : []) }).catch(() => {}).finally(() => setLoading(false))
33
+ }, [post.id])
34
+
35
+ const handleSend = async () => {
36
+ if (!replyText.trim() || !thread) return
37
+ setSending(true)
38
+ try {
39
+ const res = await apiFetch('/api/admin-data?action=create&entity=messages', {
40
+ 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' }),
42
+ })
43
+ const msg = await res.json()
44
+ if (msg?.id) setMessages(prev => [...prev, msg])
45
+ setReplyText('')
46
+ } finally { setSending(false) }
47
+ }
48
+
49
+ return (
50
+ <div className="flex flex-col h-full">
51
+ <div className="px-6 py-3 border-b border-border shrink-0">
52
+ <h3 className="text-sm font-semibold">{post.title}</h3>
53
+ {post.description && <p className="text-sm text-muted-foreground mt-1">{post.description}</p>}
54
+ </div>
55
+ <ScrollArea className="flex-1 p-4">
56
+ <div className="space-y-3">
57
+ {loading && <Skeleton className="h-10 w-2/3" />}
58
+ {!loading && messages.length === 0 && <p className="text-sm text-muted-foreground italic text-center py-8">No replies yet.</p>}
59
+ {messages.map(msg => (
60
+ <div key={msg.id} className={`flex ${msg.direction === 'outbound' ? 'justify-end' : 'justify-start'}`}>
61
+ <div className={`max-w-sm rounded-lg px-3 py-2 text-sm ${msg.direction === 'outbound' ? 'bg-primary text-primary-foreground' : 'bg-muted border border-border'}`}>{msg.content}</div>
62
+ </div>
63
+ ))}
64
+ </div>
65
+ </ScrollArea>
66
+ <div className="border-t p-3 flex gap-2 shrink-0">
67
+ <Input placeholder="Reply as agent…" value={replyText} onChange={e => setReplyText(e.target.value)}
68
+ 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>
70
+ </div>
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export default function CommunityPage() {
76
+ const [activeChannel, setActiveChannel] = useState('general')
77
+ const [selected, setSelected] = useState<Post | null>(null)
78
+ const [search, setSearch] = useState('')
79
+ const [posts, setPosts] = useState<Post[]>([])
80
+ const [loading, setLoading] = useState(true)
81
+
82
+ useEffect(() => {
83
+ apiFetch('/api/admin-data?action=list&entity=items&type_slug=community_post&limit=500')
84
+ .then(r => r.json()).then(j => setPosts(Array.isArray(j?.data) ? j.data : Array.isArray(j) ? j : [])).catch(() => setPosts([])).finally(() => setLoading(false))
85
+ }, [])
86
+
87
+ const channelPosts = posts.filter(p => (p.data?.channel as string || 'general') === activeChannel)
88
+ const filtered = channelPosts.filter(p => p.title?.toLowerCase().includes(search.toLowerCase()))
89
+ const unanswered = posts.filter(p => !p.data?.reply_count)
90
+
91
+ return (
92
+ <div className="flex h-full min-h-0">
93
+ <div className="w-44 shrink-0 border-r border-border bg-muted/30 flex flex-col min-h-0">
94
+ <div className="px-3 py-2 h-9 border-b border-border shrink-0 flex items-center">
95
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Channels</p>
96
+ </div>
97
+ <div className="flex-1 overflow-y-auto py-1">
98
+ {CHANNELS.map(ch => {
99
+ const count = posts.filter(p => (p.data?.channel as string || 'general') === ch).length
100
+ return (
101
+ <button key={ch} onClick={() => { setActiveChannel(ch); setSelected(null) }}
102
+ className={`w-full text-left px-3 py-2 flex items-center gap-2 hover:bg-accent/50 transition-colors ${activeChannel === ch ? 'bg-accent text-accent-foreground font-medium' : 'text-muted-foreground'}`}>
103
+ <Hash size={13} className="shrink-0" />
104
+ <span className="text-sm flex-1 truncate">{CHANNEL_LABELS[ch]}</span>
105
+ {count > 0 && <Badge variant="secondary" className="text-xs h-4 px-1.5">{count}</Badge>}
106
+ </button>
107
+ )
108
+ })}
109
+ </div>
110
+ </div>
111
+
112
+ <div className="w-64 shrink-0 border-r border-border flex flex-col min-h-0">
113
+ <div className="flex items-center px-3 py-2 h-9 border-b border-border shrink-0">
114
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide flex-1">#{CHANNEL_LABELS[activeChannel]}</p>
115
+ </div>
116
+ <div className="px-3 py-2 border-b border-border shrink-0">
117
+ <Input placeholder="Search…" value={search} onChange={e => setSearch(e.target.value)} className="h-7 text-xs" />
118
+ </div>
119
+ <div className="flex-1 overflow-y-auto">
120
+ {loading ? <div className="p-3 space-y-2">{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-12 w-full" />)}</div>
121
+ : filtered.length === 0 ? <div className="flex flex-col items-center justify-center gap-2 py-10 px-4 text-center"><MessageSquarePlus size={24} className="text-muted-foreground/40" /><p className="text-sm text-muted-foreground">No discussions yet.</p></div>
122
+ : filtered.map(post => (
123
+ <button key={post.id} onClick={() => setSelected(post)}
124
+ className={`w-full text-left px-3 py-2.5 border-b border-border hover:bg-accent/50 transition-colors ${selected?.id === post.id ? 'bg-accent border-l-2 border-l-primary' : ''}`}>
125
+ <p className="text-sm font-medium truncate">{post.title || 'Untitled'}</p>
126
+ {post.description && <p className="text-xs text-muted-foreground truncate mt-0.5">{post.description}</p>}
127
+ </button>
128
+ ))
129
+ }
130
+ </div>
131
+ </div>
132
+
133
+ <div className="flex-1 min-h-0 flex flex-col">
134
+ {!selected ? <div className="flex-1 flex flex-col items-center justify-center gap-3 text-muted-foreground"><MessageSquarePlus size={32} className="opacity-30" /><p className="text-sm">Select a discussion to reply</p></div>
135
+ : <ThreadPane post={selected} />}
136
+ </div>
137
+
138
+ <div className="w-52 shrink-0 border-l border-border flex flex-col min-h-0">
139
+ <div className="px-4 py-2 border-b border-border shrink-0 flex items-center gap-2">
140
+ <AlertCircle className="h-3.5 w-3.5 text-amber-500" />
141
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Moderation</p>
142
+ </div>
143
+ <ScrollArea className="flex-1">
144
+ <div className="p-3 space-y-1">
145
+ <p className="text-xs font-medium text-muted-foreground px-2 py-1">Unanswered ({unanswered.length})</p>
146
+ {unanswered.length === 0 && <p className="text-xs text-muted-foreground px-2 italic">All caught up.</p>}
147
+ {unanswered.slice(0, 15).map(p => (
148
+ <button key={p.id} onClick={() => { setActiveChannel(p.data?.channel as string || 'general'); setSelected(p) }}
149
+ className="w-full text-left px-2 py-2 rounded hover:bg-accent/50 transition-colors">
150
+ <p className="text-xs font-medium truncate">{p.title || 'Untitled'}</p>
151
+ <p className="text-xs text-muted-foreground">{new Date(p.created_at).toLocaleDateString()}</p>
152
+ </button>
153
+ ))}
154
+ </div>
155
+ </ScrollArea>
156
+ </div>
157
+ </div>
158
+ )
159
+ }
@@ -0,0 +1,231 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { resolveTypeId } from '../../../lib/resolveTypeId'
3
+ import { useNavigate } from 'react-router-dom'
4
+ import { apiFetch } from '@core/lib/api'
5
+ import { Button } from '@core/components/ui/button'
6
+ import { Input } from '@core/components/ui/input'
7
+ import { Badge } from '@core/components/ui/badge'
8
+ import { Skeleton } from '@core/components/ui/skeleton'
9
+ import { ScrollArea } from '@core/components/ui/scroll-area'
10
+ import { Separator } from '@core/components/ui/separator'
11
+ import { GraduationCap, Plus, PlayCircle, Edit, MessageSquare } from 'lucide-react'
12
+
13
+ interface Lesson { id: string; title: string; description?: string; status?: string; data?: Record<string, any>; created_at: string }
14
+ interface Message { id: string; content: string; created_at: string; direction: string }
15
+ interface Thread { id: string }
16
+
17
+ function groupByCourse(lessons: Lesson[]): Map<string, Lesson[]> {
18
+ const map = new Map<string, Lesson[]>()
19
+ for (const l of lessons) {
20
+ const key = (l.data?.course_title as string) || 'General'
21
+ if (!map.has(key)) map.set(key, [])
22
+ map.get(key)!.push(l)
23
+ }
24
+ for (const [k, v] of map) map.set(k, [...v].sort((a, b) => ((a.data?.sequence as number) ?? 9999) - ((b.data?.sequence as number) ?? 9999)))
25
+ return map
26
+ }
27
+
28
+ function DiscussionPanel({ lesson }: { lesson: Lesson }) {
29
+ const [messages, setMessages] = useState<Message[]>([])
30
+ const [thread, setThread] = useState<Thread | null>(null)
31
+ const [loading, setLoading] = useState(true)
32
+ const [newMessage, setNewMessage] = useState('')
33
+ const [submitting, setSubmitting] = useState(false)
34
+
35
+ useEffect(() => {
36
+ setLoading(true)
37
+ apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${lesson.id}&limit=5`)
38
+ .then(r => r.json()).then(j => {
39
+ const t = (j?.data ?? j)?.[0] ?? null
40
+ setThread(t)
41
+ if (t) return apiFetch(`/api/admin-data?action=list&entity=messages&thread_id=${t.id}&limit=50`).then(r => r.json())
42
+ return []
43
+ }).then(raw => { const msgs = raw?.data ?? raw; setMessages(Array.isArray(msgs) ? msgs : []) }).catch(() => {}).finally(() => setLoading(false))
44
+ }, [lesson.id])
45
+
46
+ const handleSubmit = async (e: React.FormEvent) => {
47
+ e.preventDefault()
48
+ if (!newMessage.trim() || submitting) return
49
+
50
+ setSubmitting(true)
51
+ try {
52
+ // Create thread if it doesn't exist
53
+ let currentThread = thread
54
+ if (!currentThread) {
55
+ const threadResponse = await apiFetch('/api/admin-data?action=create&entity=threads', {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({
59
+ target_type: 'item',
60
+ target_id: lesson.id,
61
+ title: `Discussion: ${lesson.title}`,
62
+ status: 'active'
63
+ })
64
+ })
65
+ const threadResult = await threadResponse.json()
66
+ currentThread = threadResult?.data || threadResult
67
+ setThread(currentThread)
68
+ }
69
+
70
+ // Create message
71
+ const messageTypeId = await resolveTypeId('message')
72
+ const messageResponse = await apiFetch('/api/admin-data?action=create&entity=messages', {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({
76
+ type_id: messageTypeId,
77
+ thread_id: currentThread.id,
78
+ content: newMessage.trim(),
79
+ direction: 'inbound',
80
+ sequence: messages.length + 1
81
+ })
82
+ })
83
+ const messageResult = await messageResponse.json()
84
+ const newMsg = messageResult?.data || messageResult
85
+
86
+ // Add to local state
87
+ setMessages(prev => [...prev, newMsg])
88
+ setNewMessage('')
89
+ } catch (error) {
90
+ console.error('Failed to send message:', error)
91
+ } finally {
92
+ setSubmitting(false)
93
+ }
94
+ }
95
+
96
+ return (
97
+ <div className="shrink-0 border-t border-border" style={{ maxHeight: '300px' }}>
98
+ <div className="px-4 py-2 border-b border-border flex items-center gap-2">
99
+ <MessageSquare className="h-3.5 w-3.5 text-muted-foreground" />
100
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Discussion ({messages.length})</p>
101
+ </div>
102
+ <ScrollArea style={{ maxHeight: '200px' }}>
103
+ <div className="px-4 py-2 space-y-2">
104
+ {loading && <Skeleton className="h-8 w-full" />}
105
+ {!loading && messages.length === 0 && <p className="text-xs text-muted-foreground italic py-2">No discussion yet.</p>}
106
+ {messages.map(msg => (
107
+ <div key={msg.id || `msg-${Math.random()}`} className="text-xs border border-border rounded p-2 bg-muted/30">
108
+ <p>{msg.content}</p>
109
+ <p className="text-muted-foreground mt-1">{msg.created_at ? new Date(msg.created_at).toLocaleString() : 'Just now'}</p>
110
+ </div>
111
+ ))}
112
+ </div>
113
+ </ScrollArea>
114
+ <div className="px-4 py-3 border-t border-border">
115
+ <form onSubmit={handleSubmit} className="flex gap-2">
116
+ <input
117
+ type="text"
118
+ value={newMessage}
119
+ onChange={(e) => setNewMessage(e.target.value)}
120
+ placeholder="Add a comment..."
121
+ className="flex-1 px-3 py-2 text-xs border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
122
+ disabled={submitting}
123
+ />
124
+ <button
125
+ type="submit"
126
+ disabled={!newMessage.trim() || submitting}
127
+ className="px-3 py-2 text-xs bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
128
+ >
129
+ {submitting ? 'Sending...' : 'Send'}
130
+ </button>
131
+ </form>
132
+ </div>
133
+ </div>
134
+ )
135
+ }
136
+
137
+ export default function CoursesPage() {
138
+ const navigate = useNavigate()
139
+ const [lessons, setLessons] = useState<Lesson[]>([])
140
+ const [loading, setLoading] = useState(true)
141
+ const [search, setSearch] = useState('')
142
+ const [selectedCourse, setSelectedCourse] = useState<string | null>(null)
143
+ const [selected, setSelected] = useState<Lesson | null>(null)
144
+
145
+ useEffect(() => {
146
+ apiFetch('/api/admin-data?action=list&entity=items&type_slug=course_lesson&limit=500')
147
+ .then(r => r.json()).then(j => setLessons(Array.isArray(j?.data) ? j.data : Array.isArray(j) ? j : [])).catch(() => setLessons([])).finally(() => setLoading(false))
148
+ }, [])
149
+
150
+ const courseMap = groupByCourse(lessons)
151
+ const courseNames = Array.from(courseMap.keys()).filter(c => !search || c.toLowerCase().includes(search.toLowerCase()))
152
+ const chaptersForCourse = selectedCourse ? courseMap.get(selectedCourse) ?? [] : []
153
+
154
+ return (
155
+ <div className="flex h-full min-h-0">
156
+ <div className="w-48 shrink-0 border-r border-border flex flex-col min-h-0">
157
+ <div className="px-4 py-3 border-b border-border shrink-0 flex items-center justify-between">
158
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Courses</p>
159
+ </div>
160
+ <div className="px-3 py-2 border-b border-border shrink-0">
161
+ <Input placeholder="Search…" value={search} onChange={e => setSearch(e.target.value)} className="h-7 text-xs" />
162
+ </div>
163
+ <div className="flex-1 overflow-y-auto">
164
+ {loading ? <div className="p-3 space-y-2">{Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}</div>
165
+ : courseNames.length === 0 ? <div className="p-4 text-sm text-muted-foreground text-center">No courses.</div>
166
+ : courseNames.map(name => (
167
+ <button key={name} onClick={() => { setSelectedCourse(name); setSelected(null) }}
168
+ className={`w-full text-left px-4 py-3 border-b border-border hover:bg-accent/50 transition-colors ${selectedCourse === name ? 'bg-accent border-l-2 border-l-primary' : ''}`}>
169
+ <p className={`text-sm font-medium truncate ${selectedCourse === name ? 'text-primary' : ''}`}>{name}</p>
170
+ <p className="text-xs text-muted-foreground mt-0.5">{courseMap.get(name)?.length} lessons</p>
171
+ </button>
172
+ ))
173
+ }
174
+ </div>
175
+ </div>
176
+
177
+ <div className="w-64 shrink-0 border-r border-border flex flex-col min-h-0">
178
+ <div className="px-4 py-3 border-b border-border shrink-0">
179
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Lessons</p>
180
+ </div>
181
+ <div className="flex-1 overflow-y-auto">
182
+ {!selectedCourse ? <div className="p-4 text-sm text-muted-foreground">Select a course</div>
183
+ : chaptersForCourse.length === 0 ? <div className="p-4 text-sm text-muted-foreground">No lessons.</div>
184
+ : chaptersForCourse.map(lesson => {
185
+ const seq = lesson.data?.sequence as number | undefined
186
+ return (
187
+ <button key={lesson.id} onClick={() => setSelected(lesson)}
188
+ className={`w-full text-left px-4 py-3 border-b border-border hover:bg-accent/50 flex items-center gap-2 transition-colors ${selected?.id === lesson.id ? 'bg-accent border-l-2 border-l-primary' : ''}`}>
189
+ <PlayCircle size={14} className="text-muted-foreground shrink-0" />
190
+ <p className="text-sm truncate">{seq != null ? `${seq}. ` : ''}{lesson.title}</p>
191
+ </button>
192
+ )
193
+ })
194
+ }
195
+ </div>
196
+ </div>
197
+
198
+ <div className="flex-1 min-h-0 flex flex-col">
199
+ {!selected ? (
200
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-muted-foreground">
201
+ <GraduationCap size={32} className="opacity-30" />
202
+ <p className="text-sm">{selectedCourse ? 'Select a lesson' : 'Select a course to get started'}</p>
203
+ </div>
204
+ ) : (
205
+ <>
206
+ <div className="px-6 py-3 border-b border-border shrink-0 flex items-center justify-between">
207
+ <div>
208
+ <h2 className="text-base font-semibold">{selected.title}</h2>
209
+ {selected.description && <p className="text-xs text-muted-foreground mt-0.5">{selected.description}</p>}
210
+ </div>
211
+ </div>
212
+ <ScrollArea className="flex-1 border-b border-border">
213
+ <div className="px-6 py-5 max-w-2xl space-y-4">
214
+ {selected.data?.video_url && (
215
+ <div className="aspect-video bg-muted rounded-lg flex items-center justify-center border border-border">
216
+ <a href={selected.data.video_url as string} target="_blank" rel="noopener noreferrer" className="flex flex-col items-center gap-2 text-muted-foreground hover:text-foreground">
217
+ <PlayCircle size={40} /><span className="text-sm">Watch Video</span>
218
+ </a>
219
+ </div>
220
+ )}
221
+ {selected.data?.content && <><Separator /><div className="text-sm leading-relaxed whitespace-pre-wrap">{selected.data.content as string}</div></>}
222
+ {!selected.data?.video_url && !selected.data?.content && <p className="text-sm text-muted-foreground italic">No content available.</p>}
223
+ </div>
224
+ </ScrollArea>
225
+ <DiscussionPanel lesson={selected} />
226
+ </>
227
+ )}
228
+ </div>
229
+ </div>
230
+ )
231
+ }