feat(nx-cloud): add styles and cleanup to ai feed container (#18593)

This commit is contained in:
Benjamin Cabanes 2023-08-15 12:16:16 -04:00 committed by GitHub
parent 6bebbff5de
commit a29de8b343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 555 additions and 303 deletions

View File

@ -51,7 +51,7 @@ let totalTokensSoFar = 0;
let supabaseClient: SupabaseClient<any, 'public', any>;
export async function nxDevDataAccessAi(
export async function queryAi(
query: string,
aiResponse?: string
): Promise<{
@ -208,7 +208,11 @@ export async function nxDevDataAccessAi(
throw new ApplicationError('Failed to generate completion', error);
}
const message = getMessageFromResponse(response.data);
// Message asking to double-check
const callout: string =
'{% callout type="warning" title="Always double-check!" %}The results may not be accurate, so please always double check with our documentation.{% /callout %}\n';
// Append the warning message asking to double-check!
const message = [callout, getMessageFromResponse(response.data)].join('');
const responseWithoutBadLinks = await sanitizeLinksInResponse(message);
@ -248,13 +252,13 @@ export function getHistory(): ChatItem[] {
return chatFullHistory;
}
export async function handleFeedback(feedback: {}): Promise<
export async function sendFeedbackAnalytics(feedback: {}): Promise<
PostgrestSingleResponse<null>
> {
return supabaseClient.from('feedback').insert(feedback);
}
export async function handleQueryReporting(queryInfo: {}) {
export async function sendQueryAnalytics(queryInfo: {}) {
const { error } = await supabaseClient.from('user_queries').insert(queryInfo);
if (error) {

View File

@ -119,6 +119,18 @@ export class UserError extends ApplicationError {
}
}
/**
* Initializes a chat session by generating the initial chat messages based on the given parameters.
*
* @param {ChatItem[]} chatFullHistory - The full chat history.
* @param {string} query - The user's query.
* @param {string} contextText - The context text or Nx Documentation.
* @param {string} prompt - The prompt message displayed to the user.
* @param {string} [aiResponse] - The AI assistant's response.
* @returns {Object} - An object containing the generated chat messages and updated chat history.
* - chatMessages: An array of chat messages for the chat session.
* - chatHistory: The updated chat history.
*/
export function initializeChat(
chatFullHistory: ChatItem[],
query: string,

View File

@ -1,3 +1 @@
// Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities
export * from './lib/feature-ai';
export * from './lib/feed-container';

View File

@ -0,0 +1,19 @@
import { XCircleIcon } from '@heroicons/react/24/outline';
export function ErrorMessage({ error }: { error: any }): JSX.Element {
return (
<div className="rounded-md bg-red-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">
Oopsies! I encountered an error
</h3>
<div className="mt-2 text-sm text-red-700">{error['message']}</div>
</div>
</div>
</div>
);
}

View File

@ -1,265 +0,0 @@
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<ChatItem[] | null>([]);
const [textResponse, setTextResponse] = useState<undefined | string>('');
const [error, setError] = useState(null);
const [query, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [feedbackSent, setFeedbackSent] = useState<Record<number, boolean>>({});
const [sources, setSources] = useState('');
const [input, setInput] = useState('');
const lastMessageRef: React.RefObject<HTMLDivElement> | 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 (
<div
className="p-2 mx-auto flex h-screen w-full flex-col h-[calc(100vh-150px)]"
id="wrapper"
data-testid="wrapper"
>
<div className="flex-1 overflow-y-auto mb-4">
<div>
{infoBox}
{warning}
</div>
{chatHistory && renderChatHistory(chatHistory)}
</div>
{renderChatInput()}
</div>
);
function renderChatHistory(history: ChatItem[]) {
return (
<div className="mx-auto bg-white p-6 rounded shadow flex flex-col">
{history.length > 30 && (
<div>
You've reached the maximum message history limit. Some previous
messages will be removed. You can always start a new chat.
</div>
)}{' '}
{history.map((chatItem, index) =>
renderChatItem(chatItem, index, history.length)
)}
</div>
);
}
function renderChatItem(
chatItem: ChatItem,
index: number,
historyLength: number
) {
return (
<div
key={index}
ref={index === historyLength - 1 ? lastMessageRef : null}
className={` p-2 m-2 rounded-lg ${
chatItem.role === 'assistant' ? 'bg-blue-200' : 'bg-gray-300'
} ${chatItem.role === 'user' ? 'text-right' : ''} ${
chatItem.role === 'user' ? 'self-end' : ''
}`}
>
{chatItem.role === 'assistant' && (
<strong className="text-gray-700">
nx assistant{' '}
<span role="img" aria-label="Narwhal">
🐳
</span>
</strong>
)}
{((chatItem.role === 'assistant' && !error) ||
chatItem.role === 'user') && (
<div className="text-gray-600 mt-1">
{renderMarkdown(chatItem.content, { filePath: '' }).node}
</div>
)}
{chatItem.role === 'assistant' &&
!error &&
chatHistory?.length &&
(index === chatHistory.length - 1 && loading ? null : !feedbackSent[
index
] ? (
<div>
<Button
variant="primary"
size="small"
onClick={() => handleUserFeedback('good', index)}
>
Answer was helpful{' '}
<span role="img" aria-label="thumbs-up">
👍
</span>
</Button>
<Button
variant="primary"
size="small"
onClick={() => handleUserFeedback('bad', index)}
>
Answer looks wrong{' '}
<span role="img" aria-label="thumbs-down">
👎
</span>
</Button>
</div>
) : (
<p>
<span role="img" aria-label="check">
</span>{' '}
Thank you for your feedback!
</p>
))}
{error && !loading && chatItem.role === 'assistant' ? (
error['data']?.['no_results'] ? (
noResults
) : (
<div>There was an error: {error['message']}</div>
)
) : null}
</div>
);
}
function renderChatInput() {
return (
<div className="flex gap-2 fixed bottom-0 left-0 right-0 p-4 bg-white">
<input
id="search"
name="search"
value={input}
disabled={loading}
className="block w-full rounded-md border border-slate-300 bg-white py-2 pl-10 pr-3 text-sm placeholder-slate-500 transition focus:placeholder-slate-400 dark:border-slate-900 dark:bg-slate-700"
placeholder="What do you want to know?"
onChange={(event) => {
setSearchTerm(event.target.value);
setInput(event.target.value);
}}
onKeyDown={(event) => {
if (event.keyCode === 13 || event.key === 'Enter') {
handleSubmit();
}
}}
type="search"
/>
<Button
variant="primary"
size="small"
disabled={loading}
onClick={() => handleSubmit()}
>
Ask
</Button>
<Button variant="secondary" size="small" onClick={() => handleReset()}>
Ask new question{' '}
<span role="img" aria-label="new question">
🔄
</span>
</Button>
</div>
);
}
}
export default FeatureAi;

View File

@ -0,0 +1,176 @@
import {
ChatItem,
getProcessedHistory,
queryAi,
resetHistory,
sendFeedbackAnalytics,
sendQueryAnalytics,
} from '@nx/nx-dev/data-access-ai';
import { sendCustomEvent } from '@nx/nx-dev/feature-analytics';
import { RefObject, useEffect, useRef, useState } from 'react';
import { ErrorMessage } from './error-message';
import { Feed } from './feed/feed';
import { LoadingState } from './loading-state';
import { Prompt } from './prompt';
import { WarningMessage } from './sidebar/warning-message';
import { formatMarkdownSources } from './utils';
interface LastQueryMetadata {
sources: string[];
textResponse: string;
usage: {
completion_tokens: number;
prompt_tokens: number;
total_tokens: number;
} | null;
}
const assistantWelcome: ChatItem = {
role: 'assistant',
content:
"👋 Hi, I'm your Nx Assistant. With my ocean of knowledge about Nx, I can answer your questions and guide you to the relevant documentation. What would you like to know?",
};
export function FeedContainer(): JSX.Element {
const [chatHistory, setChatHistory] = useState<ChatItem[]>([]);
const [hasError, setHasError] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [lastQueryMetadata, setLastQueryMetadata] =
useState<LastQueryMetadata | null>(null);
const feedContainer: RefObject<HTMLDivElement> | undefined = useRef(null);
useEffect(() => {
if (feedContainer.current) {
const elements =
feedContainer.current.getElementsByClassName('feed-item');
elements[elements.length - 1].scrollIntoView({ behavior: 'smooth' });
}
}, [chatHistory, isLoading]);
const handleSubmit = async (query: string, currentHistory: ChatItem[]) => {
if (!query) return;
currentHistory.push({ role: 'user', content: query });
setIsLoading(true);
setHasError(null);
try {
const lastAnswerChatItem =
currentHistory.filter((item) => item.role === 'assistant').pop() ||
null;
// Use previous assistant's answer if it exists
const aiResponse = await queryAi(
query,
lastAnswerChatItem ? lastAnswerChatItem.content : ''
);
// TODO: Save a list of metadata corresponding to each query
// Saving Metadata for usage like feedback and analytics
setLastQueryMetadata({
sources: aiResponse.sources
? aiResponse.sources.map((source) => source.url)
: [],
textResponse: aiResponse.textResponse,
usage: aiResponse.usage || null,
});
let content = aiResponse.textResponse;
if (aiResponse.sourcesMarkdown.length !== 0)
content += formatMarkdownSources(aiResponse.sourcesMarkdown);
// Saving the new chat history used by AI for follow-up prompts
setChatHistory([
...getProcessedHistory(),
{ role: 'assistant', content },
]);
sendCustomEvent('ai_query', 'ai', 'query', undefined, {
query,
...aiResponse.usage,
});
sendQueryAnalytics({
action: 'ai_query',
query,
...aiResponse.usage,
});
} catch (error: any) {
setHasError(error);
}
setIsLoading(false);
};
const handleFeedback = (statement: 'good' | 'bad', chatItemIndex: number) => {
const question = chatHistory[chatItemIndex - 1];
const answer = chatHistory[chatItemIndex];
sendCustomEvent('ai_feedback', 'ai', statement, undefined, {
query: question ? question.content : 'Could not retrieve the question',
result: answer ? answer.content : 'Could not retrieve the answer',
sources: lastQueryMetadata
? JSON.stringify(lastQueryMetadata.sources)
: 'Could not retrieve last answer sources',
});
sendFeedbackAnalytics({
action: 'evaluation',
result: answer ? answer.content : 'Could not retrieve the answer',
query: question ? question.content : 'Could not retrieve the question',
response: null, // TODO: Use query metadata here
sources: lastQueryMetadata
? JSON.stringify(lastQueryMetadata.sources)
: 'Could not retrieve last answer sources',
});
};
const handleReset = () => {
resetHistory();
setChatHistory([]);
setHasError(null);
};
return (
<>
{/*WRAPPER*/}
<div
id="wrapper"
data-testid="wrapper"
className="relative flex flex-grow flex-col items-stretch justify-start overflow-y-scroll"
>
<div className="mx-auto w-full grow items-stretch px-4 sm:px-8 lg:max-w-4xl">
<div
id="content-wrapper"
className="w-full flex-auto flex-grow flex-col"
>
<div className="relative min-w-0 flex-auto">
{/*MAIN CONTENT*/}
<div
ref={feedContainer}
data-document="main"
className="relative"
>
<Feed
activity={
!!chatHistory.length ? chatHistory : [assistantWelcome]
}
handleFeedback={(statement, chatItemIndex) =>
handleFeedback(statement, chatItemIndex)
}
/>
{isLoading && <LoadingState />}
{hasError && <ErrorMessage error={hasError} />}
<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={(query) => handleSubmit(query, chatHistory)}
isDisabled={isLoading}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}

View File

@ -0,0 +1,19 @@
import { ComponentProps } from 'react';
export function ChatGptLogo(
props: ComponentProps<'svg'> & { title?: string; titleId?: string }
): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
stroke="none"
{...props}
>
<path d="M18.6816 8.18531C18.8527 7.67776 18.94 7.14633 18.9401 6.61141C18.9401 5.72627 18.7011 4.85712 18.2478 4.09329C17.337 2.52895 15.6474 1.56315 13.8188 1.56315C13.4586 1.56315 13.0993 1.60069 12.7471 1.67514C12.2733 1.14844 11.6917 0.726808 11.0407 0.438063C10.3897 0.149318 9.68404 1.53941e-05 8.97029 0H8.93823L8.9262 6.97855e-05C6.71144 6.97855e-05 4.74733 1.41016 4.06649 3.48896C3.36173 3.63138 2.69594 3.92071 2.11369 4.33757C1.53143 4.75444 1.04615 5.28922 0.690321 5.90612C0.238418 6.67455 0.000276531 7.54714 0 8.43556C0.000172701 9.68414 0.469901 10.8882 1.31823 11.8147C1.14699 12.3222 1.05965 12.8537 1.05958 13.3886C1.05966 14.2737 1.29861 15.1429 1.75188 15.9067C2.2909 16.8327 3.11405 17.5659 4.10264 18.0005C5.09122 18.435 6.19415 18.5486 7.25237 18.3248C7.72623 18.8515 8.30786 19.2731 8.95891 19.5619C9.60995 19.8506 10.3156 20 11.0294 20H11.0615L11.0745 19.9999C13.2905 19.9999 15.2539 18.5898 15.9348 16.5091C16.6395 16.3666 17.3053 16.0773 17.8876 15.6604C18.4698 15.2435 18.9552 14.7087 19.311 14.0919C19.7624 13.3241 20.0001 12.4522 20 11.5646C19.9998 10.3161 19.5301 9.11202 18.6818 8.18559L18.6816 8.18531ZM11.0628 18.6925H11.0575C10.1708 18.6922 9.31227 18.3853 8.63118 17.8251C8.67162 17.8036 8.71159 17.7812 8.75105 17.758L12.787 15.4578C12.8877 15.4012 12.9714 15.3194 13.0297 15.2205C13.088 15.1217 13.1187 15.0094 13.1187 14.895V9.27708L14.8246 10.249C14.8336 10.2534 14.8413 10.2599 14.8471 10.2679C14.8529 10.276 14.8565 10.2853 14.8578 10.2951V14.9444C14.8555 17.0115 13.1579 18.6883 11.0628 18.6925ZM2.9014 15.2532C2.56803 14.6844 2.39239 14.039 2.39217 13.382C2.39217 13.1677 2.41114 12.9529 2.44808 12.7417C2.47808 12.7595 2.53045 12.791 2.56802 12.8123L6.60394 15.1125C6.70456 15.1705 6.81899 15.201 6.9355 15.201C7.05201 15.201 7.16643 15.1704 7.26702 15.1124L12.1945 12.3051V14.249L12.1945 14.2523C12.1945 14.2617 12.1923 14.2709 12.1881 14.2793C12.1838 14.2876 12.1777 14.2949 12.1701 14.3006L8.09017 16.6249C7.51288 16.9527 6.85851 17.1253 6.19244 17.1255C5.52566 17.1254 4.87062 16.9523 4.293 16.6237C3.71538 16.295 3.23547 15.8223 2.9014 15.253V15.2532ZM1.83963 6.55967C2.2829 5.79999 2.98282 5.21831 3.8169 4.91644C3.8169 4.95072 3.81492 5.01147 3.81492 5.05364V9.65413L3.81485 9.6579C3.81487 9.77213 3.84552 9.88432 3.9037 9.98308C3.96188 10.0818 4.04551 10.1636 4.1461 10.2202L9.07353 13.027L7.36772 13.9989C7.3593 14.0044 7.34965 14.0077 7.33961 14.0086C7.32957 14.0095 7.31947 14.008 7.31019 14.0041L3.22983 11.6778C2.65301 11.3481 2.17414 10.8746 1.84122 10.3048C1.5083 9.73494 1.33302 9.08877 1.33295 8.43102C1.3332 7.77431 1.50798 7.12914 1.83984 6.55988L1.83963 6.55967ZM15.8552 9.77779L10.9277 6.97059L12.6336 5.99906C12.642 5.99358 12.6517 5.99024 12.6617 5.98934C12.6718 5.98844 12.6819 5.99 12.6912 5.99389L16.7714 8.31819C17.3487 8.64738 17.8281 9.12062 18.1614 9.69041C18.4948 10.2602 18.6703 10.9065 18.6705 11.5644C18.6705 13.1346 17.6774 14.5397 16.1842 15.082V10.344C16.1844 10.3422 16.1844 10.3404 16.1844 10.3387C16.1844 10.2249 16.154 10.1131 16.0961 10.0146C16.0383 9.91613 15.9552 9.83444 15.8552 9.77779ZM17.5531 7.25638C17.5134 7.23239 17.4734 7.20889 17.4332 7.18585L13.3973 4.88558C13.2966 4.82772 13.1823 4.79722 13.0658 4.79718C12.9493 4.79722 12.835 4.82772 12.7343 4.88558L7.80682 7.69284V5.74902L7.80675 5.74567C7.80675 5.72667 7.81588 5.7088 7.83124 5.69742L11.9112 3.37508C12.4883 3.0468 13.1427 2.87398 13.8088 2.87395C15.9066 2.87395 17.6078 4.55252 17.6078 6.62238C17.6077 6.83479 17.5894 7.04681 17.5531 7.25617V7.25638ZM6.87936 10.7209L5.17313 9.74902C5.16417 9.74462 5.15646 9.7381 5.15067 9.73005C5.14488 9.72199 5.1412 9.71266 5.13994 9.70286V5.0535C5.14086 2.98476 6.84207 1.30759 8.93894 1.30759C9.82702 1.30777 10.687 1.61474 11.3697 2.17522C11.339 2.19177 11.2854 2.22096 11.2498 2.24225L7.21388 4.54246C7.11317 4.59899 7.02944 4.68082 6.97118 4.77963C6.91292 4.87845 6.88222 4.99072 6.8822 5.10503V5.10873L6.87936 10.7209ZM7.80604 8.74956L10.0006 7.49887L12.1952 8.74872V11.2493L10.0006 12.4992L7.80604 11.2493V8.74956Z" />
</svg>
);
}

View File

@ -0,0 +1,109 @@
import {
HandThumbDownIcon,
HandThumbUpIcon,
} from '@heroicons/react/24/outline';
import { renderMarkdown } from '@nx/nx-dev/ui-markdoc';
import { cx } from '@nx/nx-dev/ui-primitives';
import Link from 'next/link';
import { useState } from 'react';
import { ChatGptLogo } from './chat-gpt-logo';
import { NrwlLogo } from './nrwl-logo';
export function FeedAnswer({
content,
feedbackButtonCallback,
isFirst,
}: {
content: string;
feedbackButtonCallback: (value: 'bad' | 'good') => void;
isFirst: boolean;
}) {
const [feedbackStatement, setFeedbackStatement] = useState<
'bad' | 'good' | null
>(null);
function handleFeedbackButtonClicked(statement: 'bad' | 'good'): void {
if (!!feedbackStatement) return;
setFeedbackStatement(statement);
feedbackButtonCallback(statement);
}
return (
<>
<div className="grid h-12 w-12 items-center justify-center rounded-full bg-white dark:bg-slate-900 ring-1 ring-slate-200 dark:ring-slate-700 text-slate-900 dark:text-white">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="h-8 w-8"
fill="currentColor"
>
<title>Nx</title>
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
</svg>
</div>
<div className="min-w-0 flex-1">
<div>
<div className="text-lg flex gap-2 items-center text-slate-900 dark:text-slate-100">
Nx Assistant{' '}
<span className="rounded-md bg-red-50 dark:bg-red-900/30 px-1.5 py-0.5 text-xs font-medium text-red-600 dark:text-red-400">
alpha
</span>
</div>
<p className="mt-0.5 flex items-center gap-x-1 text-sm text-slate-500">
<ChatGptLogo
className="h-4 w-4 text-slate-400"
aria-hidden="true"
/>{' '}
AI powered
</p>
</div>
<div className="mt-2 prose prose-slate dark:prose-invert w-full max-w-none 2xl:max-w-4xl">
{renderMarkdown(content, { filePath: '' }).node}
</div>
{!isFirst && (
<div className="group text-xs flex-1 md:flex md:justify-end gap-4 md:items-center text-slate-400 hover:text-slate-500 transition">
{feedbackStatement ? (
<p className="italic group-hover:flex">
{feedbackStatement === 'good'
? 'Glad I could help!'
: 'Sorry, could you please rephrase your question?'}
</p>
) : (
<p className="hidden italic group-hover:flex">
Is that the answer you were looking for?
</p>
)}
<div className="flex gap-4">
<button
className={cx(
'hover:rotate-12 hover:text-blue-500 dark:hover:text-sky-500 transition-all p-1 disabled:cursor-not-allowed',
{ 'text-blue-500': feedbackStatement === 'bad' }
)}
disabled={!!feedbackStatement}
onClick={() => handleFeedbackButtonClicked('bad')}
title="Bad"
>
<span className="sr-only">Bad answer</span>
<HandThumbDownIcon className="h-5 w-5" aria-hidden="true" />
</button>
<button
className={cx(
'hover:rotate-12 hover:text-blue-500 dark:hover:text-sky-500 transition-all p-1 disabled:cursor-not-allowed',
{ 'text-blue-500': feedbackStatement === 'good' }
)}
disabled={!!feedbackStatement}
onClick={() => handleFeedbackButtonClicked('good')}
title="Good"
>
<span className="sr-only">Good answer</span>
<HandThumbUpIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
)}
</div>
</>
);
}

View File

@ -0,0 +1,9 @@
export function FeedQuestion({ content }: { content: string }) {
return (
<div className="flex justify-end w-full">
<p className="px-4 py-2 bg-blue-500 dark:bg-sky-500 rounded-full rounded-br-none text-white text-base">
{content}
</p>
</div>
);
}

View File

@ -0,0 +1,36 @@
import { ChatItem } from '@nx/nx-dev/data-access-ai';
import { FeedAnswer } from './feed-answer';
import { FeedQuestion } from './feed-question';
export function Feed({
activity,
handleFeedback,
}: {
activity: ChatItem[];
handleFeedback: (statement: 'bad' | 'good', chatItemIndex: number) => void;
}) {
return (
<div className="flow-root my-12">
<ul role="list" className="-mb-8 space-y-12">
{activity.map((activityItem, activityItemIdx) => (
<li
key={[activityItem.role, activityItemIdx].join('-')}
className="pt-12 relative flex items-start space-x-3 feed-item"
>
{activityItem.role === 'assistant' ? (
<FeedAnswer
content={activityItem.content}
feedbackButtonCallback={(statement) =>
handleFeedback(statement, activityItemIdx)
}
isFirst={activityItemIdx === 0}
/>
) : (
<FeedQuestion content={activityItem.content} />
)}
</li>
))}
</ul>
</div>
);
}

View File

@ -0,0 +1,25 @@
import { ComponentProps } from 'react';
export function NrwlLogo(
props: ComponentProps<'svg'> & { title?: string; titleId?: string }
): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 18.9947C9.99253 18.996 9.98382 18.996 9.97444 18.996C9.89202 18.9986 9.8084 19 9.72598 19C8.82142 19 7.94802 18.8672 7.12155 18.6189C7.06659 18.6013 7.01233 18.5831 6.95618 18.563C6.69206 18.4746 6.43664 18.3686 6.18762 18.2482C5.80872 18.4706 5.38861 18.6267 4.93846 18.6982C4.83549 18.7138 4.73307 18.7256 4.62756 18.7327C4.42408 18.747 4.22497 18.7443 4.02885 18.7242L4.02766 18.7229C3.79167 18.6982 3.54877 18.693 3.30356 18.7099C2.8802 18.7392 2.47642 18.8316 2.09927 18.9785C2.04678 18.8367 2.01442 18.6845 2.00381 18.5258C1.94952 17.6754 2.56683 16.9405 3.38217 16.8838C3.68735 16.8624 3.96833 16.934 4.21425 17.0822C4.34793 17.1589 4.49765 17.2155 4.65502 17.2486C4.63257 17.2292 4.60944 17.209 4.58825 17.1894C4.58145 17.1843 4.57518 17.1784 4.56894 17.172C4.42774 17.049 4.29054 16.9197 4.15996 16.7831C3.96956 16.5951 3.73428 16.3382 3.50076 16.0046C3.48025 15.9772 3.45955 15.9481 3.43833 15.9187C3.38217 15.8395 3.32654 15.7601 3.27296 15.6782C3.01388 15.2835 2.78907 14.8614 2.60491 14.4178C2.5844 14.3691 2.56441 14.3204 2.54509 14.2695C2.48579 14.12 2.43207 13.9679 2.38271 13.8144C2.36582 13.7643 2.34964 13.713 2.33403 13.6609C2.30533 13.5634 2.2785 13.4652 2.25284 13.3669C2.24032 13.3182 2.22724 13.2694 2.21611 13.2187C2.18794 13.101 2.1629 12.9833 2.14116 12.8644C2.1224 12.7648 2.10432 12.6633 2.08922 12.5612C2.07996 12.4987 2.07125 12.4358 2.06378 12.3719C2.05616 12.3181 2.04992 12.2621 2.04431 12.2069C2.03931 12.156 2.03441 12.1073 2.02937 12.0573C2.02937 12.0475 2.02813 12.0384 2.02813 12.0292C2.02313 11.976 2.01943 11.9232 2.01685 11.8706C2.01252 11.8134 2.00882 11.7568 2.00747 11.6996C2.00258 11.5871 2 11.4733 2 11.3583C2 11.2918 2.00135 11.2275 2.00258 11.1625C2.00381 11.1079 2.00501 11.0538 2.00747 10.9998C2.00882 10.9693 2.01128 10.9388 2.01252 10.9081C2.01562 10.8632 2.01823 10.8185 2.02189 10.7748C2.02571 10.7098 2.03194 10.6461 2.03822 10.5817C2.04188 10.5375 2.04678 10.4939 2.0525 10.4491C2.0612 10.3711 2.07125 10.293 2.08242 10.2149C2.08676 10.1856 2.09057 10.1577 2.09546 10.1291C2.1017 10.09 2.10675 10.0517 2.11422 10.0133C2.12483 9.95156 2.13611 9.88923 2.14863 9.82861C2.16047 9.76624 2.17299 9.70504 2.1867 9.6433C2.19978 9.58606 2.2123 9.53081 2.22601 9.47358C2.33403 9.03397 2.47761 8.61061 2.65553 8.20742C2.67605 8.1594 2.69742 8.11322 2.71916 8.06704C2.82225 7.84332 2.93512 7.62598 3.05824 7.41604C3.11805 7.31262 3.18049 7.21189 3.24535 7.11299C3.31092 7.0121 3.37836 6.91265 3.44842 6.81519C3.51709 6.71956 3.58808 6.6259 3.65933 6.53299C3.70245 6.48156 3.74422 6.42829 3.78734 6.37686C3.82294 6.33593 3.85844 6.29367 3.89409 6.25259C3.93395 6.20784 3.97337 6.16349 4.01391 6.11871C4.05321 6.07509 4.0951 6.03159 4.13564 5.98922C4.14326 5.97946 4.15126 5.97168 4.15996 5.96386C4.22987 5.89106 4.30235 5.82079 4.37472 5.75064C4.89792 5.25181 5.49154 4.83231 6.13893 4.50988C6.18257 4.48708 6.22692 4.46424 6.27247 4.44483C6.48477 4.3427 6.70196 4.25286 6.9242 4.17353C6.97236 4.1561 7.01981 4.14038 7.06782 4.12423C7.09599 4.11501 7.12278 4.10595 7.15024 4.09816C7.19145 4.0833 7.23199 4.07155 7.27264 4.05976C7.34378 4.0384 7.41488 4.01825 7.48617 4C5.12258 4.70426 2.49192 6.95362 2.49192 10.875C2.49192 15.859 6.47348 16.824 8.32568 17.7201C9.44882 18.2624 9.85395 18.719 10 18.9947Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.90657 17.0587C9.85993 17.2489 9.66138 17.3641 9.46073 17.3143C9.26076 17.2662 9.13694 17.0722 9.18285 16.8819C9.22961 16.6924 9.38156 16.7738 9.58026 16.823C9.78076 16.871 9.95317 16.8697 9.90657 17.0587ZM6.00468 13.816C5.86286 13.9331 5.61197 13.8627 5.44584 13.6597C5.27917 13.4561 5.25872 13.1966 5.4005 13.0794C5.5416 12.9623 5.79134 13.0328 5.95931 13.2358C6.12529 13.4394 6.14647 13.6987 6.00468 13.816ZM4.77002 12.3326C4.63336 12.3652 4.48656 12.2307 4.44123 12.0355C4.39516 11.8402 4.46932 11.655 4.60595 11.6236C4.74261 11.5922 4.88956 11.7255 4.93489 11.9208C4.98012 12.1162 4.90611 12.3012 4.77002 12.3326ZM4.70878 10.4688C4.77767 10.4662 4.83833 10.5456 4.84223 10.6462C4.84667 10.7461 4.79311 10.8301 4.72285 10.8326C4.65381 10.8365 4.59314 10.757 4.5894 10.6565C4.58565 10.5561 4.63852 10.4726 4.70878 10.4688ZM5.33662 10.1602C5.35069 9.95901 5.47577 9.80538 5.61576 9.81431C5.7549 9.82519 5.85647 9.99556 5.84187 10.1966C5.8278 10.3971 5.70315 10.5507 5.56274 10.5406C5.42344 10.5317 5.32187 10.3606 5.33662 10.1602ZM17.5589 10.6148C17.5385 10.611 17.518 10.6071 17.497 10.6027C17.4695 10.5964 17.4421 10.59 17.414 10.5822C17.3871 10.5752 17.3597 10.5675 17.3328 10.5586C17.3321 10.5579 17.3316 10.5579 17.3309 10.5579C17.3136 10.5522 17.2964 10.5463 17.2791 10.5406C17.2453 10.5291 17.2115 10.5156 17.1775 10.5015C17.1775 10.5008 17.177 10.5015 17.177 10.5015C17.0927 10.4669 17.0096 10.4253 16.9299 10.3779C16.8283 10.3183 16.7312 10.2491 16.6411 10.1722C16.5734 10.1153 16.5102 10.0532 16.452 9.98717C16.4323 9.96541 16.4131 9.94295 16.3951 9.91992C16.2854 9.78346 16.1985 9.63233 16.1423 9.47162C15.7041 7.89474 14.7639 6.52609 13.5057 5.55255C12.2462 4.57835 10.6685 4 8.95494 4C8.32641 4 7.71512 4.07813 7.13139 4.22547C7.12374 4.22796 7.1174 4.22938 7.10975 4.23191C4.6915 4.92553 2 7.14088 2 11.003C2 15.9116 6.07364 16.8621 7.96868 17.7447C9.11779 18.2788 9.53244 18.7285 9.68172 19C10.9452 18.9647 12.1478 18.6842 13.2444 18.2033C13.3474 18.1551 13.4328 18.1001 13.5031 18.0406C13.5504 18.0732 13.6059 18.095 13.6648 18.1039L23 18.9839L13.8397 17.3457C13.8333 17.3443 13.827 17.3431 13.8219 17.3418C13.813 17.1099 13.6098 16.9563 13.5319 16.8505C13.4641 16.759 13.4616 16.6981 13.4616 16.6981C13.3977 15.7676 12.9781 14.9329 12.3387 14.3335C11.7678 13.7981 11.0243 13.447 10.1992 13.378V13.3766C10.111 13.2075 9.97809 13.0628 9.81708 12.9609C9.56482 12.763 9.40258 12.4434 9.40258 12.0969V12.0905C8.86671 12.1385 8.42993 12.5331 8.31804 13.0493C7.31733 12.5517 6.63008 11.5166 6.63008 10.3215C6.63008 9.49155 6.96025 8.73892 7.4968 8.19068C8.22106 7.42521 9.2296 6.96297 10.3632 6.96297C12.2033 6.96297 13.7504 8.21762 14.2012 9.91992C13.9087 10.2837 13.4604 10.5443 13.0158 10.6187C12.1906 10.7621 11.5417 11.4218 11.412 12.2532C12.1759 12.2532 12.6843 13.2984 14.1042 13.2984H14.1048C14.4586 13.2984 14.7787 13.1563 15.0117 12.9245C15.0341 12.902 15.0551 12.8796 15.0763 12.8558C15.0968 12.8321 15.1165 12.8078 15.135 12.7828C15.149 12.7643 15.1624 12.7451 15.1759 12.7252C15.1893 12.706 15.2015 12.6856 15.2136 12.6651C15.2379 12.6248 15.2595 12.5824 15.2794 12.5389C15.2858 12.5248 15.2922 12.5101 15.2979 12.4953L15.2986 12.4959C15.3107 12.5253 15.3241 12.5536 15.3375 12.5818C15.3515 12.6107 15.3669 12.6381 15.3828 12.6651C15.4072 12.706 15.4333 12.7451 15.4614 12.7828C15.471 12.795 15.4806 12.8078 15.4901 12.8201C15.51 12.8444 15.531 12.8681 15.5522 12.8904C15.5629 12.902 15.5739 12.9135 15.5847 12.9245C15.6849 13.0237 15.8006 13.1063 15.9283 13.1691C15.9564 13.1825 15.9852 13.1953 16.0146 13.2075C16.1628 13.2658 16.3237 13.2984 16.4922 13.2984C16.5191 13.2984 16.5453 13.2978 16.5709 13.2972C16.597 13.2966 16.6226 13.2952 16.6481 13.294C16.6737 13.2927 16.6992 13.2913 16.7242 13.2888C16.7464 13.287 16.7689 13.285 16.7906 13.2825C16.8372 13.2773 16.8825 13.2709 16.9272 13.2638C16.9464 13.2606 16.9655 13.2568 16.9841 13.2536C17.0032 13.2499 17.0218 13.246 17.0403 13.2421C17.0588 13.2383 17.0773 13.2344 17.0959 13.2299C17.1137 13.2255 17.1316 13.221 17.1496 13.2165C17.2031 13.2024 17.2549 13.187 17.3053 13.1703C17.3762 13.1466 17.4439 13.1211 17.5091 13.0929C17.5257 13.0858 17.5423 13.0788 17.5589 13.0704C17.5666 13.0672 17.5743 13.064 17.5819 13.0602C17.5889 13.0563 17.5967 13.0526 17.6043 13.0493C18.2596 12.7406 18.6627 12.2532 19.1845 12.2532C19.056 11.4295 18.4186 10.775 17.6043 10.6239L17.5589 10.6148Z"
/>
</svg>
);
}

View File

@ -0,0 +1,29 @@
export function LoadingState(): JSX.Element {
return (
<div className="w-full flex items-center justify-center px-4 gap-4 py-2 text-blue-500 dark:text-sky-500 transition ease-in-out duration-150">
<svg
className="animate-spin h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<p>Let me check the docs for you</p>
</div>
);
}

View File

@ -0,0 +1,42 @@
import { PaperAirplaneIcon } from '@heroicons/react/24/outline';
import { Button } from '@nx/nx-dev/ui-common';
export function Prompt({
isDisabled,
handleSubmit,
}: {
isDisabled: boolean;
handleSubmit: (query: string) => void;
}) {
return (
<form
onSubmit={(event) => {
event.preventDefault();
handleSubmit((event.target as any).query.value);
event.currentTarget.reset();
}}
className="relative flex gap-4 max-w-xl mx-auto py-2 px-4 shadow-lg rounded-md border border-slate-300 bg-white dark:border-slate-900 dark:bg-slate-700"
>
<input
id="query-prompt"
name="query"
disabled={isDisabled}
className="p-0 flex flex-grow text-sm placeholder-slate-500 transition bg-transparent 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?"
type="text"
/>
<Button
variant="primary"
size="small"
type="submit"
disabled={isDisabled}
className="disabled:cursor-not-allowed"
>
<div hidden className="sr-only">
Ask
</div>
<PaperAirplaneIcon aria-hidden="true" className="h-5 w-5" />
</Button>
</form>
);
}

View File

@ -0,0 +1,22 @@
import { InformationCircleIcon } from '@heroicons/react/24/outline';
export function ActivityLimitReached(): JSX.Element {
return (
<div className="rounded-md bg-slate-50 dark:bg-slate-800/40 ring-slate-100 dark:ring-slate-700 ring-1 p-4 shadow-sm">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-slate-500 dark:text-slate-300"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-slate-700 dark:text-slate-400">
You've reached the maximum message history limit. Previous messages
will be removed.
</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
import { ReactNode } from 'react';
export function SidebarContainer({ children }: { children: ReactNode[] }) {
return (
<div id="sidebar" data-testid="sidebar">
<div className="hidden h-full w-72 flex-col border-r border-slate-200 dark:border-slate-700 dark:bg-slate-900 md:flex">
<div className="relative flex flex-col gap-4 overflow-y-scroll p-4">
{...children}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
export function WarningMessage(): JSX.Element {
return (
<div className="rounded-md bg-yellow-50 dark:bg-yellow-900/30 ring-1 ring-yellow-100 dark:ring-yellow-900 p-4">
<h3 className="flex gap-x-3 text-sm font-medium text-yellow-600 dark:text-yellow-400">
<ExclamationTriangleIcon
className="h-5 w-5 text-yellow-500 dark:text-yellow-400"
aria-hidden="true"
/>{' '}
Always double check!
</h3>
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-600">
<p>
The results may not be accurate, so please always double check with
our documentation.
</p>
</div>
</div>
);
}

View File

@ -1,28 +1,7 @@
import { renderMarkdown } from '@nx/nx-dev/ui-markdoc';
export const warning = renderMarkdown(
`
{% callout type="warning" title="Always double check!" %}
This feature is still in Alpha.
The results may not be accurate, so please always double check with our documentation.
export function formatMarkdownSources(sourcesMarkdown: string): string {
return `\n
{% callout type="info" title="Sources" %}
${sourcesMarkdown}
{% /callout %}
`,
{ filePath: '' }
).node;
export const infoBox = renderMarkdown(
`
{% callout type="info" title="New question or continue chat?" %}
This chat has memory. It will answer all it's questions in the context of the previous questions.
If you want to ask a new question, you can reset the chat history by clicking the "Ask new question" button.
{% /callout %}
`,
{ filePath: '' }
).node;
export const noResults = renderMarkdown(
`
Sorry, I don't know how to help with that. You can visit the [Nx documentation](https://nx.dev/getting-started/intro) for more info.
`,
{ filePath: '' }
).node;
\n`;
}

View File

@ -1,7 +1,7 @@
import { FeedContainer } from '@nx/nx-dev/feature-ai';
import { DocumentationHeader } from '@nx/nx-dev/ui-common';
import { FeatureAi } from '@nx/nx-dev/feature-ai';
import { useNavToggle } from '../../lib/navigation-toggle.effect';
import { NextSeo } from 'next-seo';
import { useNavToggle } from '../../lib/navigation-toggle.effect';
export default function AiDocs(): JSX.Element {
const { toggleNav, navIsOpen } = useNavToggle();
@ -23,8 +23,12 @@ export default function AiDocs(): JSX.Element {
<div className="w-full flex-shrink-0">
<DocumentationHeader isNavOpen={navIsOpen} toggleNav={toggleNav} />
</div>
<main id="main" role="main" className="flex h-full flex-1">
<FeatureAi />
<main
id="main"
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<FeedContainer />
</main>
</div>
</>