diff --git a/nx-dev/feature-ai/src/lib/error-message.tsx b/nx-dev/feature-ai/src/lib/error-message.tsx index a7432776a577d..afe3d075d0383 100644 --- a/nx-dev/feature-ai/src/lib/error-message.tsx +++ b/nx-dev/feature-ai/src/lib/error-message.tsx @@ -1,9 +1,10 @@ +import { type JSX, memo } from 'react'; import { - XCircleIcon, ExclamationTriangleIcon, + XCircleIcon, } from '@heroicons/react/24/outline'; -export function ErrorMessage({ error }: { error: any }): JSX.Element { +function ErrorMessage({ error }: { error: any }): JSX.Element { try { if (error.message) { error = JSON.parse(error.message); @@ -57,3 +58,6 @@ export function ErrorMessage({ error }: { error: any }): JSX.Element { ); } } + +const MemoErrorMessage = memo(ErrorMessage); +export { MemoErrorMessage as ErrorMessage }; diff --git a/nx-dev/feature-ai/src/lib/feed-container.tsx b/nx-dev/feature-ai/src/lib/feed-container.tsx index b16b433a7f73e..d4f9c038d0c8b 100644 --- a/nx-dev/feature-ai/src/lib/feed-container.tsx +++ b/nx-dev/feature-ai/src/lib/feed-container.tsx @@ -1,11 +1,20 @@ import { sendCustomEvent } from '@nx/nx-dev/feature-analytics'; -import { RefObject, useEffect, useRef, useState } from 'react'; +import { + type FormEvent, + type JSX, + RefObject, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { ErrorMessage } from './error-message'; import { Feed } from './feed/feed'; import { LoadingState } from './loading-state'; import { Prompt } from './prompt'; import { getQueryFromUid, storeQueryForUid } from '@nx/nx-dev/util-ai'; import { Message, useChat } from 'ai/react'; +import { cx } from '@nx/nx-dev/ui-primitives'; const assistantWelcome: Message = { id: 'first-custom-message', @@ -17,26 +26,38 @@ const assistantWelcome: Message = { export function FeedContainer(): JSX.Element { const [error, setError] = useState(null); const [startedReply, setStartedReply] = useState(false); + const [isStopped, setStopped] = useState(false); const feedContainer: RefObject | undefined = useRef(null); - const { messages, input, handleInputChange, handleSubmit, isLoading } = - useChat({ - api: '/api/query-ai-handler', - onError: (error) => { - setError(error); - }, - onResponse: (_response) => { - setStartedReply(true); - sendCustomEvent('ai_query', 'ai', 'query', undefined, { - query: input, - }); - setError(null); - }, - onFinish: (response: Message) => { - setStartedReply(false); - storeQueryForUid(response.id, input); - }, - }); + + const { + messages, + setMessages, + input, + handleInputChange, + handleSubmit: _handleSubmit, + stop, + reload, + isLoading, + } = useChat({ + api: '/api/query-ai-handler', + onError: (error) => { + setError(error); + }, + onResponse: (_response) => { + setStartedReply(true); + sendCustomEvent('ai_query', 'ai', 'query', undefined, { + query: input, + }); + setError(null); + }, + onFinish: (response: Message) => { + setStartedReply(false); + storeQueryForUid(response.id, input); + }, + }); + + const hasReply = useMemo(() => messages.length > 0, [messages]); useEffect(() => { if (feedContainer.current) { @@ -46,6 +67,18 @@ export function FeedContainer(): JSX.Element { } }, [messages, isLoading]); + const handleSubmit = (event: FormEvent) => { + setStopped(false); + _handleSubmit(event); + }; + + const handleNewChat = () => { + setMessages([]); + setError(null); + setStartedReply(false); + setStopped(false); + }; + const handleFeedback = (statement: 'good' | 'bad', chatItemUid: string) => { const query = getQueryFromUid(chatItemUid); sendCustomEvent('ai_feedback', 'ai', statement, undefined, { @@ -53,6 +86,16 @@ export function FeedContainer(): JSX.Element { }); }; + const handleStopGenerating = () => { + setStopped(true); + stop(); + }; + + const handleRegenerate = () => { + setStopped(false); + reload(); + }; + return ( <> {/*WRAPPER*/} @@ -71,25 +114,33 @@ export function FeedContainer(): JSX.Element {
- handleFeedback(statement, chatItemUid) - } + onFeedback={handleFeedback} /> {/* Change this message if it's loading but it's writing as well */} {isLoading && !startedReply && } {error && } -
+
diff --git a/nx-dev/feature-ai/src/lib/feed/feed-answer.tsx b/nx-dev/feature-ai/src/lib/feed/feed-answer.tsx index 678fb96ea834e..9ede178fee62c 100644 --- a/nx-dev/feature-ai/src/lib/feed/feed-answer.tsx +++ b/nx-dev/feature-ai/src/lib/feed/feed-answer.tsx @@ -66,7 +66,7 @@ export function FeedAnswer({
{!isFirst && ( -
+
{feedbackStatement ? (

{feedbackStatement === 'good' @@ -89,7 +89,7 @@ export function FeedAnswer({ title="Bad" > Bad answer -

diff --git a/nx-dev/feature-ai/src/lib/feed/feed.tsx b/nx-dev/feature-ai/src/lib/feed/feed.tsx index 8b3a00cb383d7..aaa313a007063 100644 --- a/nx-dev/feature-ai/src/lib/feed/feed.tsx +++ b/nx-dev/feature-ai/src/lib/feed/feed.tsx @@ -4,10 +4,10 @@ import { Message } from 'ai/react'; export function Feed({ activity, - handleFeedback, + onFeedback, }: { activity: Message[]; - handleFeedback: (statement: 'bad' | 'good', chatItemUid: string) => void; + onFeedback: (statement: 'bad' | 'good', chatItemUid: string) => void; }) { return (
@@ -21,7 +21,7 @@ export function Feed({ - handleFeedback(statement, activityItem.id) + onFeedback(statement, activityItem.id) } isFirst={activityItemIdx === 0} /> diff --git a/nx-dev/feature-ai/src/lib/prompt.tsx b/nx-dev/feature-ai/src/lib/prompt.tsx index 64fb78e4c69e1..48ccd2ac9e3b5 100644 --- a/nx-dev/feature-ai/src/lib/prompt.tsx +++ b/nx-dev/feature-ai/src/lib/prompt.tsx @@ -1,40 +1,108 @@ import { ChangeEvent, FormEvent, useEffect, useRef } from 'react'; -import { PaperAirplaneIcon } from '@heroicons/react/24/outline'; +import { + ArrowPathIcon, + PaperAirplaneIcon, + PlusIcon, + StopIcon, +} from '@heroicons/react/24/outline'; import { Button } from '@nx/nx-dev/ui-common'; import Textarea from 'react-textarea-autosize'; import { ChatRequestOptions } from 'ai'; +import { cx } from '@nx/nx-dev/ui-primitives'; export function Prompt({ - isDisabled, - handleSubmit, - handleInputChange, + isGenerating, + showNewChatCta, + showRegenerateCta, + onSubmit, + onInputChange, + onNewChat, + onStopGenerating, + onRegenerate, input, }: { - isDisabled: boolean; - handleSubmit: ( + isGenerating: boolean; + showNewChatCta: boolean; + showRegenerateCta: boolean; + onSubmit: ( e: FormEvent, chatRequestOptions?: ChatRequestOptions | undefined ) => void; - handleInputChange: ( + onInputChange: ( e: ChangeEvent | ChangeEvent ) => void; + onNewChat: () => void; + onStopGenerating: () => void; + onRegenerate: () => void; input: string; }) { const formRef = useRef(null); const inputRef = useRef(null); useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); + if (!isGenerating) inputRef.current?.focus(); + }, [isGenerating]); + const handleSubmit = (event: FormEvent) => { + if (inputRef.current?.value.trim()) onSubmit(event); + else event.preventDefault(); + }; + + const handleNewChat = () => { + onNewChat(); + inputRef.current?.focus(); + }; + + const handleStopGenerating = () => { + onStopGenerating(); + inputRef.current?.focus(); + }; return (
+
+ {isGenerating && ( + + )} + {showNewChatCta && ( + + )} + {showRegenerateCta && ( + + )} +