From 3af1a6e100443491d8cd3a74309718da6f144140 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 8 Jul 2025 16:49:09 -0700 Subject: [PATCH] Lint --- .../copilot-modal/copilot-modal.tsx | 56 +- .../panel/components/copilot/copilot.tsx | 792 +++++++++--------- .../w/[workflowId]/components/panel/panel.tsx | 4 +- 3 files changed, 436 insertions(+), 416 deletions(-) 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 index d431a29858..84193d7a38 100644 --- 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 @@ -1,7 +1,7 @@ 'use client' -import { type KeyboardEvent, useEffect, useRef, useState } from 'react' -import { ArrowUp, Bot, User, X } from 'lucide-react' +import { type KeyboardEvent, useEffect, useRef } from 'react' +import { ArrowUp, Bot, X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { createLogger } from '@/lib/logs/console-logger' @@ -26,7 +26,10 @@ interface CopilotModalMessage { // Modal-specific message component function ModalCopilotMessage({ message }: CopilotModalMessage) { - const renderCitations = (text: string, citations?: Array<{ id: number; title: string; url: string }>) => { + const renderCitations = ( + text: string, + citations?: Array<{ id: number; title: string; url: string }> + ) => { if (!citations || citations.length === 0) return text let processedText = text @@ -52,12 +55,24 @@ function ModalCopilotMessage({ message }: CopilotModalMessage) { ) // Handle inline code - processedText = processedText.replace(/`([^`]+)`/g, '$1') + processedText = processedText.replace( + /`([^`]+)`/g, + '$1' + ) // Handle headers - processedText = processedText.replace(/^### (.*$)/gm, '

$1

') - processedText = processedText.replace(/^## (.*$)/gm, '

$1

') - processedText = processedText.replace(/^# (.*$)/gm, '

$1

') + processedText = processedText.replace( + /^### (.*$)/gm, + '

$1

' + ) + processedText = processedText.replace( + /^## (.*$)/gm, + '

$1

' + ) + processedText = processedText.replace( + /^# (.*$)/gm, + '

$1

' + ) // Handle bold processedText = processedText.replace(/\*\*(.*?)\*\*/g, '$1') @@ -94,8 +109,8 @@ function ModalCopilotMessage({ message }: CopilotModalMessage) {
-
@@ -115,14 +130,14 @@ interface CopilotModalProps { isLoading: boolean } -export function CopilotModal({ - open, - onOpenChange, - copilotMessage, +export function CopilotModal({ + open, + onOpenChange, + copilotMessage, setCopilotMessage, messages, onSendMessage, - isLoading + isLoading, }: CopilotModalProps) { const messagesEndRef = useRef(null) const messagesContainerRef = useRef(null) @@ -149,7 +164,7 @@ export function CopilotModal({ try { await onSendMessage(copilotMessage.trim()) setCopilotMessage('') - + // Ensure input stays focused if (inputRef.current) { inputRef.current.focus() @@ -210,10 +225,11 @@ export function CopilotModal({

Welcome to Documentation Copilot

- Ask me anything about Sim Studio features, workflows, tools, or how to get started. + Ask me anything about Sim Studio features, workflows, tools, or how to get + started.

-
+
Try asking:
@@ -230,9 +246,7 @@ export function CopilotModal({
) : ( - messages.map((message) => ( - - )) + messages.map((message) => ) )} {/* Loading indicator (shows only when loading) */} @@ -284,4 +298,4 @@ export function CopilotModal({
) -} \ 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 2627fc004c..69a0e215f0 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 @@ -37,52 +37,336 @@ interface Message { isStreaming?: boolean } -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 scrollAreaRef = useRef(null) - const inputRef = useRef(null) +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 scrollAreaRef = useRef(null) + const inputRef = useRef(null) - // Expose clear function to parent - useImperativeHandle( - ref, - () => ({ - clearMessages: () => { - setMessages([]) - logger.info('Copilot messages cleared') - }, - }), - [] - ) + // Expose clear function to parent + useImperativeHandle( + ref, + () => ({ + clearMessages: () => { + setMessages([]) + logger.info('Copilot messages cleared') + }, + }), + [] + ) - // Auto-scroll to bottom when new messages are added - useEffect(() => { - if (scrollAreaRef.current) { - const scrollContainer = scrollAreaRef.current.querySelector( - '[data-radix-scroll-area-viewport]' - ) - if (scrollContainer) { - scrollContainer.scrollTop = scrollContainer.scrollHeight + // Auto-scroll to bottom when new messages are added + useEffect(() => { + if (scrollAreaRef.current) { + const scrollContainer = scrollAreaRef.current.querySelector( + '[data-radix-scroll-area-viewport]' + ) + if (scrollContainer) { + scrollContainer.scrollTop = scrollContainer.scrollHeight + } } + }, [messages]) + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault() + if (!input.trim() || isLoading) return + + const userMessage: Message = { + id: crypto.randomUUID(), + role: 'user', + content: input.trim(), + timestamp: new Date(), + } + + const streamingMessage: Message = { + id: crypto.randomUUID(), + role: 'assistant', + content: '', + timestamp: new Date(), + isStreaming: true, + } + + setMessages((prev) => [...prev, userMessage, streamingMessage]) + const query = input.trim() + setInput('') + setIsLoading(true) + + try { + logger.info('Sending docs RAG query:', { query }) + + const response = await fetch('/api/docs/ask', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query, + 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) + } + }, + [input, isLoading] + ) + + const formatTimestamp = (date: Date) => { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) } - }, [messages]) - const handleSubmit = useCallback( - async (e: React.FormEvent) => { - e.preventDefault() - if (!input.trim() || isLoading) return + // Function to render content with inline hyperlinked citations and basic markdown + const renderContentWithCitations = (content: string, sources: Message['sources'] = []) => { + if (!content) return content + let processedContent = content + + // Replace {cite:1}, {cite:2}, etc. with clickable citation icons + processedContent = processedContent.replace(/\{cite:(\d+)\}/g, (match, num) => { + const sourceIndex = Number.parseInt(num) - 1 + const source = sources[sourceIndex] + + if (source) { + return `` + } + + return match + }) + + // Basic markdown processing for better formatting + processedContent = processedContent + // Handle code blocks + .replace( + /```(\w+)?\n([\s\S]*?)```/g, + '
$2
' + ) + // Handle inline code + .replace( + /`([^`]+)`/g, + '$1' + ) + // Handle bold text + .replace(/\*\*(.*?)\*\*/g, '$1') + // Handle italic text + .replace(/\*(.*?)\*/g, '$1') + // Handle headers + .replace(/^### (.*$)/gm, '

$1

') + .replace(/^## (.*$)/gm, '

$1

') + .replace(/^# (.*$)/gm, '

$1

') + // Handle unordered lists + .replace(/^\* (.*$)/gm, '
  • • $1
  • ') + .replace(/^- (.*$)/gm, '
  • • $1
  • ') + // Handle line breaks + .replace(/\n\n/g, '

    ') + .replace(/\n/g, '
    ') + + // Wrap in paragraph tags if not already wrapped + if ( + !processedContent.includes('

    ') && + !processedContent.includes('

    ') && + !processedContent.includes('

    ') && + !processedContent.includes('

    ') + ) { + processedContent = `

    ${processedContent}

    ` + } + + return processedContent + } + + const renderMessage = (message: Message) => { + if (message.isStreaming && !message.content) { + return ( +
    +
    + +
    +
    +
    + Copilot + + {formatTimestamp(message.timestamp)} + +
    +
    + + Searching documentation... +
    +
    +
    + ) + } + + return ( +
    +
    + {message.role === 'user' ? ( + + ) : ( + + )} +
    +
    +
    + + {message.role === 'user' ? 'You' : 'Copilot'} + + + {formatTimestamp(message.timestamp)} + + {message.isStreaming && ( +
    + + Responding... +
    + )} +
    + + {/* Enhanced content rendering with inline citations */} +
    +
    +
    + + {/* Streaming cursor */} + {message.isStreaming && message.content && ( + + )} +
    +
    + ) + } + + // 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: input.trim(), + content: message, timestamp: new Date(), } @@ -95,18 +379,16 @@ export const Copilot = forwardRef(({ } setMessages((prev) => [...prev, userMessage, streamingMessage]) - const query = input.trim() - setInput('') setIsLoading(true) try { - logger.info('Sending docs RAG query:', { query }) + 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, + query: message, topK: 5, stream: true, }), @@ -206,369 +488,93 @@ export const Copilot = forwardRef(({ } finally { setIsLoading(false) } - }, - [input, isLoading] - ) - - const formatTimestamp = (date: Date) => { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) - } - - // Function to render content with inline hyperlinked citations and basic markdown - const renderContentWithCitations = (content: string, sources: Message['sources'] = []) => { - if (!content) return content - - let processedContent = content - - // Replace {cite:1}, {cite:2}, etc. with clickable citation icons - processedContent = processedContent.replace(/\{cite:(\d+)\}/g, (match, num) => { - const sourceIndex = Number.parseInt(num) - 1 - const source = sources[sourceIndex] - - if (source) { - return `` - } - - return match - }) - - // Basic markdown processing for better formatting - processedContent = processedContent - // Handle code blocks - .replace( - /```(\w+)?\n([\s\S]*?)```/g, - '
    $2
    ' - ) - // Handle inline code - .replace( - /`([^`]+)`/g, - '$1' - ) - // Handle bold text - .replace(/\*\*(.*?)\*\*/g, '$1') - // Handle italic text - .replace(/\*(.*?)\*/g, '$1') - // Handle headers - .replace(/^### (.*$)/gm, '

    $1

    ') - .replace(/^## (.*$)/gm, '

    $1

    ') - .replace(/^# (.*$)/gm, '

    $1

    ') - // Handle unordered lists - .replace(/^\* (.*$)/gm, '
  • • $1
  • ') - .replace(/^- (.*$)/gm, '
  • • $1
  • ') - // Handle line breaks - .replace(/\n\n/g, '

    ') - .replace(/\n/g, '
    ') - - // Wrap in paragraph tags if not already wrapped - if ( - !processedContent.includes('

    ') && - !processedContent.includes('

    ') && - !processedContent.includes('

    ') && - !processedContent.includes('

    ') - ) { - processedContent = `

    ${processedContent}

    ` - } - - return processedContent - } - - const renderMessage = (message: Message) => { - if (message.isStreaming && !message.content) { - return ( -
    -
    - -
    -
    -
    - Copilot - - {formatTimestamp(message.timestamp)} - -
    -
    - - Searching documentation... -
    -
    -
    - ) - } + }, []) return ( -
    -
    - {message.role === 'user' ? ( - - ) : ( - - )} -
    -
    -
    - - {message.role === 'user' ? 'You' : 'Copilot'} - - - {formatTimestamp(message.timestamp)} - - {message.isStreaming && ( -
    - - Responding... + <> +
    + {/* Header */} +
    +
    + +
    +

    Documentation Copilot

    +

    Ask questions about Sim Studio

    - )} -
    - - {/* Enhanced content rendering with inline citations */} -
    -
    -
    - - {/* Streaming cursor */} - {message.isStreaming && message.content && ( - - )} -
    -
    - ) - } - - // 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 ( - <> -
    - {/* Header */} -
    -
    - -
    -

    Documentation Copilot

    -

    Ask questions about Sim Studio

    -
    - {/* Messages */} - - {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 */} + + {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(renderMessage)}
    - )} - + ) : ( +
    {messages.map(renderMessage)}
    + )} + - {/* Input */} -
    -
    - setInput(e.target.value)} - placeholder='Ask about Sim Studio documentation...' - disabled={isLoading} - className='flex-1' - autoComplete='off' - /> - -
    + {/* Input */} +
    +
    + setInput(e.target.value)} + placeholder='Ask about Sim Studio documentation...' + disabled={isLoading} + className='flex-1' + autoComplete='off' + /> + +
    +
    -
    - {/* Fullscreen Modal */} - onFullscreenToggle?.(open)} - copilotMessage={fullscreenInput} - setCopilotMessage={(message) => onFullscreenInputChange?.(message)} - messages={modalMessages} - onSendMessage={handleModalSendMessage} - isLoading={isLoading} - /> - - ) -}) + {/* Fullscreen Modal */} + onFullscreenToggle?.(open)} + copilotMessage={fullscreenInput} + setCopilotMessage={(message) => onFullscreenInputChange?.(message)} + messages={modalMessages} + onSendMessage={handleModalSendMessage} + isLoading={isLoading} + /> + + ) + } +) Copilot.displayName = 'Copilot' 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 5a22b6905c..273c55edd9 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 @@ -157,8 +157,8 @@ export function Panel() { ) : activeTab === 'console' ? ( ) : activeTab === 'copilot' ? ( -