UNPKG

docusaurus-openai-search

Version:

AI-powered search plugin for Docusaurus - extends Algolia search with intelligent keyword generation and RAG-based answers

871 lines (869 loc) 54.7 kB
import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; import { createLogger, SearchOrchestrator, ResponseCache } from '../utils'; import { ModalCleanupUtils, RefCleanupUtils, useCleanup } from '../utils/cleanup'; import { useErrorBoundary } from '../components/ErrorBoundary'; import '../styles.css'; import { DEFAULT_CONFIG } from '../config/defaults'; // Cache for loaded components let markdownComponents = null; let markdownLoadPromise = null; // P4-002: Lazy load markdown dependencies async function loadMarkdownDependencies() { if (markdownComponents) { return markdownComponents; } if (markdownLoadPromise) { return markdownLoadPromise; } markdownLoadPromise = Promise.all([ import('react-markdown'), import('remark-gfm'), import('rehype-raw'), import('prism-react-renderer') ]).then(([reactMarkdown, remarkGfm, rehypeRaw, prismRenderer]) => { const components = { ReactMarkdown: reactMarkdown.default, remarkGfm: remarkGfm.default, rehypeRaw: rehypeRaw.default, Highlight: prismRenderer.Highlight, prismThemes: prismRenderer.themes }; markdownComponents = components; return components; }); return markdownLoadPromise; } /** * Custom code block renderer for ReactMarkdown * P4-002: Updated to work with lazy-loaded components */ const CodeBlock = React.memo(({ node, inline, className, children, components, ...props }) => { const match = /language-(\w+)/.exec(className || ''); const language = match && match[1] ? match[1] : ''; const code = String(children).replace(/\n$/, ''); if (!inline && language && components) { const { Highlight, prismThemes } = components; return (React.createElement(Highlight, { theme: props.theme || prismThemes.github, code: code, language: language }, ({ className, style, tokens, getLineProps, getTokenProps }) => (React.createElement("pre", { className: className, style: style }, tokens.map((line, i) => (React.createElement("div", { key: i, ...getLineProps({ line }) }, line.map((token, key) => (React.createElement("span", { key: key, ...getTokenProps({ token }) })))))))))); } return (React.createElement("code", { className: className, ...props }, children)); }); /** * Modal component that displays AI-generated answers to search queries */ export function AISearchModal({ query, onClose, searchResults, config, themeConfig, algoliaConfig }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [answer, setAnswer] = useState(null); const [formattedAnswer, setFormattedAnswer] = useState(''); const [retrievedContent, setRetrievedContent] = useState([]); const [searchStep, setSearchStep] = useState(null); const [fetchFailed, setFetchFailed] = useState(false); const [isRetrying, setIsRetrying] = useState(false); const [queryAnalysis, setQueryAnalysis] = useState(null); const [aiCallCount, setAiCallCount] = useState(0); const [isFromCache, setIsFromCache] = useState(false); const [isGeneratingAnswer, setIsGeneratingAnswer] = useState(false); // Week 2 Enhancement: Validation state for confidence indicators const [validation, setValidation] = useState(null); // P4-002: State for lazy-loaded markdown components const [markdownComponentsLoaded, setMarkdownComponentsLoaded] = useState(null); const [markdownLoading, setMarkdownLoading] = useState(false); // Reference to the markdown container const markdownRef = useRef(null); // Reference to search orchestrator const orchestratorRef = useRef(null); // Ref to track if answer generation has started to prevent duplicates const answerGenerationStartedRef = useRef(false); // Cache instance const cache = ResponseCache.getInstance(); // Cleanup hook for component const { registerCleanup, cleanupComponent } = useCleanup('ai-search-modal'); // P3-001: Error boundary hook for enhanced error handling const { createErrorHandler } = useErrorBoundary(); // Stage 2: Multi-source search state const [multiSourceResults, setMultiSourceResults] = useState(null); // Use useMemo to ensure these values are stable and don't change after mount const isMultiSourceEnabled = useMemo(() => !!config?.features?.multiSource, [config?.features?.multiSource]); const multiSourceConfig = useMemo(() => { if (!config?.features?.multiSource) return null; const msConfig = {}; if (config.features.multiSource.github?.repo) { msConfig.github = { repository: config.features.multiSource.github.repo }; } if (config.features.multiSource.blog?.url) { msConfig.blog = { url: config.features.multiSource.blog.url }; } if (config.features.multiSource.changelog?.url) { msConfig.changelog = { url: config.features.multiSource.changelog.url }; } return msConfig; }, [config?.features?.multiSource]); // Week 6: Conversational memory state const [sessionId, setSessionId] = useState(null); const [conversationHistory, setConversationHistory] = useState([]); // Stage 3: Fine-tuned model enhancement state const [enhancement, setEnhancement] = useState(null); // Use useMemo for conversational memory enabled const isConversationalMemoryEnabled = useMemo(() => !!config?.features?.conversationalMemory?.enabled, [config?.features?.conversationalMemory?.enabled]); // Week 6: Query Intelligence const [searchTime] = useState(Date.now()); // Initialize logger with enableLogging config useEffect(() => { createLogger(config?.enableLogging || false); }, [config?.enableLogging]); // P4-002: Load markdown components when modal opens useEffect(() => { if (!markdownComponentsLoaded && !markdownLoading) { setMarkdownLoading(true); loadMarkdownDependencies() .then((components) => { setMarkdownComponentsLoaded(components); setMarkdownLoading(false); }) .catch((error) => { console.error('[AI Search Modal] Failed to load markdown components:', error); setMarkdownLoading(false); }); } }, [markdownComponentsLoaded, markdownLoading]); // Get the backend URL from config const backendUrl = config?.backend?.url; // console.log('[AI Search Modal] Backend URL:', backendUrl); // P4-001 & P4-002: Memoize Prism theme computation with lazy-loaded components const prismTheme = useMemo(() => { // P4-002: Return null if markdown components aren't loaded yet if (!markdownComponentsLoaded?.prismThemes) { return null; } const isDarkTheme = typeof document !== 'undefined' && document.documentElement.dataset.theme === 'dark'; if (themeConfig?.prism) { if (isDarkTheme && themeConfig.prism.darkTheme && typeof themeConfig.prism.darkTheme === 'object' && 'plain' in themeConfig.prism.darkTheme && 'styles' in themeConfig.prism.darkTheme) { return themeConfig.prism.darkTheme; } else if (themeConfig.prism.theme && typeof themeConfig.prism.theme === 'object' && 'plain' in themeConfig.prism.theme && 'styles' in themeConfig.prism.theme) { return themeConfig.prism.theme; } } // Default to standard themes if custom ones aren't available return isDarkTheme ? markdownComponentsLoaded.prismThemes.vsDark : markdownComponentsLoaded.prismThemes.github; }, [themeConfig?.prism, markdownComponentsLoaded?.prismThemes]); // P4-001: Memoize modal texts to prevent object recreation on every render const modalTexts = useMemo(() => ({ modalTitle: config?.ui?.modalTitle || DEFAULT_CONFIG.ui.modalTitle, loadingText: config?.ui?.loadingText || DEFAULT_CONFIG.ui.loadingText, errorText: config?.ui?.errorText || DEFAULT_CONFIG.ui.errorText, retryButtonText: config?.ui?.retryButtonText || DEFAULT_CONFIG.ui.retryButtonText, footerText: config?.ui?.footerText || DEFAULT_CONFIG.ui.footerText, retrievingText: config?.ui?.retrievingText || DEFAULT_CONFIG.ui.retrievingText, generatingText: config?.ui?.generatingText || DEFAULT_CONFIG.ui.generatingText, questionPrefix: config?.ui?.questionPrefix || DEFAULT_CONFIG.ui.questionPrefix, searchKeywordsLabel: config?.ui?.searchKeywordsLabel || DEFAULT_CONFIG.ui.searchKeywordsLabel, documentsFoundLabel: config?.ui?.documentsFoundLabel || DEFAULT_CONFIG.ui.documentsFoundLabel, documentsMoreText: config?.ui?.documentsMoreText || DEFAULT_CONFIG.ui.documentsMoreText, sourcesHeaderText: config?.ui?.sourcesHeaderText || DEFAULT_CONFIG.ui.sourcesHeaderText, searchLinksHelpText: config?.ui?.searchLinksHelpText || DEFAULT_CONFIG.ui.searchLinksHelpText, closeButtonAriaLabel: config?.ui?.closeButtonAriaLabel || DEFAULT_CONFIG.ui.closeButtonAriaLabel, cachedResponseText: config?.ui?.cachedResponseText || DEFAULT_CONFIG.ui.cachedResponseText, documentsAnalyzedText: config?.ui?.documentsAnalyzedText || DEFAULT_CONFIG.ui.documentsAnalyzedText, searchResultsOnlyText: config?.ui?.searchResultsOnlyText || DEFAULT_CONFIG.ui.searchResultsOnlyText, noDocumentsFoundError: config?.ui?.noDocumentsFoundError || DEFAULT_CONFIG.ui.noDocumentsFoundError, noSearchResultsError: config?.ui?.noSearchResultsError || DEFAULT_CONFIG.ui.noSearchResultsError, }), [config?.ui]); // P2-001: Enhanced retry handler with proper ref reset and safety checks const handleRetry = useCallback(() => { // Reset all states setIsRetrying(true); setError(null); setLoading(true); setSearchStep(null); setRetrievedContent([]); setAnswer(null); setIsGeneratingAnswer(false); // P2-001: Enhanced ref reset with safety checks try { // Ensure answerGenerationStartedRef is properly reset if (answerGenerationStartedRef && answerGenerationStartedRef.current !== undefined) { answerGenerationStartedRef.current = false; } // P3-002: Reset orchestrator and cancel pending operations if (orchestratorRef.current) { // Cancel any ongoing operations before retry if (typeof orchestratorRef.current.cancelAllOperations === 'function') { orchestratorRef.current.cancelAllOperations(); } // Recreate orchestrator to ensure clean state orchestratorRef.current = null; if (backendUrl && config) { orchestratorRef.current = new SearchOrchestrator(config, (step) => setSearchStep(step)); console.log('[AI Search Modal] Orchestrator recreated for retry'); } } } catch (error) { console.error('[AI Search Modal] Error during retry ref reset:', error); } // Set a small delay to ensure UI updates before retrying setTimeout(() => { setIsRetrying(false); // The useEffects will handle the retry }, 100); }, []); // P2-001: Enhanced close handler with comprehensive ref reset and safety checks const handleClose = useCallback(() => { // Pre-close safety checks try { // Ensure answerGenerationStartedRef is properly reset with safety check if (answerGenerationStartedRef && answerGenerationStartedRef.current !== undefined) { answerGenerationStartedRef.current = false; } // P3-002: Stop any ongoing orchestrator operations if (orchestratorRef.current) { // Cancel all pending operations before clearing reference if (typeof orchestratorRef.current.cancelAllOperations === 'function') { orchestratorRef.current.cancelAllOperations(); } orchestratorRef.current = null; } // Reset refs on modal close with null checks RefCleanupUtils.clearRefs(markdownRef, orchestratorRef, answerGenerationStartedRef); // Reset all state to initial values ModalCleanupUtils.cleanupModal({ refs: [markdownRef, orchestratorRef, answerGenerationStartedRef], states: [ { setter: setLoading, initialValue: true }, { setter: setError, initialValue: null }, { setter: setAnswer, initialValue: null }, { setter: setFormattedAnswer, initialValue: '' }, { setter: setRetrievedContent, initialValue: [] }, { setter: setSearchStep, initialValue: null }, { setter: setFetchFailed, initialValue: false }, { setter: setIsRetrying, initialValue: false }, { setter: setQueryAnalysis, initialValue: null }, { setter: setAiCallCount, initialValue: 0 }, { setter: setIsFromCache, initialValue: false }, { setter: setIsGeneratingAnswer, initialValue: false } ] }); } catch (error) { // Safety net - log error but don't prevent modal from closing console.error('[AI Search Modal] Error during close cleanup:', error); } finally { // Always call the original onClose regardless of cleanup success onClose(); } }, [onClose]); // P2-001: Enhanced ref initialization and orchestrator setup with safety checks useEffect(() => { // Ensure answerGenerationStartedRef is properly initialized if (answerGenerationStartedRef.current === undefined || answerGenerationStartedRef.current === null) { answerGenerationStartedRef.current = false; } // Initialize orchestrator with safety checks - only create if it doesn't exist if (backendUrl && config && !orchestratorRef.current) { console.log('[AI Search Modal] Creating orchestrator with config:', { backendUrl, hasConfig: !!config, configBackendUrl: config?.backend?.url }); orchestratorRef.current = new SearchOrchestrator(config, (step) => setSearchStep(step)); console.log('[AI Search Modal] Orchestrator created'); } // Don't cleanup orchestrator on dependency changes - only on unmount return () => { // Only cleanup on actual unmount, not on dependency changes }; }, [backendUrl]); // Remove config from dependencies to prevent recreating orchestrator // P2-001: Enhanced component lifecycle management with ref monitoring useEffect(() => { // Register cleanup tasks with enhanced ref management registerCleanup('refs', () => { RefCleanupUtils.clearRefs(markdownRef, orchestratorRef, answerGenerationStartedRef); }, 10); registerCleanup('orchestrator', () => { if (orchestratorRef.current) { // P3-002: Enhanced orchestrator cleanup with operation cancellation if (typeof orchestratorRef.current.cancelAllOperations === 'function') { orchestratorRef.current.cancelAllOperations(); } orchestratorRef.current = null; } }, 5); registerCleanup('answerGeneration', () => { // Ensure answer generation ref is properly reset if (answerGenerationStartedRef && answerGenerationStartedRef.current !== undefined) { answerGenerationStartedRef.current = false; } }, 8); return () => { // Clear state on unmount with enhanced safety try { cleanupComponent(); // Also ensure orchestrator is cleaned up on unmount if (orchestratorRef.current) { orchestratorRef.current.cancelAllOperations(); orchestratorRef.current = null; } } catch (error) { console.error('[AI Search Modal] Error during component cleanup:', error); } }; }, []); // Empty dependency array - only run once on mount // P2-001: Monitor ref state changes for debugging and safety useEffect(() => { // Optional: Add development-only ref state monitoring if (process.env.NODE_ENV === 'development' && config?.enableLogging) { const checkRefs = () => { console.debug('[AI Search Modal] Ref states:', { markdownRef: !!markdownRef.current, orchestratorRef: !!orchestratorRef.current, answerGenerationStarted: answerGenerationStartedRef.current }); }; // Initial check checkRefs(); } }, [config?.enableLogging]); // Week 6: Get session ID from orchestrator when it's ready useEffect(() => { if (orchestratorRef.current && isConversationalMemoryEnabled) { const sessionId = orchestratorRef.current.getSessionId(); if (sessionId) { setSessionId(sessionId); } } }, [isConversationalMemoryEnabled]); // Week 6: Load conversation history when session is available useEffect(() => { if (sessionId && orchestratorRef.current && isConversationalMemoryEnabled) { orchestratorRef.current.getConversationHistory() .then(history => { setConversationHistory(history); }) .catch(error => { console.error('Failed to load conversation history:', error); }); } }, [sessionId, isConversationalMemoryEnabled]); // Stage 2 & Week 6: Enhanced fetchContent function with multi-source and conversational support const previousQueryRef = useRef(''); useEffect(() => { const errorHandler = createErrorHandler('AISearchModal-fetchContent'); let isCancelled = false; // P3-002: Race condition protection // Check if query actually changed const queryChanged = previousQueryRef.current !== query; previousQueryRef.current = query; async function fetchContent() { // console.log('[AI Search Modal] fetchContent called', { // query, // hasAnswer: !!answer, // retrievedContentLength: retrievedContent.length, // loading, // isRetrying // }); if (!query) { setLoading(false); return; } // Skip if we're retrying if (isRetrying) { // console.log('[AI Search Modal] Skipping - isRetrying is true'); return; } // Skip if we already have content and an answer if (retrievedContent.length > 0 && answer) { // console.log('[AI Search Modal] Skipping - already have content and answer'); setLoading(false); return; } // P3-002: Check if this effect instance was cancelled if (isCancelled) { // console.log('[AI Search Modal] Skipping - isCancelled is true'); return; } try { setFetchFailed(false); // Check cache first if enabled const enableCaching = config?.enableCaching ?? DEFAULT_CONFIG.enableCaching; const cacheTTL = config?.cacheTTL || DEFAULT_CONFIG.cacheTTL; if (enableCaching) { const cached = cache.getCached(query, cacheTTL); if (cached) { if (cached.response) { // P3-002: Check for race condition before setting state if (isCancelled) return; // Full cache hit - we have everything setAnswer(cached.response); setRetrievedContent(cached.documents || []); if (cached.queryAnalysis && typeof cached.queryAnalysis === 'object') { setQueryAnalysis(cached.queryAnalysis); } setIsFromCache(true); setLoading(false); // Track cached response if (config?.onAIQuery) { config.onAIQuery(query, true); } return; } } } // Use AI search with the new orchestrator // console.log('[AI Search Modal] Checking orchestrator:', { // hasOrchestratorRef: !!orchestratorRef, // hasOrchestratorCurrent: !!orchestratorRef.current, // hasAlgoliaConfig: !!algoliaConfig, // algoliaConfigKeys: algoliaConfig ? Object.keys(algoliaConfig) : [] // }); if (orchestratorRef.current && algoliaConfig) { // console.log('[AI Search Modal] Using orchestrator search', { // isConversationalMemoryEnabled, // isMultiSourceEnabled, // hasMultiSourceConfig: !!multiSourceConfig // }); let result; // Week 6: Use conversational search if enabled if (isConversationalMemoryEnabled) { // console.log('[AI Search Modal] Using conversational search'); result = await orchestratorRef.current.performConversationalAISearch(query, algoliaConfig.searchClient, algoliaConfig.indexName); // P3-002: Check for race condition after async operation if (isCancelled) return; // Set conversational search results setAnswer(result.answer); setRetrievedContent(result.documents); if (result.sessionId) { setSessionId(result.sessionId); } // Set validation and query analysis data if (result.validation) { setValidation(result.validation); } if (result.queryAnalysis) { setQueryAnalysis(result.queryAnalysis); } // Stage 3: Set enhancement data if (result.enhancement) { setEnhancement(result.enhancement); } } // Stage 2: Use multi-source search if enabled and conversational is not else if (isMultiSourceEnabled && multiSourceConfig) { // console.log('[AI Search Modal] Using multi-source search'); result = await orchestratorRef.current.performMultiSourceAISearch(query, algoliaConfig.searchClient, algoliaConfig.indexName, multiSourceConfig); // P3-002: Check for race condition after async operation if (isCancelled) return; // Set multi-source results setMultiSourceResults(result); setAnswer(result.answer); // Convert multi-source results to DocumentContent for backward compatibility const documents = result.sources.map((source) => ({ url: source.url, title: source.title, content: source.content })); setRetrievedContent(documents); // Set validation data if (result.validation) { setValidation(result.validation); } } else { // Use traditional single-source search // console.log('[AI Search Modal] Using traditional single-source search'); try { // console.log('[AI Search Modal] Calling performAISearch...'); result = await orchestratorRef.current.performAISearch(query, algoliaConfig.searchClient, algoliaConfig.indexName); // console.log('[AI Search Modal] performAISearch returned:', result); } catch (orchestratorError) { console.error('[AI Search Modal] performAISearch error:', orchestratorError); throw orchestratorError; } // P3-002: Check for race condition after async operation if (isCancelled) { // console.log('[AI Search Modal] Aborted after performAISearch - isCancelled is true'); return; } if (result.documents.length === 0) { throw new Error(modalTexts.noDocumentsFoundError); } // console.log('[AI Search Modal] Setting state after performAISearch'); setRetrievedContent(result.documents); setAnswer(result.answer); // Week 2 Enhancement: Capture validation data from backend if (result.validation) { setValidation(result.validation); } // Week 3 Enhancement: Capture query analysis data if (result.queryAnalysis) { setQueryAnalysis(result.queryAnalysis); } // Stage 3: Capture enhancement data if (result.enhancement) { setEnhancement(result.enhancement); } } // console.log('[AI Search Modal] Search completed, result:', { // hasAnswer: !!result?.answer, // answerLength: result?.answer?.length, // documentsCount: result?.documents?.length || result?.sources?.length // }); if (!result || !result.answer) { console.error('[AI Search Modal] Invalid result from orchestrator:', result); throw new Error('Search completed but no answer was returned'); } try { // Cache the complete response if caching is enabled if (enableCaching) { cache.set(query, result.answer, '', result.documents || result.sources); } // Track successful AI query if (config?.onAIQuery) { config.onAIQuery(query, true); } // console.log('[AI Search Modal] About to set loading to false'); setLoading(false); // console.log('[AI Search Modal] Loading set to false'); } catch (err) { console.error('[AI Search Modal] Error in post-search processing:', err); setLoading(false); // Ensure loading is set to false even if there's an error throw err; // Re-throw to be caught by outer error handler } } else { // Fallback to original search behavior if (searchResults.length === 0) { throw new Error(modalTexts.noDocumentsFoundError); } setSearchStep({ step: 'documents-found', message: modalTexts.retrievingText, progress: 50 }); // Simple content extraction from search results const maxDocs = 5; // Backend will handle the actual document limit const documents = searchResults.slice(0, maxDocs).map((result) => { let content = ''; // Build content from hierarchy if (result.hierarchy) { const levels = ['lvl0', 'lvl1', 'lvl2', 'lvl3', 'lvl4', 'lvl5']; levels.forEach(level => { const value = result.hierarchy[level]; if (value) { content += `${value}\n`; } }); } // Add snippet if (result._snippetResult?.content?.value) { content += `\n${result._snippetResult.content.value}`; } // Add full content if available if (result.content) { content += `\n${result.content}`; } return { url: result.url || '', title: result.hierarchy?.lvl1 || result.hierarchy?.lvl0 || 'Document', content: content || 'No content available' }; }); // P3-002: Check for race condition before setting fallback data if (isCancelled) return; setRetrievedContent(documents); } } catch (err) { // P3-001: Enhanced error handling with error boundary integration console.error('[AISearchModal] Error in fetchContent:', err); setSearchStep(null); // Determine if this is a critical error that should trigger error boundary const isCriticalError = err instanceof TypeError || err.message?.includes('network') || err.message?.includes('fetch') || err.name === 'AbortError'; if (isCriticalError) { // For critical errors, trigger error boundary errorHandler(err, { context: 'fetchContent', query }); } else { // For non-critical errors, show user-friendly message setError(`Unable to find relevant documentation: ${err.message || 'Unknown error'}. Please try a different search query.`); setLoading(false); // Track failed AI query if function is provided if (config?.onAIQuery) { config.onAIQuery(query, false); } } } } fetchContent(); // P3-002: Cleanup function to prevent race conditions return () => { // Only cancel if query is changing if (queryChanged) { // console.log('[AI Search Modal] fetchContent cleanup - query changed, setting isCancelled to true'); isCancelled = true; // Cancel orchestrator operations if they're running if (orchestratorRef.current && typeof orchestratorRef.current.cancelAllOperations === 'function') { orchestratorRef.current.cancelAllOperations(); } } }; }, [query, searchResults.length, algoliaConfig?.indexName]); // Remove config-based dependencies that can change // P3-001: Enhanced markdown response handling with error protection useEffect(() => { const errorHandler = createErrorHandler('AISearchModal-markdownProcessing'); try { if (answer) { let markdownContent = answer; // Add source references if we have retrieved content if (retrievedContent.length > 0) { const sourcesMarkdown = ` --- **${modalTexts.sourcesHeaderText}** ${retrievedContent.slice(0, 5).map((doc, idx) => `${idx + 1}. [${doc.title}](${doc.url})`).join('\n')}`; markdownContent += sourcesMarkdown; } setFormattedAnswer(markdownContent); } } catch (err) { console.error('[AISearchModal] Error processing markdown:', err); // For markdown processing errors, fallback to plain text if (answer) { setFormattedAnswer(answer); } } }, [answer, modalTexts.sourcesHeaderText, retrievedContent, createErrorHandler]); // P3-001: Enhanced DOM manipulation with error protection useEffect(() => { try { if (markdownRef.current && !loading && !error) { // Find all blockquotes const blockquotes = markdownRef.current.querySelectorAll('blockquote'); blockquotes.forEach(blockquote => { try { // Find the first strong tag in the blockquote const firstStrong = blockquote.querySelector('p:first-child strong:first-child'); if (firstStrong) { const text = firstStrong.textContent?.trim().toLowerCase(); // Add appropriate class based on content if (text === 'note' || text === 'info') { blockquote.classList.add('note'); } else if (text === 'tip') { blockquote.classList.add('tip'); } else if (text === 'warning') { blockquote.classList.add('warning'); } else if (text === 'danger' || text === 'caution') { blockquote.classList.add('danger'); } } } catch (blockquoteError) { console.warn('[AISearchModal] Error processing blockquote:', blockquoteError); // Continue with other blockquotes even if one fails } }); } } catch (err) { console.error('[AISearchModal] Error in DOM manipulation:', err); // DOM manipulation errors are non-critical, just log and continue } }, [loading, error, formattedAnswer]); // Create the classes for the modal based on Docusaurus theme variables const modalClasses = { overlay: [ 'ai-modal-overlay', themeConfig?.colorMode?.respectPrefersColorScheme ? 'respect-color-scheme' : '', ].filter(Boolean).join(' '), content: [ 'ai-modal-content', themeConfig?.hideableSidebar ? 'hideable-sidebar' : '', ].filter(Boolean).join(' ') }; // Stage 2: Source type indicator component const SourceTypeIndicator = ({ source }) => { const getSourceIcon = (sourceType) => { switch (sourceType) { case 'documentation': return '📚'; case 'github': return '🐙'; case 'blog': return '📝'; case 'changelog': return '📋'; default: return '📄'; } }; const getSourceLabel = (sourceType) => { switch (sourceType) { case 'documentation': return 'Documentation'; case 'github': return 'GitHub'; case 'blog': return 'Blog'; case 'changelog': return 'Changelog'; default: return 'Unknown'; } }; return (React.createElement("span", { className: `ai-search-source-indicator ai-search-source-${source.source}` }, getSourceIcon(source.source), " ", getSourceLabel(source.source))); }; // Updated sources section render function const renderSources = () => { const sourcesSection = (() => { if (multiSourceResults && multiSourceResults.sources.length > 0) { return (React.createElement("div", { className: "ai-search-sources-section" }, React.createElement("h3", { className: "ai-search-sources-title" }, "Sources"), React.createElement("div", { className: "ai-search-aggregation-metrics" }, React.createElement("span", { className: "ai-search-total-sources" }, multiSourceResults.aggregationMetrics.totalSources, " sources found"), React.createElement("span", { className: "ai-search-confidence-score" }, "Confidence: ", multiSourceResults.aggregationMetrics.confidenceScore, "%")), React.createElement("div", { className: "ai-search-sources-list" }, multiSourceResults.sources.slice(0, 8).map((source, idx) => (React.createElement("div", { key: idx, className: "ai-search-source-item" }, React.createElement(SourceTypeIndicator, { source: source }), React.createElement("a", { href: source.url, target: "_blank", rel: "noopener noreferrer" }, source.title), source.metadata.author && (React.createElement("span", { className: "ai-search-source-author" }, "by ", source.metadata.author)), source.metadata.timestamp && (React.createElement("span", { className: "ai-search-source-timestamp" }, new Date(source.metadata.timestamp).toLocaleDateString())))))))); } // Fallback to traditional sources display if (retrievedContent.length > 0) { return (React.createElement("div", { className: "ai-search-sources-section" }, React.createElement("h3", { className: "ai-search-sources-title" }, "Sources"), React.createElement("div", { className: "ai-search-sources-list" }, retrievedContent.slice(0, 5).map((doc, idx) => (React.createElement("div", { key: idx, className: "ai-search-source-item" }, React.createElement(SourceTypeIndicator, { source: { source: 'documentation', title: doc.title, url: doc.url, content: doc.content, metadata: { weight: 0.5 } } }), React.createElement("a", { href: doc.url, target: "_blank", rel: "noopener noreferrer" }, doc.title))))))); } return null; })(); return (React.createElement(React.Fragment, null, sourcesSection)); }; // console.log('[AI Search Modal] Rendering with state:', { // loading, // hasError: !!error, // hasAnswer: !!answer, // formattedAnswerLength: formattedAnswer.length // }); return (React.createElement("div", { className: modalClasses.overlay, onClick: (e) => { if (e.target === e.currentTarget) { handleClose(); } } }, React.createElement("div", { className: modalClasses.content }, React.createElement("div", { className: "ai-modal-header" }, React.createElement("h3", null, modalTexts.modalTitle), React.createElement("button", { className: "ai-modal-close", onClick: handleClose, "aria-label": modalTexts.closeButtonAriaLabel }, "\u00D7")), React.createElement("div", { className: "ai-modal-body" }, React.createElement("div", { className: "ai-question" }, React.createElement("strong", null, modalTexts.questionPrefix), " ", query), loading ? (React.createElement("div", { className: "ai-loading" }, React.createElement("div", { className: "ai-loading-spinner" }), React.createElement("div", { className: "ai-loading-status" }, searchStep ? (React.createElement(React.Fragment, null, React.createElement("div", { className: "ai-progress-bar" }, React.createElement("div", { className: "ai-progress-fill", style: { width: `${searchStep.progress}%` } })), React.createElement("div", { className: "ai-step-message" }, searchStep.message), searchStep.details && (React.createElement("div", { className: "ai-progress-details" }, searchStep.details.keywords && searchStep.details.keywords.length > 0 && (React.createElement("div", { className: "ai-keywords-section" }, React.createElement("strong", null, modalTexts.searchKeywordsLabel), React.createElement("ul", { className: "ai-keywords-list" }, searchStep.details.keywords.map((keyword, idx) => (React.createElement("li", { key: idx, className: "ai-keyword-item" }, keyword)))))), searchStep.details.documentsFound !== undefined && searchStep.details.documentsFound > 0 && (React.createElement("div", { className: "ai-documents-section" }, React.createElement("strong", null, modalTexts.documentsFoundLabel.replace('{count}', searchStep.details.documentsFound.toString())), searchStep.details.documentLinks && searchStep.details.documentLinks.length > 0 && (React.createElement("ul", { className: "ai-document-links" }, searchStep.details.documentLinks.slice(0, 5).map((link, idx) => (React.createElement("li", { key: idx, className: "ai-document-link-item" }, React.createElement("a", { href: link, target: "_blank", rel: "noopener noreferrer" }, link.split('/').pop() || 'Document')))), searchStep.details.documentLinks.length > 5 && (React.createElement("li", { className: "ai-document-link-more" }, modalTexts.documentsMoreText.replace('{count}', (searchStep.details.documentLinks.length - 5).toString()))))))))))) : (React.createElement("p", null, modalTexts.loadingText))))) : error ? (React.createElement("div", { className: "ai-error" }, React.createElement("div", { className: "alert alert--danger error-message" }, React.createElement("p", null, error)), React.createElement("div", { className: "ai-error-actions" }, React.createElement("button", { className: "button button--primary ai-retry-button", onClick: handleRetry }, React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, React.createElement("path", { d: "M21 2v6h-6" }), React.createElement("path", { d: "M3 12a9 9 0 0 1 15-6.7L21 8" }), React.createElement("path", { d: "M3 22v-6h6" }), React.createElement("path", { d: "M21 12a9 9 0 0 1-15 6.7L3 16" })), modalTexts.retryButtonText)), searchResults.length > 0 && (React.createElement("div", { className: "ai-search-links" }, React.createElement("p", null, modalTexts.searchLinksHelpText), React.createElement("ul", null, searchResults.slice(0, 3).map((result, idx) => (React.createElement("li", { key: idx }, React.createElement("a", { href: result.url, target: "_blank", rel: "noopener noreferrer" }, result.hierarchy?.lvl0 || result.hierarchy?.lvl1 || 'Result ' + (idx + 1)))))))))) : (React.createElement("div", { className: "ai-answer" }, validation && (React.createElement("div", { className: "ai-validation-section" }, validation.confidence && (React.createElement("div", { className: `ai-confidence ai-confidence-${validation.confidence.toLowerCase()}` }, React.createElement("span", { className: "ai-confidence-label" }, "Confidence:"), React.createElement("span", { className: "ai-confidence-value" }, validation.confidence))), validation.qualityMetrics && (React.createElement("div", { className: "ai-quality-indicators" }, validation.qualityMetrics.hasCodeExamples && (React.createElement("span", { className: "ai-quality-badge ai-has-code" }, React.createElement("span", { className: "ai-badge-icon" }, "\uD83D\uDCBB"), "Code Examples")), validation.qualityMetrics.hasStepByStep && (React.createElement("span", { className: "ai-quality-badge ai-has-steps" }, React.createElement("span", { className: "ai-badge-icon" }, "\uD83D\uDCCB"), "Step-by-Step")), validation.hasSources && (React.createElement("span", { className: "ai-quality-badge ai-has-sources" }, React.createElement("span", { className: "ai-badge-icon" }, "\uD83D\uDD17"), "Sources Cited")))), enhancement && (React.createElement("div", { className: "ai-enhancement-indicators" }, enhancement.fineTunedModelUsed && (React.createElement("span", { className: "ai-enhancement-badge ai-finetuned-model" }, React.createElement("span", { className: "ai-badge-icon" }, "\uD83E\uDDE0"), "Fine-Tuned Model")), enhancement.recursiveEnhanced && (React.createElement("span", { className: "ai-enhancement-badge ai-recursive-enhanced" }, React.createElement("span", { className: "ai-badge-icon" }, "\uD83D\uDD04"), "Recursively Enhanced")), enhancement.documentsAnalyzed && enhancement.documentsAnalyzed > 0 && (React.createElement("span", { className: "ai-enhancement-badge ai-docs-analyzed" }, React.createElement("span", { className: "ai-badge-icon" }, "\uD83D\uDCDA"), enhancement.documentsAnalyzed, " Documents Analyzed")))))), React.createElement("div", { className: "ai-response" }, validation?.isNotFound ? (React.createElement("div", { className: "ai-not-found" }, React.createElement("div", { className: "alert alert--info" }, React.createElement("div", { className: "ai-response-text markdown-body", ref: markdownRef }, markdownComponentsLoaded ? (React.createElement(markdownComponentsLoaded.ReactMarkdown, { remarkPlugins: [markdownComponentsLoaded.remarkGfm], rehypePlugins: [markdownComponentsLoaded.rehypeRaw], components: { // Override pre rendering to avoid nesting // @ts-ignore - The type definition for pre component in ReactMarkdown is complex pre: ({ children }) => children, // @ts-ignore - The type definition for code component in ReactMarkdown is complex code: (codeProps) => { const { className, children } = codeProps; // Check if this is a code block or inline code const match = /language-(\w+)/.exec(className || '