brainkb-assistant
Version:
A configurable, standalone BrainKB Assistant that can be integrated into any website
1 lines • 233 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../node_modules/lucide-react/dist/esm/defaultAttributes.mjs","../node_modules/lucide-react/dist/esm/createLucideIcon.mjs","../node_modules/lucide-react/dist/esm/icons/bot.mjs","../node_modules/lucide-react/dist/esm/icons/brain.mjs","../node_modules/lucide-react/dist/esm/icons/check.mjs","../node_modules/lucide-react/dist/esm/icons/download.mjs","../node_modules/lucide-react/dist/esm/icons/map-pin.mjs","../node_modules/lucide-react/dist/esm/icons/pen-line.mjs","../node_modules/lucide-react/dist/esm/icons/send.mjs","../node_modules/lucide-react/dist/esm/icons/upload.mjs","../node_modules/lucide-react/dist/esm/icons/user.mjs","../node_modules/lucide-react/dist/esm/icons/x.mjs","../src/components/BrainKBAssistantWrapper.tsx","../src/components/BrainKBAssistant.tsx","../src/index.ts"],"sourcesContent":["/**\n * lucide-react v0.0.1 - ISC\n */\n\nvar defaultAttributes = {\n xmlns: \"http://www.w3.org/2000/svg\",\n width: 24,\n height: 24,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 2,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\"\n};\n\nexport { defaultAttributes as default };\n//# sourceMappingURL=defaultAttributes.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport { forwardRef, createElement } from 'react';\nimport defaultAttributes from './defaultAttributes.mjs';\n\nconst toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase();\nconst createLucideIcon = (iconName, iconNode) => {\n const Component = forwardRef(\n ({ color = \"currentColor\", size = 24, strokeWidth = 2, absoluteStrokeWidth, children, ...rest }, ref) => createElement(\n \"svg\",\n {\n ref,\n ...defaultAttributes,\n width: size,\n height: size,\n stroke: color,\n strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth,\n className: `lucide lucide-${toKebabCase(iconName)}`,\n ...rest\n },\n [\n ...iconNode.map(([tag, attrs]) => createElement(tag, attrs)),\n ...(Array.isArray(children) ? children : [children]) || []\n ]\n )\n );\n Component.displayName = `${iconName}`;\n return Component;\n};\nvar createLucideIcon$1 = createLucideIcon;\n\nexport { createLucideIcon$1 as default, toKebabCase };\n//# sourceMappingURL=createLucideIcon.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst Bot = createLucideIcon(\"Bot\", [\n [\n \"rect\",\n { width: \"18\", height: \"10\", x: \"3\", y: \"11\", rx: \"2\", key: \"1ofdy3\" }\n ],\n [\"circle\", { cx: \"12\", cy: \"5\", r: \"2\", key: \"f1ur92\" }],\n [\"path\", { d: \"M12 7v4\", key: \"xawao1\" }],\n [\"line\", { x1: \"8\", x2: \"8\", y1: \"16\", y2: \"16\", key: \"h6x27f\" }],\n [\"line\", { x1: \"16\", x2: \"16\", y1: \"16\", y2: \"16\", key: \"5lty7f\" }]\n]);\n\nexport { Bot as default };\n//# sourceMappingURL=bot.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst Brain = createLucideIcon(\"Brain\", [\n [\n \"path\",\n {\n d: \"M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z\",\n key: \"1mhkh5\"\n }\n ],\n [\n \"path\",\n {\n d: \"M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z\",\n key: \"1d6s00\"\n }\n ]\n]);\n\nexport { Brain as default };\n//# sourceMappingURL=brain.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst Check = createLucideIcon(\"Check\", [\n [\"polyline\", { points: \"20 6 9 17 4 12\", key: \"10jjfj\" }]\n]);\n\nexport { Check as default };\n//# sourceMappingURL=check.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst Download = createLucideIcon(\"Download\", [\n [\"path\", { d: \"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\", key: \"ih7n3h\" }],\n [\"polyline\", { points: \"7 10 12 15 17 10\", key: \"2ggqvy\" }],\n [\"line\", { x1: \"12\", x2: \"12\", y1: \"15\", y2: \"3\", key: \"1vk2je\" }]\n]);\n\nexport { Download as default };\n//# sourceMappingURL=download.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst MapPin = createLucideIcon(\"MapPin\", [\n [\n \"path\",\n { d: \"M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z\", key: \"2oe9fu\" }\n ],\n [\"circle\", { cx: \"12\", cy: \"10\", r: \"3\", key: \"ilqhr7\" }]\n]);\n\nexport { MapPin as default };\n//# sourceMappingURL=map-pin.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst PenLine = createLucideIcon(\"PenLine\", [\n [\"path\", { d: \"M12 20h9\", key: \"t2du7b\" }],\n [\n \"path\",\n { d: \"M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z\", key: \"ymcmye\" }\n ]\n]);\n\nexport { PenLine as default };\n//# sourceMappingURL=pen-line.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst Send = createLucideIcon(\"Send\", [\n [\"path\", { d: \"m22 2-7 20-4-9-9-4Z\", key: \"1q3vgg\" }],\n [\"path\", { d: \"M22 2 11 13\", key: \"nzbqef\" }]\n]);\n\nexport { Send as default };\n//# sourceMappingURL=send.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst Upload = createLucideIcon(\"Upload\", [\n [\"path\", { d: \"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\", key: \"ih7n3h\" }],\n [\"polyline\", { points: \"17 8 12 3 7 8\", key: \"t8dd8p\" }],\n [\"line\", { x1: \"12\", x2: \"12\", y1: \"3\", y2: \"15\", key: \"widbto\" }]\n]);\n\nexport { Upload as default };\n//# sourceMappingURL=upload.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst User = createLucideIcon(\"User\", [\n [\"path\", { d: \"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\", key: \"975kel\" }],\n [\"circle\", { cx: \"12\", cy: \"7\", r: \"4\", key: \"17ys0d\" }]\n]);\n\nexport { User as default };\n//# sourceMappingURL=user.mjs.map\n","/**\n * lucide-react v0.0.1 - ISC\n */\n\nimport createLucideIcon from '../createLucideIcon.mjs';\n\nconst X = createLucideIcon(\"X\", [\n [\"path\", { d: \"M18 6 6 18\", key: \"1bl5f8\" }],\n [\"path\", { d: \"m6 6 12 12\", key: \"d8bk6v\" }]\n]);\n\nexport { X as default };\n//# sourceMappingURL=x.mjs.map\n","'use client';\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport { MessageCircle, X, Send, Phone, Mail, Globe, ArrowRight, User, Bot, MapPin, FileText, Search, Maximize2, Minimize2, Move, Upload, Edit3, Code, File, Image, Download, Copy, Check, Brain, Settings, MessageSquare, Zap, Lightbulb, Database, Network, BarChart3, ChevronDown, ChevronUp, Star, BookOpen, Target, TrendingUp, Users } from 'lucide-react';\nimport '../styles/brainkb-assistant.css';\n\n// Configuration Types\nexport interface BrainKBConfig {\n // Branding\n branding?: {\n title?: string;\n subtitle?: string;\n logo?: string;\n primaryColor?: string;\n secondaryColor?: string;\n accentColor?: string;\n };\n \n // Features\n features?: {\n enableQuickActions?: boolean;\n enableFileUpload?: boolean;\n enableMessageEditing?: boolean;\n enableCodeRendering?: boolean;\n enableMarkdown?: boolean;\n enableContextDetection?: boolean;\n enableTypingIndicator?: boolean;\n enableExpandableWindow?: boolean;\n enableDragAndDrop?: boolean;\n enableKeyboardShortcuts?: boolean;\n };\n \n // API Configuration\n api?: {\n endpoint?: string;\n streaming?: boolean; // Enable/disable streaming for the endpoint\n type?: 'rest' | 'websocket';\n headers?: Record<string, string>;\n timeout?: number;\n retryAttempts?: number;\n auth?: {\n enabled?: boolean;\n type?: 'jwt' | 'bearer' | 'none';\n jwtEndpoint?: string;\n username?: string; // Can be email or username\n password?: string;\n emailField?: string; // 'email' or 'username' - defaults to 'email'\n tokenKey?: string;\n refreshTokenKey?: string;\n autoRefresh?: boolean;\n refreshThreshold?: number; // seconds before expiry to refresh\n };\n };\n \n // UI Configuration\n ui?: {\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n size?: {\n width?: string;\n height?: string;\n expandedWidth?: string;\n expandedHeight?: string;\n };\n theme?: 'light' | 'dark' | 'auto';\n language?: string;\n zIndex?: number;\n // Enhanced styling options\n styling?: {\n buttonColor?: string;\n buttonHoverColor?: string;\n chatBackground?: string;\n textColor?: string;\n borderColor?: string;\n shadowColor?: string;\n // Force positioning to override site CSS\n forcePosition?: boolean;\n // Custom CSS classes\n customClasses?: {\n container?: string;\n button?: string;\n chat?: string;\n header?: string;\n };\n };\n };\n \n // Quick Actions\n quickActions?: Array<{\n id: string;\n label: string;\n icon?: string;\n action: string;\n description?: string;\n url?: string; // New field for external links\n external?: boolean; // Whether to open in new tab\n }>;\n \n // Context Detection\n contextDetection?: {\n enabled?: boolean;\n selectors?: {\n title?: string;\n description?: string;\n keywords?: string[];\n };\n autoDetect?: boolean;\n };\n \n // Customization\n customization?: {\n welcomeMessage?: string;\n placeholderText?: string;\n errorMessage?: string;\n loadingMessage?: string;\n };\n \n // Callbacks\n callbacks?: {\n onMessageSend?: (message: string) => void;\n onResponseReceived?: (response: any) => void;\n onError?: (error: any) => void;\n onFileUpload?: (file: File) => void;\n onQuickAction?: (action: string) => void;\n };\n}\n\ninterface BrainKBAssistantWrapperProps {\n config?: BrainKBConfig;\n currentPage?: string;\n pageContext?: {\n title?: string;\n description?: string;\n keywords?: string[];\n entities?: string[];\n };\n isBrainKB?: boolean;\n}\n\ninterface ChatMessage {\n id: string;\n type: 'user' | 'assistant';\n content: string;\n timestamp: Date;\n sender?: string;\n isEditing?: boolean;\n}\n\ninterface QuickAction {\n id: string;\n label: string;\n icon: React.ReactNode;\n action: string;\n description?: string;\n url?: string; // New field for external links\n external?: boolean; // Whether to open in new tab\n}\n\n// API Service Class\n// Create a persistent API service instance\nlet apiServiceInstance: BrainKBAPIService | null = null;\n\nclass BrainKBAPIService {\n private config: BrainKBConfig;\n private sessionId: string | null = null;\n private authToken: string | null = null;\n private refreshToken: string | null = null;\n private tokenExpiry: number | null = null;\n\n constructor(config: BrainKBConfig) {\n this.config = config;\n }\n\n // JWT Authentication Methods\n private async authenticate(): Promise<string | null> {\n const { auth } = this.config.api || {};\n \n console.log('🔐 Starting authentication...', {\n enabled: auth?.enabled,\n type: auth?.type,\n hasJwtEndpoint: !!auth?.jwtEndpoint,\n hasUsername: !!auth?.username,\n hasPassword: !!auth?.password\n });\n \n if (!auth?.enabled || auth.type !== 'jwt') {\n console.log('❌ JWT authentication not enabled or wrong type');\n return null;\n }\n\n // Check if we have a valid token\n if (this.authToken && this.tokenExpiry && Date.now() < this.tokenExpiry) {\n console.log('✅ Using existing valid token');\n return this.authToken;\n }\n\n // Check if we have a refresh token and auto-refresh is enabled\n if (this.refreshToken && auth.autoRefresh) {\n try {\n console.log('🔄 Attempting token refresh...');\n return await this.refreshAuthToken();\n } catch (error) {\n console.warn('Failed to refresh token, will re-authenticate:', error);\n }\n }\n\n // Perform initial authentication\n console.log('🔑 Performing initial authentication...');\n return await this.performAuthentication();\n }\n\n private async performAuthentication(): Promise<string | null> {\n const { auth } = this.config.api || {};\n \n if (!auth?.jwtEndpoint || !auth.username || !auth.password) {\n console.warn('JWT authentication enabled but missing required credentials');\n console.log('🔍 Credentials check:', {\n hasJwtEndpoint: !!auth?.jwtEndpoint,\n hasUsername: !!auth?.username,\n hasPassword: !!auth?.password\n });\n return null;\n }\n\n try {\n console.log('🌐 Making authentication request to:', auth.jwtEndpoint);\n \n // Build request body - support both 'username' and 'email' fields\n const requestBody: any = {\n password: auth.password,\n };\n \n // Use email field by default, or override with emailField config\n const emailField = auth.emailField || 'email';\n requestBody[emailField] = auth.username;\n \n console.log(`📧 Using ${emailField} field for authentication`);\n \n const response = await fetch(auth.jwtEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(requestBody),\n });\n\n console.log('📡 Authentication response status:', response.status);\n\n if (!response.ok) {\n const errorText = await response.text();\n console.error('❌ Authentication failed:', {\n status: response.status,\n statusText: response.statusText,\n errorText\n });\n throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);\n }\n\n const data = await response.json();\n console.log('📦 Authentication response received');\n \n // Extract tokens based on configuration\n const tokenKey = auth.tokenKey || 'access_token' || 'token';\n const refreshTokenKey = auth.refreshTokenKey || 'refresh_token';\n \n this.authToken = data[tokenKey];\n this.refreshToken = data[refreshTokenKey];\n \n // Calculate token expiry (default to 1 hour if not provided)\n const expiresIn = data.expires_in || 3600;\n this.tokenExpiry = Date.now() + (expiresIn * 1000);\n\n console.log('🔐 JWT authentication successful');\n return this.authToken;\n } catch (error) {\n console.error('❌ JWT authentication failed:', error);\n return null;\n }\n }\n\n private async refreshAuthToken(): Promise<string | null> {\n const { auth } = this.config.api || {};\n \n if (!auth?.jwtEndpoint || !this.refreshToken) {\n return null;\n }\n\n try {\n const response = await fetch(auth.jwtEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n refresh_token: this.refreshToken,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);\n }\n\n const data = await response.json();\n \n const tokenKey = auth.tokenKey || 'access_token' || 'token';\n const refreshTokenKey = auth.refreshTokenKey || 'refresh_token';\n \n this.authToken = data[tokenKey];\n this.refreshToken = data[refreshTokenKey];\n \n const expiresIn = data.expires_in || 3600;\n this.tokenExpiry = Date.now() + (expiresIn * 1000);\n\n console.log('🔄 JWT token refreshed successfully');\n return this.authToken;\n } catch (error) {\n console.error('❌ JWT token refresh failed:', error);\n // Clear invalid tokens\n this.authToken = null;\n this.refreshToken = null;\n this.tokenExpiry = null;\n return null;\n }\n }\n\n private async getAuthHeaders(): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Add JWT token if available\n const token = await this.authenticate();\n console.log('🔐 Authentication result:', token ? 'Token obtained' : 'No token');\n \n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n console.log('🔑 Authorization header set:', `Bearer ${token.substring(0, 20)}...`);\n } else {\n console.warn('⚠️ No JWT token available for request');\n }\n\n // Add any additional headers from config\n if (this.config.api?.headers) {\n Object.assign(headers, this.config.api.headers);\n }\n\n return headers;\n }\n\n async sendMessage(message: string, context?: any, onStream?: (chunk: string) => void): Promise<any> {\n const { api } = this.config;\n \n if (api?.endpoint) {\n // Check if streaming is enabled via config or query parameter\n const url = new URL(api.endpoint);\n const isStreamingFromQuery = url.searchParams.get('stream') === 'true';\n const isStreaming = api.streaming || isStreamingFromQuery;\n \n if (isStreaming) {\n return this.sendStreamingMessage(message, context, onStream);\n } else {\n return this.sendRESTMessage(message, context);\n }\n }\n \n // Fallback to local response\n return this.generateLocalResponse(message, context);\n }\n\n private async sendStreamingMessage(message: string, context?: any, onStream?: (chunk: string) => void): Promise<any> {\n try {\n const requestBody = {\n message,\n session_id: this.sessionId,\n currentPage: context?.currentPage,\n pageContext: context?.pageContext,\n pageContent: context?.pageContent,\n selectedPageContent: context?.selectedPageContent,\n chatHistory: context?.chatHistory,\n timestamp: new Date().toISOString(),\n };\n\n const endpoint = this.config.api!.endpoint!;\n \n console.log('📤 Sending streaming request to API:', {\n endpoint: endpoint,\n sessionId: this.sessionId,\n messageLength: message.length,\n hasContext: !!context\n });\n\n const headers = await this.getAuthHeaders();\n headers['Accept'] = 'text/event-stream';\n \n const response = await fetch(endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(requestBody),\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n throw new Error('No response body reader available');\n }\n\n const decoder = new TextDecoder();\n let fullContent = '';\n let sessionId = this.sessionId;\n\n while (true) {\n const { done, value } = await reader.read();\n \n if (done) break;\n \n const chunk = decoder.decode(value, { stream: true });\n const lines = chunk.split('\\n');\n \n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6); // Remove 'data: ' prefix\n \n if (data === '[DONE]') {\n // Stream ended\n break;\n }\n \n try {\n const parsed = JSON.parse(data);\n \n // Handle session ID\n if (parsed.session_id) {\n sessionId = parsed.session_id;\n this.sessionId = sessionId;\n console.log('🔗 Session ID received and stored:', sessionId);\n }\n \n // Handle content chunks\n if (parsed.content) {\n fullContent += parsed.content;\n onStream?.(parsed.content);\n }\n \n // Handle other fields\n if (parsed.type === 'error') {\n console.error('Streaming API Error:', parsed.error);\n throw new Error(parsed.error || 'Streaming API error');\n }\n \n } catch (parseError) {\n // If it's not JSON, treat as plain text content\n if (data.trim()) {\n fullContent += data;\n onStream?.(data);\n }\n }\n }\n }\n }\n\n return {\n content: fullContent,\n session_id: sessionId,\n parsed_content: this.parseResponse(fullContent)\n };\n \n } catch (error) {\n console.error('Streaming API Error:', error);\n return this.generateLocalResponse(message, context);\n }\n }\n\n private async sendRESTMessage(message: string, context?: any): Promise<any> {\n try {\n // Try simplified request first, then fallback to full request\n let requestBody = {\n message,\n session_id: this.sessionId,\n };\n\n // If the first request fails, try with more context\n if (context) {\n requestBody = {\n message,\n session_id: this.sessionId,\n currentPage: context?.currentPage,\n pageContext: context?.pageContext,\n pageContent: context?.pageContent,\n selectedPageContent: context?.selectedPageContent,\n chatHistory: context?.chatHistory,\n timestamp: new Date().toISOString(),\n // Add action-specific fields if they exist\n ...(context?.action && { action: context.action }),\n ...(context?.actionLabel && { actionLabel: context.actionLabel }),\n ...(context?.actionDescription && { actionDescription: context.actionDescription }),\n };\n }\n\n console.log('📤 Sending request to API:', {\n endpoint: this.config.api!.endpoint,\n sessionId: this.sessionId,\n messageLength: message.length,\n hasContext: !!context,\n requestBody: requestBody\n });\n\n const headers = await this.getAuthHeaders();\n \n const response = await fetch(this.config.api!.endpoint!, {\n method: 'POST',\n headers,\n body: JSON.stringify(requestBody),\n });\n \n if (!response.ok) {\n const errorText = await response.text();\n console.error('❌ API Error:', {\n status: response.status,\n statusText: response.statusText,\n errorText: errorText\n });\n \n // Try to parse error response\n let errorData;\n try {\n errorData = JSON.parse(errorText);\n } catch {\n errorData = { detail: errorText };\n }\n \n throw new Error(`API Error ${response.status}: ${JSON.stringify(errorData)}`);\n }\n \n const responseData = await response.json();\n \n // Debug the response data\n console.log('📥 API Response Data:', responseData);\n console.log('📥 Response Data Type:', typeof responseData);\n console.log('📥 Response Data Keys:', Object.keys(responseData || {}));\n \n // Store session ID from response for future requests\n if (responseData.session_id) {\n this.sessionId = responseData.session_id;\n console.log('🔗 Session ID received and stored:', this.sessionId);\n }\n \n // Parse the response content dynamically\n const parsedContent = this.parseResponse(responseData);\n \n return {\n ...responseData,\n parsed_content: parsedContent\n };\n } catch (error) {\n console.error('REST API Error:', error);\n return this.generateLocalResponse(message, context);\n }\n }\n\n private generateLocalResponse(message: string, context?: any): any {\n // Generate contextual response based on message content and page context\n const responses = {\n greeting: \"Hello! I'm your BrainKB Assistant. How can I help you today?\",\n question: \"I understand your question. Let me help you find the information you need.\",\n knowledge: \"I can help you explore the knowledge base and find relevant information.\",\n default: \"I'm here to help! What would you like to know about?\"\n };\n\n const lowerMessage = message.toLowerCase();\n \n // If selected page content is available, provide more contextual response\n if (context?.selectedPageContent) {\n const selectedContent = context.selectedPageContent;\n \n return {\n content: `I can see you've selected specific content from the page (${selectedContent.length} characters). I can help you analyze this content and answer questions about it. What would you like to know about the selected text?`\n };\n }\n \n // If page content is available, provide more contextual response\n if (context?.pageContent) {\n const pageContent = context.pageContent;\n const pageContext = context.pageContext;\n \n return {\n content: `I can see you're on the ${pageContext?.title || 'current page'}. I have access to the page content and can help you with questions about what's displayed here. What would you like to know about this page?`\n };\n }\n \n // If page context is available but no content\n if (context?.pageContext) {\n return {\n content: `I can help you with questions about ${context.pageContext.title || 'this page'}. What would you like to know?`\n };\n }\n\n // Default responses based on message content\n if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {\n return { content: responses.greeting };\n } else if (lowerMessage.includes('?')) {\n return { content: responses.question };\n } else if (lowerMessage.includes('knowledge') || lowerMessage.includes('data')) {\n return { content: responses.knowledge };\n }\n \n return { content: responses.default };\n }\n\n // Dynamic response parser that handles any API response structure\n private parseResponse(response: any): string {\n console.log('🔍 Parsing response:', response);\n console.log('🔍 Response type:', typeof response);\n \n // If response is already a string, return it\n if (typeof response === 'string') {\n console.log('✅ Response is string, returning as-is');\n return response;\n }\n \n // If response is null or undefined, return default message\n if (response === null || response === undefined) {\n console.log('⚠️ Response is null/undefined, returning default');\n return 'I received your message but got an empty response. Please try again.';\n }\n \n // If response is an object, try to extract content\n if (typeof response === 'object') {\n console.log('🔍 Response keys:', Object.keys(response));\n \n // Common response content field names\n const contentFields = [\n 'content', 'message', 'response', 'text', 'data', 'answer', 'reply',\n 'result', 'output', 'body', 'value', 'description', 'summary'\n ];\n \n // Try to find content in common fields\n for (const field of contentFields) {\n if (response[field] !== undefined && response[field] !== null) {\n console.log(`✅ Found content in field '${field}':`, response[field]);\n return this.stringifyIfNeeded(response[field]);\n }\n }\n \n // If no content field found, check if the object itself is the content\n // (e.g., if the API returns the message directly as an object)\n if (response.toString && response.toString() !== '[object Object]') {\n console.log('✅ Using response.toString():', response.toString());\n return response.toString();\n }\n \n // If it's an array, try to join it or take the first element\n if (Array.isArray(response)) {\n if (response.length === 0) {\n console.log('⚠️ Response is empty array');\n return 'I received an empty response. Please try again.';\n }\n \n // If array has one element, use it\n if (response.length === 1) {\n console.log('✅ Using first array element:', response[0]);\n return this.stringifyIfNeeded(response[0]);\n }\n \n // If array has multiple elements, join them\n console.log('✅ Joining array elements');\n return response.map(item => this.stringifyIfNeeded(item)).join('\\n\\n');\n }\n \n // Last resort: stringify the entire response\n console.log('⚠️ No content field found, stringifying entire response');\n return JSON.stringify(response, null, 2);\n }\n \n // For any other type, convert to string\n console.log('✅ Converting response to string:', String(response));\n return String(response);\n }\n \n // Helper method to stringify objects if needed\n private stringifyIfNeeded(value: any): string {\n if (typeof value === 'string') {\n return value;\n }\n \n if (typeof value === 'object' && value !== null) {\n // If it's a simple object with a few properties, format it nicely\n const keys = Object.keys(value);\n if (keys.length <= 5) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n }\n // For complex objects, just stringify\n return JSON.stringify(value, null, 2);\n }\n \n return String(value);\n }\n}\n\n// Code Block Component with Syntax Highlighting\nconst CodeBlock: React.FC<{ code: string; language?: string }> = ({ code, language = 'javascript' }) => {\n const copyToClipboard = async () => {\n try {\n await navigator.clipboard.writeText(code);\n } catch (err) {\n console.error('Failed to copy: ', err);\n }\n };\n\n return (\n <div className=\"bg-gray-900 rounded-lg overflow-hidden border border-gray-700 my-2\" style={{ \n maxWidth: '100%',\n width: '100%'\n }}>\n <div className=\"flex items-center justify-between px-4 py-2 bg-gray-800 border-b border-gray-700\">\n <span className=\"text-xs font-medium text-gray-300 uppercase\">{language}</span>\n <button\n onClick={copyToClipboard}\n className=\"text-xs text-gray-400 hover:text-white transition-colors\"\n title=\"Copy to clipboard\"\n >\n Copy\n </button>\n </div>\n <pre \n className=\"p-4 overflow-x-auto\" \n style={{ \n fontSize: '13px', \n lineHeight: '1.4',\n maxWidth: '100%',\n width: '100%',\n wordWrap: 'break-word',\n overflowWrap: 'break-word'\n }}\n >\n <code className=\"text-gray-100\" style={{ \n wordWrap: 'break-word', \n overflowWrap: 'break-word',\n width: '100%'\n }}>{code}</code>\n </pre>\n </div>\n );\n};\n\n// Helper function to extract table data for download (standalone)\nconst extractTableDataForDownload = (headers: string[], rows: string[]): any => {\n const dataRows: string[][] = [];\n \n rows.forEach((row, index) => {\n if (index === 0) return; // Skip header row\n const cells = row.split('|').filter(cell => cell.trim());\n if (cells.length > 0) {\n dataRows.push(cells.map(cell => cell.trim()));\n }\n });\n \n return {\n headers: headers,\n rows: dataRows\n };\n};\n\n// Enhanced Table Component with Download\nconst EnhancedTable: React.FC<{ \n headers: string[]; \n rows: string[]; \n tableId: string;\n}> = ({ headers, rows, tableId }) => {\n const [tableData] = useState(() => extractTableDataForDownload(headers, rows));\n \n const handleDownload = (format: string) => {\n console.log(`📥 Downloading table ${tableId} as ${format}`);\n // You can add analytics or tracking here\n };\n\n return (\n <div className=\"table-container\" style={{\n background: 'white',\n borderRadius: '16px',\n border: '1px solid #e2e8f0',\n overflow: 'hidden',\n margin: '16px 0',\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',\n maxWidth: '100%',\n boxSizing: 'border-box',\n width: '100%'\n }}>\n <div className=\"table-header\" style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '24px 28px 20px 28px',\n borderBottom: '2px solid #e2e8f0',\n background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',\n position: 'relative',\n minHeight: '70px',\n boxSizing: 'border-box'\n }}>\n <div className=\"table-title\" style={{\n fontSize: '18px',\n color: '#1e293b',\n fontWeight: '700',\n display: 'flex',\n alignItems: 'center',\n gap: '12px',\n textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',\n flex: '1',\n wordWrap: 'break-word',\n overflowWrap: 'break-word',\n maxWidth: '100%'\n }}>\n 📊 Query Results Table\n </div>\n <div className=\"download-container\" style={{\n position: 'relative',\n display: 'inline-block',\n zIndex: 10\n }}>\n <DownloadDataButton \n data={tableData}\n filename={`query-results-${tableId}`}\n onDownload={handleDownload}\n />\n </div>\n </div>\n <div style={{\n overflowX: 'auto',\n maxWidth: '100%',\n width: '100%'\n }}>\n <table style={{\n width: '100%',\n borderCollapse: 'collapse',\n fontSize: '13px',\n lineHeight: '1.4',\n tableLayout: 'fixed',\n wordWrap: 'break-word',\n overflowWrap: 'break-word',\n minWidth: '100%'\n }}>\n <thead style={{\n background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',\n borderBottom: '2px solid #e2e8f0'\n }}>\n <tr>\n {headers.map((header, index) => (\n <th key={index} style={{\n padding: '12px 16px',\n textAlign: 'left',\n fontWeight: '600',\n color: '#1e293b',\n fontSize: '13px',\n borderBottom: '1px solid #e2e8f0',\n background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',\n wordWrap: 'break-word',\n overflowWrap: 'break-word',\n maxWidth: '120px',\n minWidth: '80px',\n whiteSpace: 'normal',\n verticalAlign: 'top'\n }}>\n {header}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, rowIndex) => {\n if (rowIndex === 0) return null; // Skip header row\n const cells = row.split('|').filter(cell => cell.trim());\n if (cells.length > 0) {\n return (\n <tr key={rowIndex} style={{\n borderBottom: '1px solid #f1f5f9',\n transition: 'background-color 0.2s ease',\n backgroundColor: rowIndex % 2 === 0 ? 'transparent' : '#fafbfc'\n }}>\n {cells.map((cell, cellIndex) => {\n const cellContent = cell.trim();\n let cellStyle: React.CSSProperties = {\n padding: '12px 16px',\n color: '#374151',\n fontSize: '12px',\n lineHeight: '1.4',\n verticalAlign: 'top',\n wordWrap: 'break-word',\n overflowWrap: 'break-word',\n hyphens: 'auto',\n maxWidth: '120px',\n minWidth: '80px',\n whiteSpace: 'normal'\n };\n \n // Apply special styling based on content type\n if (cellIndex === 0 && /^\\d+$/.test(cellContent)) {\n // Index cell styling\n cellStyle = {\n ...cellStyle,\n fontWeight: '600',\n color: '#1e293b',\n textAlign: 'center',\n background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',\n borderRadius: '8px',\n padding: '6px 10px',\n minWidth: '40px',\n display: 'inline-block',\n border: '1px solid #bae6fd',\n fontSize: '12px'\n };\n } else if (cellContent.includes('ncbitaxon:') || cellContent.includes('http') || cellContent.includes('://') || cellContent.includes('GCF_') || cellContent.includes('GCA_')) {\n // URI cell styling\n cellStyle = {\n ...cellStyle,\n fontFamily: \"'Monaco', 'Menlo', 'Ubuntu Mono', monospace\",\n fontSize: '11px',\n color: '#0369a1',\n background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',\n borderRadius: '6px',\n padding: '4px 8px',\n border: '1px solid #bae6fd',\n wordBreak: 'break-all',\n overflowWrap: 'break-word',\n maxWidth: '100%'\n };\n }\n \n return (\n <td key={cellIndex} style={cellStyle}>\n {cellContent}\n </td>\n );\n })}\n </tr>\n );\n }\n return null;\n })}\n </tbody>\n </table>\n </div>\n </div>\n );\n};\n\n// Markdown Renderer Component\nconst MarkdownRenderer: React.FC<{ content: string }> = ({ content }) => {\n const renderContent = (text: string): React.ReactNode[] => {\n const elements: React.ReactNode[] = [];\n let currentIndex = 0;\n \n // Split by code blocks first\n const parts = text.split(/(```[\\s\\S]*?```)/);\n \n for (const part of parts) {\n if (part.startsWith('```')) {\n // Extract language and code\n const match = part.match(/```(\\w+)?\\n([\\s\\S]*?)```/);\n if (match) {\n const language = match[1] || 'text';\n const code = match[2];\n \n elements.push(\n <CodeBlock \n key={currentIndex++} \n code={code} \n language={language} \n />\n );\n continue;\n }\n }\n \n // Enhanced markdown processing with better structure handling\n let processedText = part;\n \n // Clean up repeated content patterns (common in AI responses)\n processedText = processedText\n .replace(/(Certainly! Let's break down and analyze[^.]*\\.)/g, '')\n .replace(/(Based on your data, the following entities are central:)/g, '### Key Entities Identified')\n .replace(/(The relationships between these entities can be visualized as follows:)/g, '### Relationships and Data Flow')\n .replace(/(In a knowledge graph, these entities and relationships might look like:)/g, '### Knowledge Graph Representation')\n .replace(/(Would you like a visual diagram[^?]*\\?)/g, '');\n \n // Format query result summaries\n processedText = processedText\n .replace(/Here is a (?:formatted summary|clear and concise formatting) of your query results:/g, '<div class=\"query-summary\"><strong>📊 Query Results Summary</strong></div>')\n .replace(/Variable:\\s*(\\w+)/g, '<div class=\"query-summary\">Variable: <strong>$1</strong></div>')\n .replace(/Results:\\s*(\\d+)\\s*entries?/g, '<div class=\"query-summary\">Results: <strong>$1 entries</strong></div>');\n \n // Enhanced table detection and formatting\n const lines = processedText.split('\\n');\n const processedLines: string[] = [];\n let inTable = false;\n let tableRows: string[] = [];\n let tableHeaders: string[] = [];\n \n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n \n // Detect table structure (lines with | and ---)\n if (line.includes('|') && line.trim().startsWith('|') && line.trim().endsWith('|')) {\n if (!inTable) {\n inTable = true;\n tableRows = [];\n tableHeaders = [];\n }\n \n // Extract headers from first row\n if (tableHeaders.length === 0) {\n const headerCells = line.split('|').filter(cell => cell.trim());\n tableHeaders = headerCells.map(cell => cell.trim());\n }\n \n // Extract data cells\n const cells = line.split('|').filter(cell => cell.trim());\n if (cells.length > 0) {\n tableRows.push(line);\n }\n } else if (line.includes('---') && inTable) {\n // Skip separator lines\n continue;\n } else if (inTable && !line.includes('|')) {\n // End of table\n inTable = false;\n \n // Convert table to React component\n if (tableRows.length > 0) {\n const tableId = `table-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n elements.push(\n <EnhancedTable \n key={currentIndex++}\n headers={tableHeaders}\n rows={tableRows}\n tableId={tableId}\n />\n );\n }\n \n // Add the current line as regular text\n if (line.trim()) {\n processedLines.push(line);\n }\n } else if (inTable) {\n // Continue building table\n tableRows.push(line);\n } else {\n // Regular text processing\n processedLines.push(line);\n }\n }\n \n // Handle table at end of content\n if (inTable && tableRows.length > 0) {\n const tableId = `table-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n elements.push(\n <EnhancedTable \n key={currentIndex++}\n headers={tableHeaders}\n rows={tableRows}\n tableId={tableId}\n />\n );\n }\n \n processedText = processedLines.join('\\n');\n \n // Headers (h1-h6) - improved regex to handle edge cases\n processedText = processedText\n .replace(/^#{6}\\s+(.+?)(?:\\n|$)/gm, '<h6 style=\"font-size: 1rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;\">$1</h6>')\n .replace(/^#{5}\\s+(.+?)(?:\\n|$)/gm, '<h5 style=\"font-size: 1.125rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;\">$1</h5>')\n .replace(/^#{4}\\s+(.+?)(?:\\n|$)/gm, '<h4 style=\"font-size: 1.25rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;\">$1</h4>')\n .replace(/^#{3}\\s+(.+?)(?:\\n|$)/gm, '<h3 style=\"font-size: 1.5rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;\">$1</h3>')\n .replace(/^#{2}\\s+(.+?)(?:\\n|$)/gm, '<h2 style=\"font-size: 1.875rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;\">$1</h2>')\n .replace(/^#{1}\\s+(.+?)(?:\\n|$)/gm, '<h1 style=\"font-size: 2.25rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;\">$1</h1>');\n \n // Bold and italic - improved to handle nested patterns\n processedText = processedText\n .replace(/\\*\\*(.*?)\\*\\*/g, '<strong style=\"font-weight: 600;\">$1</strong>')\n .replace(/\\*(.*?)\\*/g, '<em style=\"font-style: italic;\">$1</em>')\n .replace(/__(.*?)__/g, '<strong style=\"font-weight: 600;\">$1</strong>')\n .replace(/_(.*?)_/g, '<em style=\"font-style: italic;\">$1</em>');\n \n // Inline code\n processedText = processedText\n .replace(/`([^`]+)`/g, '<code style=\"background-color: #f3f4f6; padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-size: 0.875rem; font-family: monospace; color: #1f2937;\">$1</code>');\n \n // Links\n processedText = processedText\n .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" style=\"color: #3b82f6; text-decoration: underline;\" target=\"_blank\" rel=\"noopener noreferrer\">$1</a>');\n \n // Lists - improved to handle numbered and bulleted lists better\n processedText = processedText\n .replace(/^\\s*[-*+]\\s+(.+?)(?:\\n|$)/gm, '<li style=\"margin: 0.25rem 0; padding-left: 0.5rem;\">$1</li>')\n .replace(/^\\s*\\d+\\.\\s+(.+?)(?:\\n|$)/gm, '<li style=\"margin: 0.25rem 0; padding-left: 0.5rem;\">$1</li>');\n \n // Wrap consecutive list items in ul/ol\n processedText = processedText\n .replace(/(<li[^>]*>.*?<\\/li>)(?:\\s*<li[^>]*>.*?<\\/li>)*/gs, (match) => {\n return `<ul style=\"margin: 0.5rem 0; padding-left: 1.5rem; list-style-type: disc;\">${match}</ul>`;\n });\n \n // Blockquotes\n processedText = processedText\n .replace(/^>\\s+(.+?)(?:\\n|$)/gm, '<blockquote style=\"border-left: 4px solid #e5e7eb; padding-left: 1rem; margin: 1rem 0; color: #6b7280; font-style: italic;\">$1</blockquote>');\n \n // Horizontal rules\n processedText = processedText\n .replace(/^---+$/gm, '<hr style=\"border: none; border-top: 1px solid #e5e7eb; margin: 1rem 0;\">');\n \n // Paragraphs - wrap text in paragraphs for better structure\n processedText = processedText\n .split('\\n\\n')\n .map(paragraph => {\n if (paragraph.trim() && !paragraph.match(/^<[^>]+>/) && !paragraph.match(/^#{1,6}\\s/)) {\n return `<p style=\"margin: 0.75rem 0; line-height: 1.6;\">${paragraph.trim()}</p>`;\n }\n return paragraph;\n })\n .join('\\n\\n');\n \n // Line breaks within paragraphs\n processedText = processedText\n .replace(/\\n(?!\\n)/g, '<br>');\n \n // Add processed text as HTML element\n if (processedText.trim()) {\n elements.push(\n <div \n key={currentIndex++} \n style={{ \n fontSize: '14px', \n lineHeight: '1.6',\n wordWrap: 'break-word',\n overflowWrap: 'break-word',\n maxWidth: '100%',\n width: '100%',\n color: '#374151'\n }}\n dangerouslySetInnerHTML={{ __html: processedText }} \n />\n );\n }\n }\n \n return elements;\n };\n\n // Helper function to extract table data for download\n const extractTableData = (headers: string[], rows: string[]): any => {\n const dataRows: string[][] = [];\n \n rows.forEach((row, index) => {\n if (index === 0) return; // Skip header row\n const cells = row.split('|').filter(cell => cell.trim());\n if (cells.length > 0) {\n dataRows.push(cells.map(cell => cell.trim()));\n }\n });\n \n return {\n headers: headers,\n rows: dataRows\n };\n };\n\n // Helper function to convert table rows to HTML\n const convertTableToHtml = (headers: string[], rows: string[]): string => {\n let html = '<div class=\"table-container\">';\n html += '<table>';\n \n // Header row\n html += '<thead>';\n html += '<tr>';\n headers.forEach(header => {\n html += `<th>${header}</th>`;\n });\n html += '</tr>';\n html += '</thead>';\n \n // Data rows\n html += '<tbody>';\n rows.forEach((row, index) => {\n if (index === 0) return; // Skip header row\n const cells = row.split('|').filter(cell => cell.trim());\n if (cells.length > 0) {\n html += '<tr>';\n cells.forEach((cell, cellIndex) => {\n const cellContent = cell.trim();\n let cellClass = '';\n \n // Apply special styling based on content type\n if (cellIndex === 0 && /^\\d+$/.test(cellContent)) {\n // Index column\n cellClass = 'index-cell';\n } else if (cellContent.includes('ncbitaxon:') || cellContent.includes('http') || cellContent.includes('://')) {\n // URI column\n cellClass = 'uri-cell';\n } else if (cellContent.includes('GCF_') || cellContent.includes('GCA_')) {\n // Assembly identifiers\n cellClass = 'uri-cell';\n }\n \n html += `<td class=\"${cellClass}\">${cellContent}</td>`;\n });\n html += '</tr>';\n }\n });\n html += '</tbody>';\n html += '</table>';\n html += '</div>';\n \n return html;\n };\n\n return (\n <div \n style={{ \n wordWrap: 'break-word', \n