UNPKG

brainkb-assistant

Version:

A configurable, standalone BrainKB Assistant that can be integrated into any website

1,060 lines (1,022 loc) 136 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var jsxRuntime = require('react/jsx-runtime'); var React = require('react'); var ReactDOM = require('react-dom'); /** * lucide-react v0.0.1 - ISC */ var defaultAttributes = { xmlns: "http://www.w3.org/2000/svg", width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }; /** * lucide-react v0.0.1 - ISC */ const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); const createLucideIcon = (iconName, iconNode) => { const Component = React.forwardRef( ({ color = "currentColor", size = 24, strokeWidth = 2, absoluteStrokeWidth, children, ...rest }, ref) => React.createElement( "svg", { ref, ...defaultAttributes, width: size, height: size, stroke: color, strokeWidth: absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth, className: `lucide lucide-${toKebabCase(iconName)}`, ...rest }, [ ...iconNode.map(([tag, attrs]) => React.createElement(tag, attrs)), ...(Array.isArray(children) ? children : [children]) || [] ] ) ); Component.displayName = `${iconName}`; return Component; }; var createLucideIcon$1 = createLucideIcon; /** * lucide-react v0.0.1 - ISC */ const Bot = createLucideIcon$1("Bot", [ [ "rect", { width: "18", height: "10", x: "3", y: "11", rx: "2", key: "1ofdy3" } ], ["circle", { cx: "12", cy: "5", r: "2", key: "f1ur92" }], ["path", { d: "M12 7v4", key: "xawao1" }], ["line", { x1: "8", x2: "8", y1: "16", y2: "16", key: "h6x27f" }], ["line", { x1: "16", x2: "16", y1: "16", y2: "16", key: "5lty7f" }] ]); /** * lucide-react v0.0.1 - ISC */ const Brain = createLucideIcon$1("Brain", [ [ "path", { 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", key: "1mhkh5" } ], [ "path", { 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", key: "1d6s00" } ] ]); /** * lucide-react v0.0.1 - ISC */ const Check = createLucideIcon$1("Check", [ ["polyline", { points: "20 6 9 17 4 12", key: "10jjfj" }] ]); /** * lucide-react v0.0.1 - ISC */ const Download = createLucideIcon$1("Download", [ ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }], ["polyline", { points: "7 10 12 15 17 10", key: "2ggqvy" }], ["line", { x1: "12", x2: "12", y1: "15", y2: "3", key: "1vk2je" }] ]); /** * lucide-react v0.0.1 - ISC */ const MapPin = createLucideIcon$1("MapPin", [ [ "path", { d: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z", key: "2oe9fu" } ], ["circle", { cx: "12", cy: "10", r: "3", key: "ilqhr7" }] ]); /** * lucide-react v0.0.1 - ISC */ const PenLine = createLucideIcon$1("PenLine", [ ["path", { d: "M12 20h9", key: "t2du7b" }], [ "path", { d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z", key: "ymcmye" } ] ]); /** * lucide-react v0.0.1 - ISC */ const Send = createLucideIcon$1("Send", [ ["path", { d: "m22 2-7 20-4-9-9-4Z", key: "1q3vgg" }], ["path", { d: "M22 2 11 13", key: "nzbqef" }] ]); /** * lucide-react v0.0.1 - ISC */ const Upload = createLucideIcon$1("Upload", [ ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }], ["polyline", { points: "17 8 12 3 7 8", key: "t8dd8p" }], ["line", { x1: "12", x2: "12", y1: "3", y2: "15", key: "widbto" }] ]); /** * lucide-react v0.0.1 - ISC */ const User = createLucideIcon$1("User", [ ["path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", key: "975kel" }], ["circle", { cx: "12", cy: "7", r: "4", key: "17ys0d" }] ]); /** * lucide-react v0.0.1 - ISC */ const X = createLucideIcon$1("X", [ ["path", { d: "M18 6 6 18", key: "1bl5f8" }], ["path", { d: "m6 6 12 12", key: "d8bk6v" }] ]); // API Service Class // Create a persistent API service instance let apiServiceInstance = null; class BrainKBAPIService { constructor(config) { this.sessionId = null; this.authToken = null; this.refreshToken = null; this.tokenExpiry = null; this.config = config; } // JWT Authentication Methods async authenticate() { const { auth } = this.config.api || {}; console.log('🔐 Starting authentication...', { enabled: auth?.enabled, type: auth?.type, hasJwtEndpoint: !!auth?.jwtEndpoint, hasUsername: !!auth?.username, hasPassword: !!auth?.password }); if (!auth?.enabled || auth.type !== 'jwt') { console.log('❌ JWT authentication not enabled or wrong type'); return null; } // Check if we have a valid token if (this.authToken && this.tokenExpiry && Date.now() < this.tokenExpiry) { console.log('✅ Using existing valid token'); return this.authToken; } // Check if we have a refresh token and auto-refresh is enabled if (this.refreshToken && auth.autoRefresh) { try { console.log('🔄 Attempting token refresh...'); return await this.refreshAuthToken(); } catch (error) { console.warn('Failed to refresh token, will re-authenticate:', error); } } // Perform initial authentication console.log('🔑 Performing initial authentication...'); return await this.performAuthentication(); } async performAuthentication() { const { auth } = this.config.api || {}; if (!auth?.jwtEndpoint || !auth.username || !auth.password) { console.warn('JWT authentication enabled but missing required credentials'); console.log('🔍 Credentials check:', { hasJwtEndpoint: !!auth?.jwtEndpoint, hasUsername: !!auth?.username, hasPassword: !!auth?.password }); return null; } try { console.log('🌐 Making authentication request to:', auth.jwtEndpoint); // Build request body - support both 'username' and 'email' fields const requestBody = { password: auth.password, }; // Use email field by default, or override with emailField config const emailField = auth.emailField || 'email'; requestBody[emailField] = auth.username; console.log(`📧 Using ${emailField} field for authentication`); const response = await fetch(auth.jwtEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); console.log('📡 Authentication response status:', response.status); if (!response.ok) { const errorText = await response.text(); console.error('❌ Authentication failed:', { status: response.status, statusText: response.statusText, errorText }); throw new Error(`Authentication failed: ${response.status} ${response.statusText}`); } const data = await response.json(); console.log('📦 Authentication response received'); // Extract tokens based on configuration const tokenKey = auth.tokenKey || 'access_token' || 'token'; const refreshTokenKey = auth.refreshTokenKey || 'refresh_token'; this.authToken = data[tokenKey]; this.refreshToken = data[refreshTokenKey]; // Calculate token expiry (default to 1 hour if not provided) const expiresIn = data.expires_in || 3600; this.tokenExpiry = Date.now() + (expiresIn * 1000); console.log('🔐 JWT authentication successful'); return this.authToken; } catch (error) { console.error('❌ JWT authentication failed:', error); return null; } } async refreshAuthToken() { const { auth } = this.config.api || {}; if (!auth?.jwtEndpoint || !this.refreshToken) { return null; } try { const response = await fetch(auth.jwtEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refresh_token: this.refreshToken, }), }); if (!response.ok) { throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`); } const data = await response.json(); const tokenKey = auth.tokenKey || 'access_token' || 'token'; const refreshTokenKey = auth.refreshTokenKey || 'refresh_token'; this.authToken = data[tokenKey]; this.refreshToken = data[refreshTokenKey]; const expiresIn = data.expires_in || 3600; this.tokenExpiry = Date.now() + (expiresIn * 1000); console.log('🔄 JWT token refreshed successfully'); return this.authToken; } catch (error) { console.error('❌ JWT token refresh failed:', error); // Clear invalid tokens this.authToken = null; this.refreshToken = null; this.tokenExpiry = null; return null; } } async getAuthHeaders() { const headers = { 'Content-Type': 'application/json', }; // Add JWT token if available const token = await this.authenticate(); console.log('🔐 Authentication result:', token ? 'Token obtained' : 'No token'); if (token) { headers['Authorization'] = `Bearer ${token}`; console.log('🔑 Authorization header set:', `Bearer ${token.substring(0, 20)}...`); } else { console.warn('⚠️ No JWT token available for request'); } // Add any additional headers from config if (this.config.api?.headers) { Object.assign(headers, this.config.api.headers); } return headers; } async sendMessage(message, context, onStream) { const { api } = this.config; if (api?.endpoint) { // Check if streaming is enabled via config or query parameter const url = new URL(api.endpoint); const isStreamingFromQuery = url.searchParams.get('stream') === 'true'; const isStreaming = api.streaming || isStreamingFromQuery; if (isStreaming) { return this.sendStreamingMessage(message, context, onStream); } else { return this.sendRESTMessage(message, context); } } // Fallback to local response return this.generateLocalResponse(message, context); } async sendStreamingMessage(message, context, onStream) { try { const requestBody = { message, session_id: this.sessionId, currentPage: context?.currentPage, pageContext: context?.pageContext, pageContent: context?.pageContent, selectedPageContent: context?.selectedPageContent, chatHistory: context?.chatHistory, timestamp: new Date().toISOString(), }; const endpoint = this.config.api.endpoint; console.log('📤 Sending streaming request to API:', { endpoint: endpoint, sessionId: this.sessionId, messageLength: message.length, hasContext: !!context }); const headers = await this.getAuthHeaders(); headers['Accept'] = 'text/event-stream'; const response = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(requestBody), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('No response body reader available'); } const decoder = new TextDecoder(); let fullContent = ''; let sessionId = this.sessionId; 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: ')) { const data = line.slice(6); // Remove 'data: ' prefix if (data === '[DONE]') { // Stream ended break; } try { const parsed = JSON.parse(data); // Handle session ID if (parsed.session_id) { sessionId = parsed.session_id; this.sessionId = sessionId; console.log('🔗 Session ID received and stored:', sessionId); } // Handle content chunks if (parsed.content) { fullContent += parsed.content; onStream?.(parsed.content); } // Handle other fields if (parsed.type === 'error') { console.error('Streaming API Error:', parsed.error); throw new Error(parsed.error || 'Streaming API error'); } } catch (parseError) { // If it's not JSON, treat as plain text content if (data.trim()) { fullContent += data; onStream?.(data); } } } } } return { content: fullContent, session_id: sessionId, parsed_content: this.parseResponse(fullContent) }; } catch (error) { console.error('Streaming API Error:', error); return this.generateLocalResponse(message, context); } } async sendRESTMessage(message, context) { try { // Try simplified request first, then fallback to full request let requestBody = { message, session_id: this.sessionId, }; // If the first request fails, try with more context if (context) { requestBody = { message, session_id: this.sessionId, currentPage: context?.currentPage, pageContext: context?.pageContext, pageContent: context?.pageContent, selectedPageContent: context?.selectedPageContent, chatHistory: context?.chatHistory, timestamp: new Date().toISOString(), // Add action-specific fields if they exist ...(context?.action && { action: context.action }), ...(context?.actionLabel && { actionLabel: context.actionLabel }), ...(context?.actionDescription && { actionDescription: context.actionDescription }), }; } console.log('📤 Sending request to API:', { endpoint: this.config.api.endpoint, sessionId: this.sessionId, messageLength: message.length, hasContext: !!context, requestBody: requestBody }); const headers = await this.getAuthHeaders(); const response = await fetch(this.config.api.endpoint, { method: 'POST', headers, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); console.error('❌ API Error:', { status: response.status, statusText: response.statusText, errorText: errorText }); // Try to parse error response let errorData; try { errorData = JSON.parse(errorText); } catch { errorData = { detail: errorText }; } throw new Error(`API Error ${response.status}: ${JSON.stringify(errorData)}`); } const responseData = await response.json(); // Debug the response data console.log('📥 API Response Data:', responseData); console.log('📥 Response Data Type:', typeof responseData); console.log('📥 Response Data Keys:', Object.keys(responseData || {})); // Store session ID from response for future requests if (responseData.session_id) { this.sessionId = responseData.session_id; console.log('🔗 Session ID received and stored:', this.sessionId); } // Parse the response content dynamically const parsedContent = this.parseResponse(responseData); return { ...responseData, parsed_content: parsedContent }; } catch (error) { console.error('REST API Error:', error); return this.generateLocalResponse(message, context); } } generateLocalResponse(message, context) { // Generate contextual response based on message content and page context const responses = { greeting: "Hello! I'm your BrainKB Assistant. How can I help you today?", question: "I understand your question. Let me help you find the information you need.", knowledge: "I can help you explore the knowledge base and find relevant information.", default: "I'm here to help! What would you like to know about?" }; const lowerMessage = message.toLowerCase(); // If selected page content is available, provide more contextual response if (context?.selectedPageContent) { const selectedContent = context.selectedPageContent; return { 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?` }; } // If page content is available, provide more contextual response if (context?.pageContent) { context.pageContent; const pageContext = context.pageContext; return { 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?` }; } // If page context is available but no content if (context?.pageContext) { return { content: `I can help you with questions about ${context.pageContext.title || 'this page'}. What would you like to know?` }; } // Default responses based on message content if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) { return { content: responses.greeting }; } else if (lowerMessage.includes('?')) { return { content: responses.question }; } else if (lowerMessage.includes('knowledge') || lowerMessage.includes('data')) { return { content: responses.knowledge }; } return { content: responses.default }; } // Dynamic response parser that handles any API response structure parseResponse(response) { console.log('🔍 Parsing response:', response); console.log('🔍 Response type:', typeof response); // If response is already a string, return it if (typeof response === 'string') { console.log('✅ Response is string, returning as-is'); return response; } // If response is null or undefined, return default message if (response === null || response === undefined) { console.log('⚠️ Response is null/undefined, returning default'); return 'I received your message but got an empty response. Please try again.'; } // If response is an object, try to extract content if (typeof response === 'object') { console.log('🔍 Response keys:', Object.keys(response)); // Common response content field names const contentFields = [ 'content', 'message', 'response', 'text', 'data', 'answer', 'reply', 'result', 'output', 'body', 'value', 'description', 'summary' ]; // Try to find content in common fields for (const field of contentFields) { if (response[field] !== undefined && response[field] !== null) { console.log(`✅ Found content in field '${field}':`, response[field]); return this.stringifyIfNeeded(response[field]); } } // If no content field found, check if the object itself is the content // (e.g., if the API returns the message directly as an object) if (response.toString && response.toString() !== '[object Object]') { console.log('✅ Using response.toString():', response.toString()); return response.toString(); } // If it's an array, try to join it or take the first element if (Array.isArray(response)) { if (response.length === 0) { console.log('⚠️ Response is empty array'); return 'I received an empty response. Please try again.'; } // If array has one element, use it if (response.length === 1) { console.log('✅ Using first array element:', response[0]); return this.stringifyIfNeeded(response[0]); } // If array has multiple elements, join them console.log('✅ Joining array elements'); return response.map(item => this.stringifyIfNeeded(item)).join('\n\n'); } // Last resort: stringify the entire response console.log('⚠️ No content field found, stringifying entire response'); return JSON.stringify(response, null, 2); } // For any other type, convert to string console.log('✅ Converting response to string:', String(response)); return String(response); } // Helper method to stringify objects if needed stringifyIfNeeded(value) { if (typeof value === 'string') { return value; } if (typeof value === 'object' && value !== null) { // If it's a simple object with a few properties, format it nicely const keys = Object.keys(value); if (keys.length <= 5) { try { return JSON.stringify(value, null, 2); } catch { return String(value); } } // For complex objects, just stringify return JSON.stringify(value, null, 2); } return String(value); } } // Code Block Component with Syntax Highlighting const CodeBlock = ({ code, language = 'javascript' }) => { const copyToClipboard = async () => { try { await navigator.clipboard.writeText(code); } catch (err) { console.error('Failed to copy: ', err); } }; return (jsxRuntime.jsxs("div", { className: "bg-gray-900 rounded-lg overflow-hidden border border-gray-700 my-2", style: { maxWidth: '100%', width: '100%' }, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 bg-gray-800 border-b border-gray-700", children: [jsxRuntime.jsx("span", { className: "text-xs font-medium text-gray-300 uppercase", children: language }), jsxRuntime.jsx("button", { onClick: copyToClipboard, className: "text-xs text-gray-400 hover:text-white transition-colors", title: "Copy to clipboard", children: "Copy" })] }), jsxRuntime.jsx("pre", { className: "p-4 overflow-x-auto", style: { fontSize: '13px', lineHeight: '1.4', maxWidth: '100%', width: '100%', wordWrap: 'break-word', overflowWrap: 'break-word' }, children: jsxRuntime.jsx("code", { className: "text-gray-100", style: { wordWrap: 'break-word', overflowWrap: 'break-word', width: '100%' }, children: code }) })] })); }; // Helper function to extract table data for download (standalone) const extractTableDataForDownload = (headers, rows) => { const dataRows = []; rows.forEach((row, index) => { if (index === 0) return; // Skip header row const cells = row.split('|').filter(cell => cell.trim()); if (cells.length > 0) { dataRows.push(cells.map(cell => cell.trim())); } }); return { headers: headers, rows: dataRows }; }; // Enhanced Table Component with Download const EnhancedTable = ({ headers, rows, tableId }) => { const [tableData] = React.useState(() => extractTableDataForDownload(headers, rows)); const handleDownload = (format) => { console.log(`📥 Downloading table ${tableId} as ${format}`); // You can add analytics or tracking here }; return (jsxRuntime.jsxs("div", { className: "table-container", style: { background: 'white', borderRadius: '16px', border: '1px solid #e2e8f0', overflow: 'hidden', margin: '16px 0', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)', maxWidth: '100%', boxSizing: 'border-box', width: '100%' }, children: [jsxRuntime.jsxs("div", { className: "table-header", style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '24px 28px 20px 28px', borderBottom: '2px solid #e2e8f0', background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)', position: 'relative', minHeight: '70px', boxSizing: 'border-box' }, children: [jsxRuntime.jsx("div", { className: "table-title", style: { fontSize: '18px', color: '#1e293b', fontWeight: '700', display: 'flex', alignItems: 'center', gap: '12px', textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)', flex: '1', wordWrap: 'break-word', overflowWrap: 'break-word', maxWidth: '100%' }, children: "\uD83D\uDCCA Query Results Table" }), jsxRuntime.jsx("div", { className: "download-container", style: { position: 'relative', display: 'inline-block', zIndex: 10 }, children: jsxRuntime.jsx(DownloadDataButton, { data: tableData, filename: `query-results-${tableId}`, onDownload: handleDownload }) })] }), jsxRuntime.jsx("div", { style: { overflowX: 'auto', maxWidth: '100%', width: '100%' }, children: jsxRuntime.jsxs("table", { style: { width: '100%', borderCollapse: 'collapse', fontSize: '13px', lineHeight: '1.4', tableLayout: 'fixed', wordWrap: 'break-word', overflowWrap: 'break-word', minWidth: '100%' }, children: [jsxRuntime.jsx("thead", { style: { background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)', borderBottom: '2px solid #e2e8f0' }, children: jsxRuntime.jsx("tr", { children: headers.map((header, index) => (jsxRuntime.jsx("th", { style: { padding: '12px 16px', textAlign: 'left', fontWeight: '600', color: '#1e293b', fontSize: '13px', borderBottom: '1px solid #e2e8f0', background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)', wordWrap: 'break-word', overflowWrap: 'break-word', maxWidth: '120px', minWidth: '80px', whiteSpace: 'normal', verticalAlign: 'top' }, children: header }, index))) }) }), jsxRuntime.jsx("tbody", { children: rows.map((row, rowIndex) => { if (rowIndex === 0) return null; // Skip header row const cells = row.split('|').filter(cell => cell.trim()); if (cells.length > 0) { return (jsxRuntime.jsx("tr", { style: { borderBottom: '1px solid #f1f5f9', transition: 'background-color 0.2s ease', backgroundColor: rowIndex % 2 === 0 ? 'transparent' : '#fafbfc' }, children: cells.map((cell, cellIndex) => { const cellContent = cell.trim(); let cellStyle = { padding: '12px 16px', color: '#374151', fontSize: '12px', lineHeight: '1.4', verticalAlign: 'top', wordWrap: 'break-word', overflowWrap: 'break-word', hyphens: 'auto', maxWidth: '120px', minWidth: '80px', whiteSpace: 'normal' }; // Apply special styling based on content type if (cellIndex === 0 && /^\d+$/.test(cellContent)) { // Index cell styling cellStyle = { ...cellStyle, fontWeight: '600', color: '#1e293b', textAlign: 'center', background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)', borderRadius: '8px', padding: '6px 10px', minWidth: '40px', display: 'inline-block', border: '1px solid #bae6fd', fontSize: '12px' }; } else if (cellContent.includes('ncbitaxon:') || cellContent.includes('http') || cellContent.includes('://') || cellContent.includes('GCF_') || cellContent.includes('GCA_')) { // URI cell styling cellStyle = { ...cellStyle, fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace", fontSize: '11px', color: '#0369a1', background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)', borderRadius: '6px', padding: '4px 8px', border: '1px solid #bae6fd', wordBreak: 'break-all', overflowWrap: 'break-word', maxWidth: '100%' }; } return (jsxRuntime.jsx("td", { style: cellStyle, children: cellContent }, cellIndex)); }) }, rowIndex)); } return null; }) })] }) })] })); }; // Markdown Renderer Component const MarkdownRenderer = ({ content }) => { const renderContent = (text) => { const elements = []; let currentIndex = 0; // Split by code blocks first const parts = text.split(/(```[\s\S]*?```)/); for (const part of parts) { if (part.startsWith('```')) { // Extract language and code const match = part.match(/```(\w+)?\n([\s\S]*?)```/); if (match) { const language = match[1] || 'text'; const code = match[2]; elements.push(jsxRuntime.jsx(CodeBlock, { code: code, language: language }, currentIndex++)); continue; } } // Enhanced markdown processing with better structure handling let processedText = part; // Clean up repeated content patterns (common in AI responses) processedText = processedText .replace(/(Certainly! Let's break down and analyze[^.]*\.)/g, '') .replace(/(Based on your data, the following entities are central:)/g, '### Key Entities Identified') .replace(/(The relationships between these entities can be visualized as follows:)/g, '### Relationships and Data Flow') .replace(/(In a knowledge graph, these entities and relationships might look like:)/g, '### Knowledge Graph Representation') .replace(/(Would you like a visual diagram[^?]*\?)/g, ''); // Format query result summaries processedText = processedText .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>') .replace(/Variable:\s*(\w+)/g, '<div class="query-summary">Variable: <strong>$1</strong></div>') .replace(/Results:\s*(\d+)\s*entries?/g, '<div class="query-summary">Results: <strong>$1 entries</strong></div>'); // Enhanced table detection and formatting const lines = processedText.split('\n'); const processedLines = []; let inTable = false; let tableRows = []; let tableHeaders = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Detect table structure (lines with | and ---) if (line.includes('|') && line.trim().startsWith('|') && line.trim().endsWith('|')) { if (!inTable) { inTable = true; tableRows = []; tableHeaders = []; } // Extract headers from first row if (tableHeaders.length === 0) { const headerCells = line.split('|').filter(cell => cell.trim()); tableHeaders = headerCells.map(cell => cell.trim()); } // Extract data cells const cells = line.split('|').filter(cell => cell.trim()); if (cells.length > 0) { tableRows.push(line); } } else if (line.includes('---') && inTable) { // Skip separator lines continue; } else if (inTable && !line.includes('|')) { // End of table inTable = false; // Convert table to React component if (tableRows.length > 0) { const tableId = `table-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; elements.push(jsxRuntime.jsx(EnhancedTable, { headers: tableHeaders, rows: tableRows, tableId: tableId }, currentIndex++)); } // Add the current line as regular text if (line.trim()) { processedLines.push(line); } } else if (inTable) { // Continue building table tableRows.push(line); } else { // Regular text processing processedLines.push(line); } } // Handle table at end of content if (inTable && tableRows.length > 0) { const tableId = `table-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; elements.push(jsxRuntime.jsx(EnhancedTable, { headers: tableHeaders, rows: tableRows, tableId: tableId }, currentIndex++)); } processedText = processedLines.join('\n'); // Headers (h1-h6) - improved regex to handle edge cases processedText = processedText .replace(/^#{6}\s+(.+?)(?:\n|$)/gm, '<h6 style="font-size: 1rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;">$1</h6>') .replace(/^#{5}\s+(.+?)(?:\n|$)/gm, '<h5 style="font-size: 1.125rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;">$1</h5>') .replace(/^#{4}\s+(.+?)(?:\n|$)/gm, '<h4 style="font-size: 1.25rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;">$1</h4>') .replace(/^#{3}\s+(.+?)(?:\n|$)/gm, '<h3 style="font-size: 1.5rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;">$1</h3>') .replace(/^#{2}\s+(.+?)(?:\n|$)/gm, '<h2 style="font-size: 1.875rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;">$1</h2>') .replace(/^#{1}\s+(.+?)(?:\n|$)/gm, '<h1 style="font-size: 2.25rem; font-weight: 600; margin: 1rem 0 0.5rem 0; color: #374151;">$1</h1>'); // Bold and italic - improved to handle nested patterns processedText = processedText .replace(/\*\*(.*?)\*\*/g, '<strong style="font-weight: 600;">$1</strong>') .replace(/\*(.*?)\*/g, '<em style="font-style: italic;">$1</em>') .replace(/__(.*?)__/g, '<strong style="font-weight: 600;">$1</strong>') .replace(/_(.*?)_/g, '<em style="font-style: italic;">$1</em>'); // Inline code processedText = processedText .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>'); // Links processedText = processedText .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color: #3b82f6; text-decoration: underline;" target="_blank" rel="noopener noreferrer">$1</a>'); // Lists - improved to handle numbered and bulleted lists better processedText = processedText .replace(/^\s*[-*+]\s+(.+?)(?:\n|$)/gm, '<li style="margin: 0.25rem 0; padding-left: 0.5rem;">$1</li>') .replace(/^\s*\d+\.\s+(.+?)(?:\n|$)/gm, '<li style="margin: 0.25rem 0; padding-left: 0.5rem;">$1</li>'); // Wrap consecutive list items in ul/ol processedText = processedText .replace(/(<li[^>]*>.*?<\/li>)(?:\s*<li[^>]*>.*?<\/li>)*/gs, (match) => { return `<ul style="margin: 0.5rem 0; padding-left: 1.5rem; list-style-type: disc;">${match}</ul>`; }); // Blockquotes processedText = processedText .replace(/^>\s+(.+?)(?:\n|$)/gm, '<blockquote style="border-left: 4px solid #e5e7eb; padding-left: 1rem; margin: 1rem 0; color: #6b7280; font-style: italic;">$1</blockquote>'); // Horizontal rules processedText = processedText .replace(/^---+$/gm, '<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 1rem 0;">'); // Paragraphs - wrap text in paragraphs for better structure processedText = processedText .split('\n\n') .map(paragraph => { if (paragraph.trim() && !paragraph.match(/^<[^>]+>/) && !paragraph.match(/^#{1,6}\s/)) { return `<p style="margin: 0.75rem 0; line-height: 1.6;">${paragraph.trim()}</p>`; } return paragraph; }) .join('\n\n'); // Line breaks within paragraphs processedText = processedText .replace(/\n(?!\n)/g, '<br>'); // Add processed text as HTML element if (processedText.trim()) { elements.push(jsxRuntime.jsx("div", { style: { fontSize: '14px', lineHeight: '1.6', wordWrap: 'break-word', overflowWrap: 'break-word', maxWidth: '100%', width: '100%', color: '#374151' }, dangerouslySetInnerHTML: { __html: processedText } }, currentIndex++)); } } return elements; }; return (jsxRuntime.jsx("div", { style: { wordWrap: 'break-word', overflowWrap: 'break-word', maxWidth: '100%', width: '100%' }, children: renderContent(content) })); }; // File Renderer Component const FileRenderer = ({ file, content }) => { const [imageUrl, setImageUrl] = React.useState(null); const getLanguage = (filename) => { const ext = filename.split('.').pop()?.toLowerCase(); switch (ext) { case 'json': return 'json'; case 'jsonld': return 'json'; case 'ttl': return 'turtle'; case 'csv': return 'csv'; case 'txt': return 'text'; default: return 'text'; } }; const isImageFile = (filename) => { const ext = filename.split('.').pop()?.toLowerCase(); return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext || ''); }; const formatContent = (content, filename) => { const ext = filename.split('.').pop()?.toLowerCase(); if (ext === 'json' || ext === 'jsonld') { try { return JSON.stringify(JSON.parse(content), null, 2); } catch { return content; } } if (ext === 'csv') { return content; } if (ext === 'ttl') { return content; } return content; }; // Handle image files React.useEffect(() => { if (isImageFile(file.name)) { const url = URL.createObjectURL(file); setImageUrl(url); // Cleanup URL when component unmounts return () => URL.revokeObjectURL(url); } }, [file]); // Render image files if (isImageFile(file.name) && imageUrl) { return (jsxRuntime.jsxs("div", { className: "bg-gray-50 rounded-lg p-4 border border-gray-200", children: [jsxRuntime.jsx("div", { className: "flex items-center justify-between mb-3", children: jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx(Upload, { className: "w-4 h-4 text-gray-500" }), jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700", children: file.name }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: ["(", (file.size / 1024).toFixed(1), " KB)"] })] }) }), jsxRuntime.jsx("div", { className: "flex justify-center", children: jsxRuntime.jsx("img", { src: imageUrl, alt: file.name, className: "max-w-full max-h-96 rounded-lg shadow-md", style: { objectFit: 'contain' } }) })] })); } // Render text files if (content) { return (jsxRuntime.jsxs("div", { className: "bg-gray-50 rounded-lg p-4 border border-gray-200", children: [jsxRuntime.jsx("div", { className: "flex items-center justify-between mb-3", children: jsxRuntime.jsxs("div", { className: "flex items-center space-x-2", children: [jsxRuntime.jsx(Upload, { className: "w-4 h-4 text-gray-500" }), jsxRuntime.jsx("span", { className: "text-sm font-medium text-gray-700", children: file.name }), jsxRuntime.jsxs("span", { className: "text-xs text-gray-500", children: ["(", (file.size / 1024).toFixed(1), " KB)"] })] }) }), jsxRuntime.jsx(CodeBlock, { code: formatContent(content, file.name), language: getLanguage(file.name) })] })); } return null; }; // File Upload Component const FileUpload = ({ onFileUpload, enabled = true }) => { const [isDragOver, setIsDragOver] = React.useState(false); const fileInputRef = React.useRef(null); if (!enabled) return null; const handleFileSelect = (files) => { if (files && files.length > 0) { onFileUpload(files[0]); } }; const handleDrag = (e) => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); const files = e.dataTransfer.files; if (files && files.length > 0) { onFileUpload(files[0]); } }; return (jsxRuntime.jsxs("div", { className: `border-2 border-dashed rounded-lg p-6 text-center transition-colors ${isDragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'}`, onDragOver: handleDrag, onDragEnter: (e) => { e.preventDefault(); setIsDragOver(true); }, onDragLeave: (e) => { e.preventDefault(); setIsDragOver(false); }, onDrop: handleDrop, children: [jsxRuntime.jsx(Upload, { className: "w-8 h-8 text-gray-400 mx-auto mb-2" }), jsxRuntime.jsxs("p", { className: "text-sm text-gray-600 mb-2", children: ["Drag and drop files here, or", ' ', jsxRuntime.jsx("button", { onClick: () => fileInputRef.current?.click(), className: "text-blue-600 hover:text-blue-700 underline", children: "browse" })] }), jsxRuntime.jsx("p", { className: "text-xs text-gray-500", children: "Supports: JSON, JSON-LD, TTL, CSV, TXT, Images (PNG, JPG, GIF)" }), jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", className: "hidden", accept: ".json,.jsonld,.ttl,.csv,.txt,.png,.jpg,.jpeg,.gif", onChange: (e) => handleFileSelect(e.target.files) })] })); };