import React, { useEffect, useCallback } from 'react'; import styled from 'styled-components'; import type { JSX } from 'react'; import type { AiSearchConversationItem, SearchAiMessageResource, FeedbackType, ToolCall, ContentSegment, } from '@redocly/theme/core/types'; import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks'; import { Button } from '@redocly/theme/components/Button/Button'; import { SearchAiConversationInput } from '@redocly/theme/components/Search/SearchAiConversationInput'; import { AiSearchError, AI_SEARCH_ERROR_CONFIG as ERROR_CONFIG, AiSearchConversationRole, } from '@redocly/theme/core/constants'; import { SearchAiMessage } from '@redocly/theme/components/Search/SearchAiMessage'; import { Admonition } from '@redocly/theme/components/Admonition/Admonition'; import { AiStarsIcon } from '@redocly/theme/icons/AiStarsIcon/AiStarsIcon'; export type SearchAiDialogProps = { response: string | undefined; isGeneratingResponse: boolean; error: AiSearchError | null; resources: SearchAiMessageResource[]; initialMessage?: string; className?: string; conversation: AiSearchConversationItem[]; setConversation: React.Dispatch>; onMessageSent: ( message: string, history?: AiSearchConversationItem[], messageId?: string, ) => void; toolCalls?: ToolCall[]; contentSegments?: ContentSegment[]; }; export function SearchAiDialog({ isGeneratingResponse, response, initialMessage, error, resources, onMessageSent, className, conversation, setConversation, toolCalls = [], contentSegments, }: SearchAiDialogProps): JSX.Element { const { useTranslate } = useThemeHooks(); const { aiAssistant } = useThemeConfig(); const { translate } = useTranslate(); const conversationEndRef = React.useRef(null); const suggestions = aiAssistant?.suggestions; const placeholder = isGeneratingResponse ? translate('search.ai.generatingResponse', 'Generating response...') : conversation.length > 0 ? translate('search.ai.followUpQuestion', 'Ask a follow up question?') : translate('search.ai.placeholder', 'Ask a question...'); const scrollToBottom = useCallback(() => { conversationEndRef.current?.scrollIntoView({ block: 'end' }); }, []); const handleOnMessageSent = useCallback( (message: string) => { if (!message.trim()) { return; } const mappedHistory = conversation.map(({ role, content }) => ({ role, content, })); onMessageSent(message, mappedHistory); setConversation((prev) => [ ...prev, { role: AiSearchConversationRole.USER, content: message }, ]); }, [conversation, onMessageSent, setConversation], ); useEffect(() => { if (!initialMessage?.trim().length) { return; } setConversation((prev) => [ ...prev, { role: AiSearchConversationRole.USER, content: initialMessage }, ]); }, [initialMessage, setConversation]); useEffect(() => { if (response === undefined || conversation.length === 0 || error) { return; } setConversation((prev) => { const lastMessage = prev[prev.length - 1]; const content: string = response || ''; if (lastMessage && lastMessage.role === AiSearchConversationRole.ASSISTANT) { return [ ...prev.slice(0, -1), { role: AiSearchConversationRole.ASSISTANT, content, resources, messageId: lastMessage.messageId, toolCalls, contentSegments, }, ]; } return [ ...prev, { role: AiSearchConversationRole.ASSISTANT, content, resources, toolCalls, contentSegments, }, ]; }); }, [ response, conversation.length, error, resources, toolCalls, contentSegments, setConversation, ]); useEffect(() => { if (error) { setConversation((prev) => prev.slice(0, -1)); } }, [error, setConversation]); useEffect(() => { scrollToBottom(); }, [conversation, isGeneratingResponse, scrollToBottom]); const handleFeedbackChange = useCallback( (messageId: string, feedback: FeedbackType | undefined) => { setConversation((prev) => prev.map((item) => (item.messageId === messageId ? { ...item, feedback } : item)), ); }, [setConversation], ); return ( {!conversation.length && ( {translate( 'search.ai.welcomeText', 'Welcome to AI search! Feel free to ask me anything. How can I help you? ', )} )} {conversation.map((item, index) => ( ))} {error && ( {translate(ERROR_CONFIG[error].messageKey, ERROR_CONFIG[error].messageDefault)} )} {!conversation.length && !error && ( {suggestions?.map((suggestion) => ( ))} )}
); } const SearchAiDialogWrapper = styled.div` display: flex; flex-direction: column; flex: 1 1 auto; width: 100%; min-width: 0; min-height: 0; position: relative; background: var(--search-ai-dialog-bg-color); overflow: hidden; `; const ConversationWrapper = styled.div` flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; padding: var(--search-ai-dialog-body-padding); padding-bottom: 0; display: flex; flex-direction: column; align-items: stretch; gap: var(--search-ai-dialog-body-gap); min-height: 0; > :first-child { margin-top: auto; } > :last-child { margin-bottom: 0; gap: 0; } `; const SuggestionsWrapper = styled.div` display: flex; flex-direction: row; flex-wrap: wrap; gap: var(--search-ai-suggestions-gap); align-items: center; justify-content: center; `; const ConversationInputWrapper = styled.div` padding: var(--search-ai-dialog-input-padding); border-top: var(--search-ai-dialog-input-border); background: var(--search-ai-dialog-input-bg-color); `; const WelcomeWrapper = styled.div` display: flex; flex-direction: row; align-items: center; width: auto; margin: var(--search-ai-welcome-margin); position: relative; `;