diff --git a/docusaurus.config.js b/docusaurus.config.js index 462ea55b05..b7614e42b6 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -131,6 +131,14 @@ module.exports = { require.resolve('./src/client-modules/trackTrialClick.js'), require.resolve('./src/client-modules/fixAnchorScroll.js'), ], + customFields: { + askAi: { + // Keep Algolia Ask AI feedback targets out of source control. + // This Google Form is a shared public collection endpoint for Algolia Ask AI feedback. + feedbackFormUrl: + 'https://docs.google.com/forms/d/e/1FAIpQLSdJOWBAXsM86C-buZm0N3a9vwBOxxjOfBe7jiEn6PkLqctv5A/viewform?usp=pp_url&entry.778922322=a&entry.898262107=a&entry.1217042173=a&entry.1416546939=a&entry.1571666595=a&entry.554631687=a', + }, + }, storage: { type: 'localStorage', namespace: true, @@ -388,6 +396,13 @@ module.exports = { contextualSearch: false, searchPagePath: 'docs-search', // Default value is 'search'; renamed to 'docs-search' so it doesn't conflict with '/Search' redirect insights: true, + askAi: { + assistantId: 'T7pp7iENesuU', + indexName: 'crawler_sumodocs', + apiKey: 'fb2f4e1fb40f962900631121cb365549', + appId: '2SJPGMLW1Q', + suggestedQuestions: true, + }, insightsConfig: { useCookie: true, // alt to useCookie: true, }, @@ -549,6 +564,12 @@ module.exports = { type: 'search', position: 'left', }, + { + type: 'html', + position: 'left', + className: 'navbar-ask-ai-item', + value: '', + }, ], }, footer: { diff --git a/package.json b/package.json index ce7ccae7d0..52c566792f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "@babel/runtime-corejs3": "7.26.10", "@braintree/sanitize-url": "^6.0.1", "@csstools/selector-resolve-nested": "3.1.0", - "@docsearch/css": "4", + "@docsearch/core": "4.6.2", + "@docsearch/css": "4.6.2", + "@docsearch/react": "4.6.2", "@docusaurus/bundler": "^3.10.1", "@docusaurus/core": "^3.10.1", "@docusaurus/cssnano-preset": "^3.10.1", diff --git a/src/components/AskAiButton/index.tsx b/src/components/AskAiButton/index.tsx new file mode 100644 index 0000000000..eab4142246 --- /dev/null +++ b/src/components/AskAiButton/index.tsx @@ -0,0 +1,50 @@ +/** + * Ask AI Button Component + * + * Renders a button in the navbar that opens the Algolia Ask AI sidepanel. + * Includes keyboard shortcut support (Cmd/Ctrl + I). + * + * The sidepanel itself is rendered in Root.tsx, outside the navbar portal, + * so it is never unmounted by navbar re-renders on resize. + */ + +import React, { useEffect } from 'react'; +import styles from './styles.module.css'; + +interface AskAiButtonProps { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +export default function AskAiButton({ isOpen, setIsOpen }: AskAiButtonProps) { + // Keyboard shortcut: Cmd/Ctrl + I + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === 'i') { + event.preventDefault(); + setIsOpen(!isOpen); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, setIsOpen]); + + return ( + + ); +} diff --git a/src/components/AskAiButton/styles.module.css b/src/components/AskAiButton/styles.module.css new file mode 100644 index 0000000000..04fb8dc154 --- /dev/null +++ b/src/components/AskAiButton/styles.module.css @@ -0,0 +1,94 @@ +/** + * Ask AI Button - Premium navbar button styling + * Matches Algolia and Stripe documentation design patterns + */ + +.askAiButton { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0 0.875rem; + height: 36px; + background: transparent; + border: 1px solid var(--ifm-color-border-pale); + border-radius: 8px; + color: #5f6368; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + margin-left: -1rem; + position: relative; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); +} + +/* Hover effect with subtle background and shadow */ +.askAiButton:hover { + background: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-300); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); +} + +/* Active/pressed state - minimal animation */ +.askAiButton:active { + transform: scale(0.98); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + transition: transform 0.1s ease; +} + +/* Focus state for accessibility */ +.askAiButton:focus-visible { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 2px; +} + +/* Dark mode styling */ +[data-theme='dark'] .askAiButton { + border-color: #6b6f78; + color: #bdc1c6; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +[data-theme='dark'] .askAiButton:hover { + background: var(--ifm-color-emphasis-200); + border-color: #6b6f78; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +/* Button text and icon */ +.buttonText { + display: inline; + letter-spacing: -0.01em; +} + +.buttonIcon { + display: inline-flex; + align-items: center; + flex-shrink: 0; + transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1); + color: #5f6368; +} + +/* Dark mode icon color to match DocSearch-Search-Icon */ +[data-theme='dark'] .buttonIcon { + color: #bdc1c6; +} + +/* Subtle icon animation on hover */ +.askAiButton:hover .buttonIcon { + opacity: 0.8; +} + +/* Responsive: Hide text on mobile, show icon only */ +@media (max-width: 768px) { + .buttonText { + display: none; + } + + .askAiButton { + padding: 0 0.625rem; + min-width: 36px; + justify-content: center; + } +} diff --git a/src/components/AskAiSidepanel/index.tsx b/src/components/AskAiSidepanel/index.tsx new file mode 100644 index 0000000000..1fba3c91e5 --- /dev/null +++ b/src/components/AskAiSidepanel/index.tsx @@ -0,0 +1,886 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import '@docsearch/css/dist/sidepanel.css'; +import './styles.css'; + +interface AskAiSidepanelProps { + isOpen: boolean; + onClose: () => void; + initialMessage?: { query: string } | null; +} + +const FEEDBACK_FORM_FIELDS = [ + 'created_at', + 'question', + 'answer', + 'details', + 'page_url', + 'client_timestamp', +] as const; + +type FeedbackFormField = (typeof FEEDBACK_FORM_FIELDS)[number]; + +function parseGoogleFormConfig(formUrl: string): { + actionUrl: string; + fields: FeedbackFormField[]; + entryMap: Partial>; +} | null { + try { + const url = new URL(formUrl); + const entryKeys = Array.from(url.searchParams.keys()).filter((key) => + key.startsWith('entry.') + ); + + if ( + !url.hostname.includes('docs.google.com') || + !url.pathname.endsWith('/viewform') || + entryKeys.length < FEEDBACK_FORM_FIELDS.length + ) { + return null; + } + + const entryMap = {} as Partial>; + FEEDBACK_FORM_FIELDS.forEach((field, index) => { + entryMap[field] = entryKeys[index]; + }); + + return { + actionUrl: `${url.origin}${url.pathname.replace( + /\/viewform$/, + '/formResponse' + )}`, + fields: [...FEEDBACK_FORM_FIELDS], + entryMap, + }; + } catch { + return null; + } +} + +export default function AskAiSidepanel({ + isOpen, + onClose, + initialMessage, +}: AskAiSidepanelProps) { + const { siteConfig } = useDocusaurusContext(); + const algoliaConfig = (siteConfig.themeConfig as any)?.algolia; + const askAiConfig = { + ...(algoliaConfig?.askAi || {}), + ...(((siteConfig.customFields as any)?.askAi || {}) as Record< + string, + unknown + >), + }; + const feedbackFormUrl = askAiConfig?.feedbackFormUrl || ''; + const [SidepanelComponent, setSidepanelComponent] = useState(null); + const [isExpanded, setIsExpanded] = useState(false); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [shortcutHint, setShortcutHint] = useState('Ctrl + I'); + const [headerActionsEl, setHeaderActionsEl] = useState( + null + ); + const [feedbackModal, setFeedbackModal] = useState<{ + question: string; + answer: string; + } | null>(null); + const [feedbackDetails, setFeedbackDetails] = useState(''); + const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false); + const [feedbackSubmitError, setFeedbackSubmitError] = useState(''); + const [feedbackSubmittedNotice, setFeedbackSubmittedNotice] = useState(''); + const isResizingRef = useRef(false); + const feedbackTextareaRef = useRef(null); + + // Lazy load the Algolia Sidepanel component + useEffect(() => { + if (isOpen && !SidepanelComponent) { + import('@docsearch/react/sidepanel').then((module) => { + setSidepanelComponent(() => module.Sidepanel); + }); + } + }, [isOpen, SidepanelComponent]); + + useEffect(() => { + if (typeof navigator === 'undefined') return; + + if (/(Mac|iPhone|iPad|iPod)/i.test(navigator.platform)) { + setShortcutHint('\u2318 + I'); + } else { + setShortcutHint('Ctrl + I'); + } + }, []); + + useEffect(() => { + if (!isOpen) return; + + const tipText = `Tip: Start a new chat with ${shortcutHint}`; + const checkInterval = setInterval(() => { + const intro = document.querySelector( + '.DocSearch-Sidepanel-NewConversationScreen .DocSearch-Sidepanel-Screen--introduction' + ) as HTMLElement | null; + + if (intro) { + intro.setAttribute('data-shortcut-tip', tipText); + clearInterval(checkInterval); + } + }, 100); + + return () => clearInterval(checkInterval); + }, [isOpen, shortcutHint]); + + // Track resize so we can suppress Algolia's resize-triggered onClose + useEffect(() => { + let timer: ReturnType; + const onResize = () => { + isResizingRef.current = true; + clearTimeout(timer); + timer = setTimeout(() => { + isResizingRef.current = false; + }, 500); + }; + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + clearTimeout(timer); + }; + }, []); + + // Reset expanded state when panel closes + useEffect(() => { + if (!isOpen) { + setIsExpanded(false); + setIsHistoryOpen(false); + setFeedbackModal(null); + setFeedbackDetails(''); + setIsSubmittingFeedback(false); + setFeedbackSubmitError(''); + setFeedbackSubmittedNotice(''); + } + }, [isOpen]); + + useEffect(() => { + if (!feedbackSubmittedNotice) return; + + const timer = window.setTimeout(() => { + setFeedbackSubmittedNotice(''); + }, 3000); + + return () => window.clearTimeout(timer); + }, [feedbackSubmittedNotice]); + + useEffect(() => { + if (!feedbackModal) return; + + const textarea = feedbackTextareaRef.current; + if (textarea) { + textarea.focus(); + const length = textarea.value.length; + textarea.setSelectionRange(length, length); + } + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setFeedbackModal(null); + setFeedbackDetails(''); + setFeedbackSubmitError(''); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [feedbackModal]); + + // Close on Escape key + useEffect(() => { + if (!isOpen) return; + + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose(); + } + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose]); + + // Prevent panel from disappearing on window resize + useEffect(() => { + if (!isOpen) return; + + const handleResize = () => { + const container = document.querySelector( + '.DocSearch-Sidepanel-Container' + ); + if (container && !container.classList.contains('is-open')) { + container.classList.add('is-open'); + } + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [isOpen]); + + useEffect(() => { + if (!isOpen) return; + + const syncHistoryState = () => { + const sidepanel = document.querySelector('.DocSearch-Sidepanel'); + const historyOpen = + sidepanel?.classList.contains('conversation-history') ?? false; + + setIsHistoryOpen((prev) => (prev === historyOpen ? prev : historyOpen)); + }; + + syncHistoryState(); + const syncInterval = setInterval(syncHistoryState, 150); + + return () => { + clearInterval(syncInterval); + }; + }, [isOpen]); + + // Hook into existing expand button + useEffect(() => { + if (!isOpen) return; + + const handleClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const button = target.closest('button'); + + if (!button) return; + + const headerButtons = Array.from( + document.querySelectorAll('.DocSearch-Sidepanel-Header button') + ); + + if (!headerButtons.includes(button)) return; + + const ariaLabel = button.getAttribute('aria-label')?.toLowerCase() || ''; + + if (ariaLabel.includes('close')) return; + if (!button.classList.contains('DocSearch-Sidepanel-Action-expand')) { + return; + } + + setIsExpanded((prev) => !prev); + }; + + const checkInterval = setInterval(() => { + const header = document.querySelector('.DocSearch-Sidepanel-Header'); + if (header) { + header.addEventListener('click', handleClick); + clearInterval(checkInterval); + } + }, 100); + + return () => { + const header = document.querySelector('.DocSearch-Sidepanel-Header'); + if (header) { + header.removeEventListener('click', handleClick); + } + clearInterval(checkInterval); + }; + }, [isOpen]); + + // Add visible shortcuts for Algolia actions hidden in the overflow menu. + useEffect(() => { + if (!isOpen) { + setHeaderActionsEl(null); + return; + } + + const checkInterval = setInterval(() => { + const headerRight = document.querySelector( + '.DocSearch-Sidepanel-Header--right' + ) as HTMLElement | null; + if (headerRight) { + setHeaderActionsEl(headerRight); + clearInterval(checkInterval); + } + }, 100); + + return () => { + clearInterval(checkInterval); + setHeaderActionsEl(null); + }; + }, [isOpen]); + + const triggerHeaderAction = React.useCallback((title: string) => { + const buttons = Array.from( + document.querySelectorAll( + '.DocSearch-Sidepanel-Header button' + ) + ); + const button = buttons.find( + (item) => + item.getAttribute('title') === title && + !item.classList.contains('ask-ai-shortcut-button') + ); + button?.click(); + }, []); + + const focusPromptTextarea = React.useCallback(() => { + let attempts = 0; + const maxAttempts = 12; + + const focus = () => { + const textarea = document.querySelector( + '.DocSearch-Sidepanel-Prompt--textarea' + ) as HTMLTextAreaElement | null; + + if (textarea) { + textarea.focus(); + const length = textarea.value.length; + textarea.setSelectionRange(length, length); + return; + } + + attempts += 1; + if (attempts < maxAttempts) { + window.setTimeout(focus, 50); + } + }; + + focus(); + }, []); + + const handleNewConversation = React.useCallback(() => { + triggerHeaderAction('Start a new conversation'); + focusPromptTextarea(); + }, [focusPromptTextarea, triggerHeaderAction]); + + const handleConversationHistory = React.useCallback(() => { + const sidepanel = document.querySelector('.DocSearch-Sidepanel'); + const isHistoryOpen = sidepanel?.classList.contains('conversation-history'); + + if (isHistoryOpen) { + const backButton = document.querySelector( + '.DocSearch-Sidepanel-Action-back:not(.mobile)' + ); + backButton?.click(); + return; + } + + triggerHeaderAction('Conversation history'); + }, [triggerHeaderAction]); + + const copyTextWithTextarea = React.useCallback((text: string) => { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + textarea.style.pointerEvents = 'none'; + document.body.appendChild(textarea); + textarea.select(); + textarea.setSelectionRange(0, text.length); + + try { + return document.execCommand('copy'); + } finally { + document.body.removeChild(textarea); + } + }, []); + + const copyTextToClipboard = React.useCallback( + (text: string) => { + const copiedWithTextarea = copyTextWithTextarea(text); + + if (navigator.clipboard?.writeText) { + navigator.clipboard.writeText(text).catch(() => { + if (!copiedWithTextarea) { + copyTextWithTextarea(text); + } + }); + } + }, + [copyTextWithTextarea] + ); + + const showCopiedState = React.useCallback((btn: HTMLButtonElement) => { + btn.classList.add('DocSearch-AskAiScreen-CopyButton--copied'); + btn.disabled = true; + setTimeout(() => { + btn.classList.remove('DocSearch-AskAiScreen-CopyButton--copied'); + btn.disabled = false; + }, 1500); + }, []); + + const getAssistantMessageText = React.useCallback((btn: HTMLElement) => { + const response = btn.closest('.DocSearch-AskAiScreen-Response'); + const assistantMessage = + btn.closest('.DocSearch-AskAiScreen-Message--assistant') || + response?.querySelector('.DocSearch-AskAiScreen-Message--assistant'); + const contentEl = assistantMessage?.querySelector( + '.DocSearch-AskAiScreen-MessageContent' + ) as HTMLElement | null; + if (!contentEl) return ''; + + const markdownBlocks = Array.from( + contentEl.querySelectorAll('.DocSearch-Markdown-Content') + ) as HTMLElement[]; + + if (markdownBlocks.length > 0) { + return markdownBlocks + .map((block) => block.innerText.trim()) + .filter(Boolean) + .join('\n\n'); + } + + const clone = contentEl.cloneNode(true) as HTMLElement; + clone + .querySelectorAll( + [ + '.DocSearch-AskAiScreen-Answer-Footer', + '.DocSearch-AskAiScreen-MessageContent-Tool', + '.DocSearch-AskAiScreen-MessageContent-Tool-Query', + '.DocSearch-AskAiScreen-MessageContent-Reasoning', + '.DocSearck-AskAiScreen-MessageContent-Stopped', + ].join(',') + ) + .forEach((el) => el.remove()); + + return clone.innerText.trim(); + }, []); + + const copyAssistantMessage = React.useCallback( + ( + btn: HTMLButtonElement, + event: Pick< + MouseEvent | React.MouseEvent, + 'preventDefault' | 'stopPropagation' + > & { nativeEvent?: MouseEvent } + ) => { + const text = getAssistantMessageText(btn); + if (!text) return false; + + event.preventDefault(); + event.stopPropagation(); + event.nativeEvent?.stopImmediatePropagation(); + if ('stopImmediatePropagation' in event) { + event.stopImmediatePropagation(); + } + + copyTextToClipboard(text); + showCopiedState(btn); + return true; + }, + [copyTextToClipboard, getAssistantMessageText, showCopiedState] + ); + + const getFeedbackContext = React.useCallback( + (btn: HTMLButtonElement) => { + const response = btn.closest('.DocSearch-AskAiScreen-Response'); + const question = + response + ?.querySelector('.DocSearch-AskAiScreen-Query') + ?.textContent?.trim() ?? ''; + const answer = getAssistantMessageText(btn); + + return { question, answer }; + }, + [getAssistantMessageText] + ); + + const closeFeedbackModal = React.useCallback(() => { + setFeedbackModal(null); + setFeedbackDetails(''); + setFeedbackSubmitError(''); + }, []); + + const handleFeedbackButtonClick = React.useCallback( + (btn: HTMLButtonElement) => { + const title = btn.getAttribute('title')?.toLowerCase() ?? ''; + const isNegativeFeedback = title.includes('dislike'); + if (!isNegativeFeedback) { + return false; + } + + const { question, answer } = getFeedbackContext(btn); + + setFeedbackDetails(''); + setFeedbackSubmitError(''); + setFeedbackModal({ + question, + answer, + }); + return true; + }, + [getFeedbackContext] + ); + + // Fix Algolia copy button bug: it can copy the first streamed text chunk + // instead of the rendered answer. Native capture catches the click before + // Algolia's own React handler writes stale content to the clipboard. + useEffect(() => { + if (!isOpen) return; + + const handleDocumentCopyClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const btn = target.closest( + '.DocSearch-AskAiScreen-CopyButton' + ) as HTMLButtonElement | null; + if (!btn) return; + + copyAssistantMessage(btn, event); + }; + + document.addEventListener('click', handleDocumentCopyClick, true); + return () => { + document.removeEventListener('click', handleDocumentCopyClick, true); + }; + }, [copyAssistantMessage, isOpen]); + + useEffect(() => { + if (!isOpen) return; + + const handleDocumentFeedbackClick = (event: MouseEvent) => { + const target = event.target as HTMLElement; + const btn = target.closest( + '.DocSearch-AskAiScreen-Actions button:not(.DocSearch-AskAiScreen-CopyButton)' + ) as HTMLButtonElement | null; + if (!btn) return; + + handleFeedbackButtonClick(btn); + }; + + document.addEventListener('click', handleDocumentFeedbackClick, true); + return () => { + document.removeEventListener('click', handleDocumentFeedbackClick, true); + }; + }, [handleFeedbackButtonClick, isOpen]); + + const handleCopyCapture = React.useCallback( + (e: React.MouseEvent) => { + const target = e.target as HTMLElement; + const btn = target.closest( + '.DocSearch-AskAiScreen-CopyButton' + ) as HTMLButtonElement | null; + if (!btn) return; + + copyAssistantMessage(btn, e); + }, + [copyAssistantMessage] + ); + + const submitFeedbackModal = React.useCallback(async () => { + if (!feedbackModal) return; + const trimmedDetails = feedbackDetails.trim(); + + if (!trimmedDetails) { + closeFeedbackModal(); + return; + } + + const payload = { + details: trimmedDetails, + question: feedbackModal.question, + answer: feedbackModal.answer, + timestamp: new Date().toISOString(), + url: window.location.href, + }; + + setIsSubmittingFeedback(true); + setFeedbackSubmitError(''); + + try { + if (feedbackFormUrl) { + const formConfig = parseGoogleFormConfig(feedbackFormUrl); + if (!formConfig) { + throw new Error('Invalid Google Form feedback URL'); + } + + const formValues: Record = { + created_at: new Date().toISOString(), + question: payload.question, + answer: payload.answer, + details: payload.details, + page_url: payload.url, + client_timestamp: payload.timestamp, + }; + + const formData = new URLSearchParams(); + formConfig.fields.forEach((field) => { + const entryKey = formConfig.entryMap[field]; + if (!entryKey) return; + formData.append(entryKey, formValues[field]); + }); + + await fetch(formConfig.actionUrl, { + method: 'POST', + mode: 'no-cors', + body: formData, + }); + } + + window.dispatchEvent( + new CustomEvent('ask-ai-feedback-submitted', { + detail: payload, + }) + ); + + closeFeedbackModal(); + setFeedbackSubmittedNotice('Additional feedback submitted.'); + } catch (error) { + setFeedbackSubmitError( + error instanceof Error + ? error.message + : 'Failed to submit feedback details' + ); + } finally { + setIsSubmittingFeedback(false); + } + }, [ + closeFeedbackModal, + feedbackDetails, + feedbackFormUrl, + feedbackModal, + ]); + + // Handle submit button state based on textarea content + useEffect(() => { + if (!isOpen) return; + + const updateButtonState = () => { + const textarea = document.querySelector( + '.DocSearch-Sidepanel-Prompt--textarea' + ) as HTMLTextAreaElement; + const submitButton = document.querySelector( + '.DocSearch-Sidepanel-Prompt--submit' + ) as HTMLButtonElement; + + if (textarea && submitButton) { + if (textarea.value.trim().length > 0) { + submitButton.classList.add('has-text'); + } else { + submitButton.classList.remove('has-text'); + } + } + }; + + const autoResize = (el: HTMLTextAreaElement) => { + el.style.height = '0'; + el.style.height = Math.min(el.scrollHeight, 200) + 'px'; + el.style.overflowY = el.scrollHeight > 200 ? 'auto' : 'hidden'; + }; + + const handleInput = (e: Event) => { + updateButtonState(); + autoResize(e.target as HTMLTextAreaElement); + }; + + const checkInterval = setInterval(() => { + const textarea = document.querySelector( + '.DocSearch-Sidepanel-Prompt--textarea' + ) as HTMLTextAreaElement; + if (textarea) { + updateButtonState(); + autoResize(textarea); + textarea.addEventListener('input', handleInput); + textarea.focus(); + clearInterval(checkInterval); + } + }, 100); + + return () => { + clearInterval(checkInterval); + const textarea = document.querySelector( + '.DocSearch-Sidepanel-Prompt--textarea' + ); + if (textarea) { + textarea.removeEventListener('input', handleInput); + } + }; + }, [isOpen]); + + if (!SidepanelComponent) { + return null; + } + + if (!askAiConfig) { + console.error('Ask AI configuration not found in docusaurus.config.js'); + return null; + } + + const { assistantId, indexName, appId, apiKey, suggestedQuestions } = + askAiConfig; + + const handleAlgoliaClose = () => { + if (!isResizingRef.current) onClose(); + }; + + const sidepanel = ( + <> +
+ {}} + onClose={handleAlgoliaClose} + initialMessage={initialMessage || undefined} + suggestedQuestions={suggestedQuestions} + translations={{ + title: 'Ask AI about Sumo Logic', + placeholder: 'Ask a question about Sumo Logic...', + greeting: 'How can I help you with Sumo Logic today?', + introduction: + 'I can help you find information about Sumo Logic features, integrations, troubleshooting, APIs, and best practices across our documentation.', + poweredBy: 'Powered by Algolia', + }} + insights + /> +
+ {headerActionsEl && + createPortal( +
+ + +
, + headerActionsEl + )} + {feedbackModal && + createPortal( +
+
e.stopPropagation()} + > +
+

Thanks for the feedback

+ +
+

+ Provide additional feedback (optional) +

+