diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-modal/copilot-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-modal/copilot-modal.tsx
new file mode 100644
index 0000000000..d431a29858
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-modal/copilot-modal.tsx
@@ -0,0 +1,287 @@
+'use client'
+
+import { type KeyboardEvent, useEffect, useRef, useState } from 'react'
+import { ArrowUp, Bot, User, X } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { createLogger } from '@/lib/logs/console-logger'
+
+const logger = createLogger('CopilotModal')
+
+interface Message {
+ id: string
+ content: string
+ type: 'user' | 'assistant'
+ timestamp: Date
+ citations?: Array<{
+ id: number
+ title: string
+ url: string
+ }>
+}
+
+interface CopilotModalMessage {
+ message: Message
+}
+
+// Modal-specific message component
+function ModalCopilotMessage({ message }: CopilotModalMessage) {
+ const renderCitations = (text: string, citations?: Array<{ id: number; title: string; url: string }>) => {
+ if (!citations || citations.length === 0) return text
+
+ let processedText = text
+ citations.forEach((citation) => {
+ const citationRegex = new RegExp(`\\{cite:${citation.id}\\}`, 'g')
+ processedText = processedText.replace(
+ citationRegex,
+ `↗`
+ )
+ })
+
+ return processedText
+ }
+
+ const renderMarkdown = (text: string) => {
+ // Handle citations first
+ let processedText = renderCitations(text, message.citations)
+
+ // Handle code blocks
+ processedText = processedText.replace(
+ /```(\w+)?\n([\s\S]*?)\n```/g,
+ '
$2
'
+ )
+
+ // Handle inline code
+ processedText = processedText.replace(/`([^`]+)`/g, '$1')
+
+ // Handle headers
+ processedText = processedText.replace(/^### (.*$)/gm, '$1
')
+ processedText = processedText.replace(/^## (.*$)/gm, '$1
')
+ processedText = processedText.replace(/^# (.*$)/gm, '$1
')
+
+ // Handle bold
+ processedText = processedText.replace(/\*\*(.*?)\*\*/g, '$1')
+
+ // Handle lists
+ processedText = processedText.replace(/^- (.*$)/gm, '• $1')
+
+ // Handle line breaks
+ processedText = processedText.replace(/\n/g, '
')
+
+ return processedText
+ }
+
+ // For user messages (on the right)
+ if (message.type === 'user') {
+ return (
+
+
+
+
+
+ {message.content}
+
+
+
+
+
+ )
+ }
+
+ // For assistant messages (on the left)
+ return (
+
+ )
+}
+
+interface CopilotModalProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ copilotMessage: string
+ setCopilotMessage: (message: string) => void
+ messages: Message[]
+ onSendMessage: (message: string) => Promise
+ isLoading: boolean
+}
+
+export function CopilotModal({
+ open,
+ onOpenChange,
+ copilotMessage,
+ setCopilotMessage,
+ messages,
+ onSendMessage,
+ isLoading
+}: CopilotModalProps) {
+ const messagesEndRef = useRef(null)
+ const messagesContainerRef = useRef(null)
+ const inputRef = useRef(null)
+
+ // Auto-scroll to bottom when new messages are added
+ useEffect(() => {
+ if (messagesEndRef.current) {
+ messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
+ }
+ }, [messages])
+
+ // Focus input when modal opens
+ useEffect(() => {
+ if (open && inputRef.current) {
+ inputRef.current.focus()
+ }
+ }, [open])
+
+ // Handle send message
+ const handleSendMessage = async () => {
+ if (!copilotMessage.trim() || isLoading) return
+
+ try {
+ await onSendMessage(copilotMessage.trim())
+ setCopilotMessage('')
+
+ // Ensure input stays focused
+ if (inputRef.current) {
+ inputRef.current.focus()
+ }
+ } catch (error) {
+ logger.error('Failed to send message', error)
+ }
+ }
+
+ // Handle key press
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault()
+ handleSendMessage()
+ }
+ }
+
+ if (!open) return null
+
+ return (
+
+
+
+ {/* Header with title and close button */}
+
+
Documentation Copilot
+
+
+
+ {/* Messages container */}
+
+
+ {messages.length === 0 ? (
+
+
+
+
+
Welcome to Documentation Copilot
+
+ Ask me anything about Sim Studio features, workflows, tools, or how to get started.
+
+
+
+
Try asking:
+
+
+ "How do I create a workflow?"
+
+
+ "What tools are available?"
+
+
+ "How do I deploy my workflow?"
+
+
+
+
+
+ ) : (
+ messages.map((message) => (
+
+ ))
+ )}
+
+ {/* Loading indicator (shows only when loading) */}
+ {isLoading && (
+
+ )}
+
+
+
+
+
+ {/* Input area (fixed at bottom) */}
+
+
+
+
setCopilotMessage(e.target.value)}
+ onKeyDown={handleKeyPress}
+ placeholder='Ask about Sim Studio documentation...'
+ 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'
+ disabled={isLoading}
+ />
+
+
+
+
+
Ask questions about Sim Studio documentation and features
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
index e9e536e000..2627fc004c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/copilot.tsx
@@ -1,17 +1,21 @@
'use client'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
-import { Bot, Expand, Loader2, Send, User, X } from 'lucide-react'
+import { Bot, Loader2, Send, User } from 'lucide-react'
import { Button } from '@/components/ui/button'
-import { Dialog, DialogContent } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { createLogger } from '@/lib/logs/console-logger'
+import { CopilotModal } from './components/copilot-modal/copilot-modal'
const logger = createLogger('Copilot')
interface CopilotProps {
panelWidth: number
+ isFullscreen?: boolean
+ onFullscreenToggle?: (fullscreen: boolean) => void
+ fullscreenInput?: string
+ onFullscreenInputChange?: (input: string) => void
}
interface CopilotRef {
@@ -33,11 +37,16 @@ interface Message {
isStreaming?: boolean
}
-export const Copilot = forwardRef(({ panelWidth }, ref) => {
+export const Copilot = forwardRef(({
+ panelWidth,
+ isFullscreen = false,
+ onFullscreenToggle,
+ fullscreenInput = '',
+ onFullscreenInputChange
+}, ref) => {
const [messages, setMessages] = useState([])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
- const [isFullscreen, setIsFullscreen] = useState(false)
const scrollAreaRef = useRef(null)
const inputRef = useRef(null)
@@ -334,30 +343,159 @@ export const Copilot = forwardRef(({ panelWidth }, ref
)
}
+ // Convert messages for modal (role -> type)
+ const modalMessages = messages.map(msg => ({
+ id: msg.id,
+ content: msg.content,
+ type: msg.role as 'user' | 'assistant',
+ timestamp: msg.timestamp,
+ citations: msg.sources?.map((source, index) => ({
+ id: index + 1,
+ title: source.title,
+ url: source.link
+ }))
+ }))
+
+ // Handle modal message sending
+ const handleModalSendMessage = useCallback(async (message: string) => {
+ // Use the same handleSubmit logic but with the message parameter
+ const userMessage: Message = {
+ id: crypto.randomUUID(),
+ role: 'user',
+ content: message,
+ timestamp: new Date(),
+ }
+
+ const streamingMessage: Message = {
+ id: crypto.randomUUID(),
+ role: 'assistant',
+ content: '',
+ timestamp: new Date(),
+ isStreaming: true,
+ }
+
+ setMessages((prev) => [...prev, userMessage, streamingMessage])
+ setIsLoading(true)
+
+ try {
+ logger.info('Sending docs RAG query:', { query: message })
+
+ const response = await fetch('/api/docs/ask', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ query: message,
+ topK: 5,
+ stream: true,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`)
+ }
+
+ // Handle streaming response
+ if (response.headers.get('content-type')?.includes('text/event-stream')) {
+ const reader = response.body?.getReader()
+ const decoder = new TextDecoder()
+ let accumulatedContent = ''
+ let sources: any[] = []
+
+ if (!reader) {
+ throw new Error('Failed to get response reader')
+ }
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ const chunk = decoder.decode(value, { stream: true })
+ const lines = chunk.split('\n')
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ try {
+ const data = JSON.parse(line.slice(6))
+
+ if (data.type === 'metadata') {
+ sources = data.sources || []
+ } else if (data.type === 'content') {
+ accumulatedContent += data.content
+
+ // Update the streaming message with accumulated content
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === streamingMessage.id
+ ? { ...msg, content: accumulatedContent, sources }
+ : msg
+ )
+ )
+ } else if (data.type === 'done') {
+ // Finish streaming
+ setMessages((prev) =>
+ prev.map((msg) =>
+ msg.id === streamingMessage.id
+ ? { ...msg, isStreaming: false, sources }
+ : msg
+ )
+ )
+ } else if (data.type === 'error') {
+ throw new Error(data.error || 'Streaming error')
+ }
+ } catch (parseError) {
+ logger.warn('Failed to parse SSE data:', parseError)
+ }
+ }
+ }
+ }
+
+ logger.info('Received docs RAG response:', {
+ contentLength: accumulatedContent.length,
+ sourcesCount: sources.length,
+ })
+ } else {
+ // Fallback to non-streaming response
+ const data = await response.json()
+
+ const assistantMessage: Message = {
+ id: streamingMessage.id,
+ role: 'assistant',
+ content: data.response || 'Sorry, I could not generate a response.',
+ timestamp: new Date(),
+ sources: data.sources || [],
+ isStreaming: false,
+ }
+
+ setMessages((prev) => prev.slice(0, -1).concat(assistantMessage))
+ }
+ } catch (error) {
+ logger.error('Docs RAG error:', error)
+
+ const errorMessage: Message = {
+ id: streamingMessage.id,
+ role: 'assistant',
+ content:
+ 'Sorry, I encountered an error while searching the documentation. Please try again.',
+ timestamp: new Date(),
+ isStreaming: false,
+ }
+
+ setMessages((prev) => prev.slice(0, -1).concat(errorMessage))
+ } finally {
+ setIsLoading(false)
+ }
+ }, [])
+
return (
<>
- {/* Main Panel Content */}
{/* Header */}
-
-
-
-
-
Documentation Copilot
-
Ask questions about Sim Studio
-
-
-
-
+
+
+
+
Documentation Copilot
+
Ask questions about Sim Studio
@@ -420,95 +558,15 @@ export const Copilot = forwardRef
(({ panelWidth }, ref
{/* Fullscreen Modal */}
- {isFullscreen && (
-
- )}
+
onFullscreenToggle?.(open)}
+ copilotMessage={fullscreenInput}
+ setCopilotMessage={(message) => onFullscreenInputChange?.(message)}
+ messages={modalMessages}
+ onSendMessage={handleModalSendMessage}
+ isLoading={isLoading}
+ />
>
)
})
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
index 8398d5bb56..5a22b6905c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx
@@ -17,7 +17,9 @@ export function Panel() {
const [width, setWidth] = useState(336) // 84 * 4 = 336px (default width)
const [isDragging, setIsDragging] = useState(false)
const [chatMessage, setChatMessage] = useState('')
+ const [copilotMessage, setCopilotMessage] = useState('')
const [isChatModalOpen, setIsChatModalOpen] = useState(false)
+ const [isCopilotModalOpen, setIsCopilotModalOpen] = useState(false)
const copilotRef = useRef<{ clearMessages: () => void }>(null)
const isOpen = usePanelStore((state) => state.isOpen)
@@ -155,7 +157,14 @@ export function Panel() {
) : activeTab === 'console' ? (
) : activeTab === 'copilot' ? (
-
+
) : (
)}
@@ -190,6 +199,21 @@ export function Panel() {
Expand Chat
)}
+
+ {activeTab === 'copilot' && (
+
+
+
+
+ Expand Copilot
+
+ )}