Skip to content

Commit

Permalink
feat(nx-dev): add "new chat" button to AI page
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysoo committed Sep 13, 2023
1 parent 5056d6c commit c51b8b9
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 41 deletions.
8 changes: 6 additions & 2 deletions nx-dev/feature-ai/src/lib/error-message.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -57,3 +58,6 @@ export function ErrorMessage({ error }: { error: any }): JSX.Element {
);
}
}

const MemoErrorMessage = memo(ErrorMessage);
export { MemoErrorMessage as ErrorMessage };
60 changes: 38 additions & 22 deletions nx-dev/feature-ai/src/lib/feed-container.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { sendCustomEvent } from '@nx/nx-dev/feature-analytics';
import { RefObject, useEffect, useRef, useState } from 'react';
import { type JSX, RefObject, useEffect, useRef, useState } from 'react';
import { ErrorMessage } from './error-message';
import { Feed } from './feed/feed';
import { LoadingState } from './loading-state';
Expand All @@ -19,24 +19,37 @@ export function FeedContainer(): JSX.Element {
const [startedReply, setStartedReply] = useState(false);

const feedContainer: RefObject<HTMLDivElement> | 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,
stop: handleStopGenerating,
reload: handleRegenerate,
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 handleNewChat = () => {
setMessages([]);
setError(null);
};

useEffect(() => {
if (feedContainer.current) {
Expand Down Expand Up @@ -86,10 +99,13 @@ export function FeedContainer(): JSX.Element {

<div className="sticky bottom-0 left-0 right-0 w-full pt-6 pb-4 bg-gradient-to-t from-white via-white dark:from-slate-900 dark:via-slate-900">
<Prompt
handleSubmit={handleSubmit}
handleInputChange={handleInputChange}
onSubmit={handleSubmit}
onInputChange={handleInputChange}
onNewChat={handleNewChat}
onStopGenerating={handleStopGenerating}
onRegenerate={handleRegenerate}
input={input}
isDisabled={isLoading}
isLoading={isLoading}
/>
</div>
</div>
Expand Down
113 changes: 98 additions & 15 deletions nx-dev/feature-ai/src/lib/prompt.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,123 @@
import { ChangeEvent, FormEvent, useEffect, useRef } from 'react';
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
import { ChangeEvent, FormEvent, useEffect, useRef, useState } from 'react';
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,
isLoading,
onSubmit,
onInputChange,
onNewChat,
onStopGenerating,
onRegenerate,
input,
}: {
isDisabled: boolean;
handleSubmit: (
isLoading: boolean;
onSubmit: (
e: FormEvent<HTMLFormElement>,
chatRequestOptions?: ChatRequestOptions | undefined
) => void;
handleInputChange: (
onInputChange: (
e: ChangeEvent<HTMLTextAreaElement> | ChangeEvent<HTMLInputElement>
) => void;
onNewChat: () => void;
onStopGenerating: () => void;
onRegenerate: () => void;
input: string;
}) {
const formRef = useRef<HTMLFormElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [isStopped, setStopped] = useState(false);

useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
if (!isLoading) inputRef.current?.focus();
}, [isLoading]);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
setStopped(false);
if (inputRef.current?.value.trim()) onSubmit(event);
else event.preventDefault();
};

const handleNewChat = () => {
onNewChat();
inputRef.current?.focus();
};

const handleStopGenerating = () => {
setStopped(true);
onStopGenerating();
inputRef.current?.focus();
};

const handleOnRegenerate = () => {
setStopped(false);
onRegenerate();
};

return (
<form
ref={formRef}
onSubmit={handleSubmit}
className="relative flex gap-2 max-w-2xl mx-auto py-0 px-2 shadow-lg rounded-md border border-slate-300 bg-white dark:border-slate-900 dark:bg-slate-700"
>
{isLoading && (
<div className="absolute -top-full right-0 mt-1">
<Button
variant="secondary"
size="small"
className={cx('bg-white dark:bg-slate-900')}
onClick={handleStopGenerating}
>
<StopIcon aria-hidden="true" className="h-5 w-5" />
<span className="text-base">Stop generating</span>
</Button>
</div>
)}
{isStopped && (
<div className="absolute -top-full right-0 mt-1">
<Button
variant="secondary"
size="small"
className={cx('bg-white dark:bg-slate-900')}
onClick={handleOnRegenerate}
>
<ArrowPathIcon aria-hidden="true" className="h-5 w-5" />
<span className="text-base">Regenerated</span>
</Button>
</div>
)}
<div className="flex pb-2 relative group">
<Button
variant="secondary"
size="small"
rounded="full"
className="self-end w-12 h-12 disabled:cursor-not-allowed"
onClick={handleNewChat}
>
<div hidden className="sr-only">
New chat
</div>
<PlusIcon aria-hidden="true" className="h-5 w-5" />
</Button>
<span
aria-hidden="true"
className={cx(
'opacity-0 group-hover:opacity-100 transition-opacity delay-75',
'w-max py-1.5 px-3 rounded-md border shadow-md',
'text-sm bg-white text-slate-700 border-slate-300 dark:bg-slate-900 dark:text-slate-200 dark:border-slate-300',
'absolute top-0 left-1/2 -translate-x-1/2 -translate-y-full'
)}
>
New chat
</span>
</div>
<div className="overflow-y-auto w-full h-full max-h-[300px]">
<Textarea
onKeyDown={(event) => {
Expand All @@ -49,10 +132,10 @@ export function Prompt({
}}
ref={inputRef}
value={input}
onChange={handleInputChange}
onChange={onInputChange}
id="query-prompt"
name="query"
disabled={isDisabled}
disabled={isLoading}
className="block w-full p-0 resize-none bg-transparent text-sm placeholder-slate-500 pl-2 py-[1.3rem] focus-within:outline-none focus:placeholder-slate-400 dark:focus:placeholder-slate-300 dark:text-white focus:outline-none focus:ring-0 border-none disabled:cursor-not-allowed"
placeholder="How does caching work?"
rows={1}
Expand All @@ -63,7 +146,7 @@ export function Prompt({
variant="primary"
size="small"
type="submit"
disabled={isDisabled}
disabled={isLoading}
className="self-end w-12 h-12 disabled:cursor-not-allowed"
>
<div hidden className="sr-only">
Expand Down
9 changes: 7 additions & 2 deletions nx-dev/ui-common/src/lib/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type AllowedSizes = 'large' | 'default' | 'small';
interface ButtonProps {
variant?: AllowedVariants;
size?: AllowedSizes;
rounded?: 'full' | 'default';
children: ReactNode | ReactNode[];
}

Expand Down Expand Up @@ -45,12 +46,15 @@ function ButtonInner({
children,
variant = 'primary',
size = 'default',
rounded = 'default',
}: ButtonProps): JSX.Element {
return (
<>
<span
className={cx(
'flex h-full w-full items-center justify-center whitespace-nowrap rounded-md border border-transparent font-medium shadow-sm transition',
'flex h-full w-full items-center justify-center whitespace-nowrap',
rounded === 'full' ? 'rounded-full' : 'rounded-md',
'border border-transparent font-medium shadow-sm transition',
variantStyles[variant],
sizes[size]
)}
Expand All @@ -69,11 +73,12 @@ export function Button({
className = '',
variant = 'primary',
size = 'large',
rounded = 'default',
...props
}: ButtonProps & JSX.IntrinsicElements['button']): JSX.Element {
return (
<button {...props} className={getLayoutClassName(className)}>
<ButtonInner variant={variant} size={size}>
<ButtonInner variant={variant} size={size} rounded={rounded}>
{children}
</ButtonInner>
</button>
Expand Down

0 comments on commit c51b8b9

Please sign in to comment.