1+ 'use client'
2+
3+ import { type KeyboardEvent , useEffect , useRef , useState } from 'react'
4+ import { ArrowUp , Bot , User , X } from 'lucide-react'
5+ import { Button } from '@/components/ui/button'
6+ import { Input } from '@/components/ui/input'
7+ import { createLogger } from '@/lib/logs/console-logger'
8+
9+ const logger = createLogger ( 'CopilotModal' )
10+
11+ interface Message {
12+ id : string
13+ content : string
14+ type : 'user' | 'assistant'
15+ timestamp : Date
16+ citations ?: Array < {
17+ id : number
18+ title : string
19+ url : string
20+ } >
21+ }
22+
23+ interface CopilotModalMessage {
24+ message : Message
25+ }
26+
27+ // Modal-specific message component
28+ function ModalCopilotMessage ( { message } : CopilotModalMessage ) {
29+ const renderCitations = ( text : string , citations ?: Array < { id : number ; title : string ; url : string } > ) => {
30+ if ( ! citations || citations . length === 0 ) return text
31+
32+ let processedText = text
33+ citations . forEach ( ( citation ) => {
34+ const citationRegex = new RegExp ( `\\{cite:${ citation . id } \\}` , 'g' )
35+ processedText = processedText . replace (
36+ citationRegex ,
37+ `<a href="${ citation . url } " target="_blank" rel="noopener noreferrer" class="inline-flex items-center text-primary hover:text-primary/80 text-sm" title="${ citation . title } ">↗</a>`
38+ )
39+ } )
40+
41+ return processedText
42+ }
43+
44+ const renderMarkdown = ( text : string ) => {
45+ // Handle citations first
46+ let processedText = renderCitations ( text , message . citations )
47+
48+ // Handle code blocks
49+ processedText = processedText . replace (
50+ / ` ` ` ( \w + ) ? \n ( [ \s \S ] * ?) \n ` ` ` / g,
51+ '<pre class="bg-muted rounded-md p-3 my-2 overflow-x-auto"><code class="text-sm">$2</code></pre>'
52+ )
53+
54+ // Handle inline code
55+ processedText = processedText . replace ( / ` ( [ ^ ` ] + ) ` / g, '<code class="bg-muted px-1 rounded text-sm">$1</code>' )
56+
57+ // Handle headers
58+ processedText = processedText . replace ( / ^ # # # ( .* $ ) / gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>' )
59+ processedText = processedText . replace ( / ^ # # ( .* $ ) / gm, '<h2 class="text-xl font-semibold mt-4 mb-2">$1</h2>' )
60+ processedText = processedText . replace ( / ^ # ( .* $ ) / gm, '<h1 class="text-2xl font-bold mt-4 mb-2">$1</h1>' )
61+
62+ // Handle bold
63+ processedText = processedText . replace ( / \* \* ( .* ?) \* \* / g, '<strong>$1</strong>' )
64+
65+ // Handle lists
66+ processedText = processedText . replace ( / ^ - ( .* $ ) / gm, '<li class="ml-4">• $1</li>' )
67+
68+ // Handle line breaks
69+ processedText = processedText . replace ( / \n / g, '<br>' )
70+
71+ return processedText
72+ }
73+
74+ // For user messages (on the right)
75+ if ( message . type === 'user' ) {
76+ return (
77+ < div className = 'px-4 py-5' >
78+ < div className = 'mx-auto max-w-3xl' >
79+ < div className = 'flex justify-end' >
80+ < div className = 'max-w-[80%] rounded-3xl bg-[#F4F4F4] px-4 py-3 shadow-sm dark:bg-primary/10' >
81+ < div className = 'whitespace-pre-wrap break-words text-[#0D0D0D] text-base leading-relaxed dark:text-white' >
82+ { message . content }
83+ </ div >
84+ </ div >
85+ </ div >
86+ </ div >
87+ </ div >
88+ )
89+ }
90+
91+ // For assistant messages (on the left)
92+ return (
93+ < div className = 'px-4 py-5' >
94+ < div className = 'mx-auto max-w-3xl' >
95+ < div className = 'flex' >
96+ < div className = 'max-w-[80%]' >
97+ < div
98+ className = 'whitespace-pre-wrap break-words text-base leading-relaxed prose prose-sm max-w-none dark:prose-invert'
99+ dangerouslySetInnerHTML = { { __html : renderMarkdown ( message . content ) } }
100+ />
101+ </ div >
102+ </ div >
103+ </ div >
104+ </ div >
105+ )
106+ }
107+
108+ interface CopilotModalProps {
109+ open : boolean
110+ onOpenChange : ( open : boolean ) => void
111+ copilotMessage : string
112+ setCopilotMessage : ( message : string ) => void
113+ messages : Message [ ]
114+ onSendMessage : ( message : string ) => Promise < void >
115+ isLoading : boolean
116+ }
117+
118+ export function CopilotModal ( {
119+ open,
120+ onOpenChange,
121+ copilotMessage,
122+ setCopilotMessage,
123+ messages,
124+ onSendMessage,
125+ isLoading
126+ } : CopilotModalProps ) {
127+ const messagesEndRef = useRef < HTMLDivElement > ( null )
128+ const messagesContainerRef = useRef < HTMLDivElement > ( null )
129+ const inputRef = useRef < HTMLInputElement > ( null )
130+
131+ // Auto-scroll to bottom when new messages are added
132+ useEffect ( ( ) => {
133+ if ( messagesEndRef . current ) {
134+ messagesEndRef . current . scrollIntoView ( { behavior : 'smooth' } )
135+ }
136+ } , [ messages ] )
137+
138+ // Focus input when modal opens
139+ useEffect ( ( ) => {
140+ if ( open && inputRef . current ) {
141+ inputRef . current . focus ( )
142+ }
143+ } , [ open ] )
144+
145+ // Handle send message
146+ const handleSendMessage = async ( ) => {
147+ if ( ! copilotMessage . trim ( ) || isLoading ) return
148+
149+ try {
150+ await onSendMessage ( copilotMessage . trim ( ) )
151+ setCopilotMessage ( '' )
152+
153+ // Ensure input stays focused
154+ if ( inputRef . current ) {
155+ inputRef . current . focus ( )
156+ }
157+ } catch ( error ) {
158+ logger . error ( 'Failed to send message' , error )
159+ }
160+ }
161+
162+ // Handle key press
163+ const handleKeyPress = ( e : KeyboardEvent < HTMLInputElement > ) => {
164+ if ( e . key === 'Enter' && ! e . shiftKey ) {
165+ e . preventDefault ( )
166+ handleSendMessage ( )
167+ }
168+ }
169+
170+ if ( ! open ) return null
171+
172+ return (
173+ < div className = 'fixed inset-0 z-[100] flex flex-col bg-background' >
174+ < style jsx > { `
175+ @keyframes growShrink {
176+ 0%,
177+ 100% {
178+ transform: scale(0.9);
179+ }
180+ 50% {
181+ transform: scale(1.1);
182+ }
183+ }
184+ .loading-dot {
185+ animation: growShrink 1.5s infinite ease-in-out;
186+ }
187+ ` } </ style >
188+
189+ { /* Header with title and close button */ }
190+ < div className = 'flex items-center justify-between px-4 py-3' >
191+ < h2 className = 'font-medium text-lg' > Documentation Copilot</ h2 >
192+ < Button
193+ variant = 'ghost'
194+ size = 'icon'
195+ className = 'h-8 w-8 rounded-md hover:bg-accent/50'
196+ onClick = { ( ) => onOpenChange ( false ) }
197+ >
198+ < X className = 'h-4 w-4' />
199+ < span className = 'sr-only' > Close</ span >
200+ </ Button >
201+ </ div >
202+
203+ { /* Messages container */ }
204+ < div ref = { messagesContainerRef } className = 'flex-1 overflow-y-auto' >
205+ < div className = 'mx-auto max-w-3xl' >
206+ { messages . length === 0 ? (
207+ < div className = 'flex h-full flex-col items-center justify-center px-4 py-10' >
208+ < div className = 'space-y-4 text-center' >
209+ < Bot className = 'mx-auto h-12 w-12 text-muted-foreground' />
210+ < div className = 'space-y-2' >
211+ < h3 className = 'font-medium text-lg' > Welcome to Documentation Copilot</ h3 >
212+ < p className = 'text-muted-foreground text-sm' >
213+ Ask me anything about Sim Studio features, workflows, tools, or how to get started.
214+ </ p >
215+ </ div >
216+ < div className = 'space-y-2 text-left max-w-xs mx-auto' >
217+ < div className = 'text-muted-foreground text-xs' > Try asking:</ div >
218+ < div className = 'space-y-1' >
219+ < div className = 'rounded bg-muted/50 px-2 py-1 text-xs' >
220+ "How do I create a workflow?"
221+ </ div >
222+ < div className = 'rounded bg-muted/50 px-2 py-1 text-xs' >
223+ "What tools are available?"
224+ </ div >
225+ < div className = 'rounded bg-muted/50 px-2 py-1 text-xs' >
226+ "How do I deploy my workflow?"
227+ </ div >
228+ </ div >
229+ </ div >
230+ </ div >
231+ </ div >
232+ ) : (
233+ messages . map ( ( message ) => (
234+ < ModalCopilotMessage key = { message . id } message = { message } />
235+ ) )
236+ ) }
237+
238+ { /* Loading indicator (shows only when loading) */ }
239+ { isLoading && (
240+ < div className = 'px-4 py-5' >
241+ < div className = 'mx-auto max-w-3xl' >
242+ < div className = 'flex' >
243+ < div className = 'max-w-[80%]' >
244+ < div className = 'flex h-6 items-center' >
245+ < div className = 'loading-dot h-3 w-3 rounded-full bg-black dark:bg-black' />
246+ </ div >
247+ </ div >
248+ </ div >
249+ </ div >
250+ </ div >
251+ ) }
252+
253+ < div ref = { messagesEndRef } className = 'h-1' />
254+ </ div >
255+ </ div >
256+
257+ { /* Input area (fixed at bottom) */ }
258+ < div className = 'bg-background p-4' >
259+ < div className = 'mx-auto max-w-3xl' >
260+ < div className = 'relative rounded-2xl border bg-background shadow-sm' >
261+ < Input
262+ ref = { inputRef }
263+ value = { copilotMessage }
264+ onChange = { ( e ) => setCopilotMessage ( e . target . value ) }
265+ onKeyDown = { handleKeyPress }
266+ placeholder = 'Ask about Sim Studio documentation...'
267+ className = 'min-h-[50px] flex-1 rounded-2xl border-0 bg-transparent py-7 pr-16 pl-6 text-base focus-visible:ring-0 focus-visible:ring-offset-0'
268+ disabled = { isLoading }
269+ />
270+ < Button
271+ onClick = { handleSendMessage }
272+ size = 'icon'
273+ disabled = { ! copilotMessage . trim ( ) || isLoading }
274+ className = '-translate-y-1/2 absolute top-1/2 right-3 h-10 w-10 rounded-xl bg-black p-0 text-white hover:bg-gray-800 dark:bg-primary dark:hover:bg-primary/80'
275+ >
276+ < ArrowUp className = 'h-4 w-4 dark:text-black' />
277+ </ Button >
278+ </ div >
279+
280+ < div className = 'mt-2 text-center text-muted-foreground text-xs' >
281+ < p > Ask questions about Sim Studio documentation and features</ p >
282+ </ div >
283+ </ div >
284+ </ div >
285+ </ div >
286+ )
287+ }
0 commit comments