Skip to content

Commit 840a028

Browse files
committed
Add footer fullscreen option
1 parent 7bc644a commit 840a028

File tree

3 files changed

+482
-113
lines changed

3 files changed

+482
-113
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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

Comments
 (0)