import { useEffect, useRef, useState } from 'react'; import { Button } from '@nx/nx-dev/ui-common'; import { sendCustomEvent } from '@nx/nx-dev/feature-analytics'; import { renderMarkdown } from '@nx/nx-dev/ui-markdoc'; import { nxDevDataAccessAi, resetHistory, getProcessedHistory, ChatItem, handleFeedback, handleQueryReporting, } from '@nx/nx-dev/data-access-ai'; import { warning, infoBox, noResults } from './utils'; export function FeatureAi(): JSX.Element { const [chatHistory, setChatHistory] = useState([]); const [textResponse, setTextResponse] = useState(''); const [error, setError] = useState(null); const [query, setSearchTerm] = useState(''); const [loading, setLoading] = useState(false); const [feedbackSent, setFeedbackSent] = useState>({}); const [sources, setSources] = useState(''); const [input, setInput] = useState(''); const lastMessageRef: React.RefObject | undefined = useRef(null); useEffect(() => { if (lastMessageRef.current) { lastMessageRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [chatHistory]); const handleSubmit = async () => { setInput(''); if (query) { setChatHistory([ ...(chatHistory ?? []), { role: 'user', content: query }, { role: 'assistant', content: 'Let me think about that...' }, ]); } setLoading(true); setError(null); let completeText = ''; let usage; let sourcesMarkdown = ''; try { const aiResponse = await nxDevDataAccessAi(query, textResponse); completeText = aiResponse.textResponse; setTextResponse(completeText); usage = aiResponse.usage; setSources( JSON.stringify(aiResponse.sources?.map((source) => source.url)) ); sourcesMarkdown = aiResponse.sourcesMarkdown; setLoading(false); } catch (error: any) { setError(error); setLoading(false); } sendCustomEvent('ai_query', 'ai', 'query', undefined, { query, }); handleQueryReporting({ action: 'ai_query', query, ...usage, }); const sourcesMd = sourcesMarkdown.length === 0 ? '' : ` \n {% callout type="info" title="Sources" %} ${sourcesMarkdown} {% /callout %} \n `; if (completeText) { setChatHistory([ ...getProcessedHistory(), { role: 'assistant', content: completeText + sourcesMd }, ]); } }; const handleUserFeedback = (result: 'good' | 'bad', index: number) => { try { sendCustomEvent('ai_feedback', 'ai', result); handleFeedback({ action: 'evaluation', result, query, response: textResponse, sources, }); setFeedbackSent((prev) => ({ ...prev, [index]: true })); } catch (error) { setFeedbackSent((prev) => ({ ...prev, [index]: false })); } }; const handleReset = () => { resetHistory(); setSearchTerm(''); setTextResponse(''); setSources(''); setChatHistory(null); setInput(''); setFeedbackSent({}); }; return (
{infoBox} {warning}
{chatHistory && renderChatHistory(chatHistory)}
{renderChatInput()}
); function renderChatHistory(history: ChatItem[]) { return (
{history.length > 30 && (
You've reached the maximum message history limit. Some previous messages will be removed. You can always start a new chat.
)}{' '} {history.map((chatItem, index) => renderChatItem(chatItem, index, history.length) )}
); } function renderChatItem( chatItem: ChatItem, index: number, historyLength: number ) { return (
{chatItem.role === 'assistant' && ( nx assistant{' '} 🐳 )} {((chatItem.role === 'assistant' && !error) || chatItem.role === 'user') && (
{renderMarkdown(chatItem.content, { filePath: '' }).node}
)} {chatItem.role === 'assistant' && !error && chatHistory?.length && (index === chatHistory.length - 1 && loading ? null : !feedbackSent[ index ] ? (
) : (

{' '} Thank you for your feedback!

))} {error && !loading && chatItem.role === 'assistant' ? ( error['data']?.['no_results'] ? ( noResults ) : (
There was an error: {error['message']}
) ) : null}
); } function renderChatInput() { return (
{ setSearchTerm(event.target.value); setInput(event.target.value); }} onKeyDown={(event) => { if (event.keyCode === 13 || event.key === 'Enter') { handleSubmit(); } }} type="search" />
); } } export default FeatureAi;