Skip to content

Commit

Permalink
feat(nx-dev): create a more chat-like interface for ai docs (#18530)
Browse files Browse the repository at this point in the history
  • Loading branch information
mandarini committed Aug 9, 2023
1 parent 8f82d9c commit c85a4d9
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 120 deletions.
261 changes: 142 additions & 119 deletions nx-dev/feature-ai/src/lib/feature-ai.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, useState } from 'react';
import { ReactNode, 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';
Expand All @@ -8,6 +8,7 @@ import {
getProcessedHistory,
ChatItem,
} from '@nx/nx-dev/data-access-ai';
import { warning, infoBox } from './utils';

export function FeatureAi(): JSX.Element {
const [chatHistory, setChatHistory] = useState<ChatItem[] | null>([]);
Expand All @@ -16,34 +17,25 @@ export function FeatureAi(): JSX.Element {
const [error, setError] = useState(null);
const [query, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [feedbackSent, setFeedbackSent] = useState<boolean>(false);
const [feedbackSent, setFeedbackSent] = useState<Record<number, boolean>>({});
const [sources, setSources] = useState('');
const [input, setInput] = useState('');
const lastMessageRef: React.RefObject<HTMLDivElement> | undefined =
useRef(null);

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.
{% /callout %}
`,
{ filePath: '' }
).node;

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 with the button below.
{% /callout %}
`,
{ filePath: '' }
).node;
useEffect(() => {
if (lastMessageRef.current) {
lastMessageRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [chatHistory]);

const handleSubmit = async () => {
if (textResponse) {
setInput('');
if (query) {
setChatHistory([
...(chatHistory ?? []),
{ role: 'assistant', content: textResponse },
{ role: 'user', content: query },
{ role: 'assistant', content: 'Let me think about that...' },
]);
}
setLoading(true);
Expand All @@ -60,129 +52,122 @@ export function FeatureAi(): JSX.Element {
JSON.stringify(aiResponse.sources?.map((source) => source.url))
);
sourcesMarkdown = aiResponse.sourcesMarkdown;

setLoading(false);
} catch (error) {
setError(error as any);
setLoading(false);
}
setChatHistory(getProcessedHistory());
sendCustomEvent('ai_query', 'ai', 'query', undefined, {
query,
...usage,
});
setFeedbackSent(false);

const sourcesMd = `
{% callout type="info" title="Sources" %}
${sourcesMarkdown}
{% /callout %}`;
const sourcesMd =
sourcesMarkdown.length === 0
? ''
: `
{% callout type="info" title="Sources" %}
${sourcesMarkdown}
{% /callout %}`;

setFinalResult(
renderMarkdown(completeText + sourcesMd, { filePath: '' }).node
);
if (completeText) {
setChatHistory([
...getProcessedHistory(),
{ role: 'assistant', content: completeText + sourcesMd },
]);
}
};

const handleReset = () => {
resetHistory();
setFinalResult(null);
setSearchTerm('');
setTextResponse('');
setSources('');
setFeedbackSent(false);
setChatHistory(null);
};

const handleFeedback = (type: 'good' | 'bad') => {
const handleFeedback = (type: 'good' | 'bad', index: number) => {
try {
sendCustomEvent('ai_feedback', 'ai', type, undefined, {
query,
result: finalResult,
sources,
});
setFeedbackSent(true);
setFeedbackSent((prev) => ({ ...prev, [index]: true }));
} catch (error) {
setFeedbackSent(false);
setFeedbackSent((prev) => ({ ...prev, [index]: false }));
}
};

const handleReset = () => {
resetHistory();
setFinalResult(null);
setSearchTerm('');
setTextResponse('');
setSources('');
setChatHistory(null);
setInput('');
setFeedbackSent({});
};

return (
<div
className="p-2 mx-auto flex h-full w-full flex-col"
className="p-2 mx-auto flex h-screen w-full flex-col h-[calc(100vh-150px)]"
id="wrapper"
data-testid="wrapper"
>
<div className="w-full flex">
<input
id="search"
name="search"
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)}
onKeyDown={(event) => {
if (event.keyCode === 13 || event.key === 'Enter') {
handleSubmit();
}
}}
type="search"
/>
<Button
variant="primary"
size="small"
disabled={loading}
onClick={() => handleSubmit()}
>
Ask
</Button>
</div>
<div>
{infoBox}
<Button variant="primary" size="small" onClick={() => handleReset()}>
Ask new question{' '}
<span role="img" aria-label="thumbs-down">
🔄
</span>
</Button>
{warning}
</div>
{loading ? (
<div className="p-4 max-w-none">
<h1>Thinking...</h1>
<div className="flex-1 overflow-y-auto mb-4">
<div>
{infoBox}
{warning}
</div>
) : null}
{chatHistory && renderChatHistory(chatHistory)}
</div>
{renderChatInput()}
</div>
);

{chatHistory ? (
<div className="p-4 bg-gray-100">
<div className="mx-auto bg-white p-6 rounded shadow">
{chatHistory.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>
)}
<p>HISTORY</p>
{chatHistory.map((chatItem, index) => (
<div key={index} className="mb-4 border-b pb-2">
<strong className="text-gray-700 capitalize">
{chatItem.role}:
</strong>
<p className="text-gray-600 mt-1">{chatItem.content}</p>
</div>
))}
function renderChatHistory(history: ChatItem[]) {
return (
<div className="mx-auto bg-white p-6 rounded shadow">
{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'
}`}
>
<strong className="text-gray-700">
{chatItem.role === 'user' ? 'you' : 'nx assistant'}:
</strong>
<div className="text-gray-600 mt-1">
{renderMarkdown(chatItem.content, { filePath: '' }).node}
</div>
) : null}
{finalResult && !loading && !error ? (
<>
<div className="p-4 max-w-none prose prose-slate dark:prose-invert">
{finalResult}
</div>
{!feedbackSent && (

{chatItem.role === 'assistant' &&
chatHistory?.length &&
(index === chatHistory.length - 1 && loading ? null : !feedbackSent[
index
] ? (
<div>
<Button
variant="primary"
size="small"
onClick={() => handleFeedback('good')}
onClick={() => handleFeedback('good', index)}
>
Answer was helpful{' '}
<span role="img" aria-label="thumbs-up">
Expand All @@ -192,30 +177,68 @@ export function FeatureAi(): JSX.Element {
<Button
variant="primary"
size="small"
onClick={() => handleFeedback('bad')}
onClick={() => handleFeedback('bad', index)}
>
Answer looks wrong{' '}
<span role="img" aria-label="thumbs-down">
👎
</span>
</Button>
</div>
)}
{feedbackSent && (
) : (
<p>
<span role="img" aria-label="check">
</span>{' '}
Thank you for your feedback!
</p>
)}
</>
) : null}
{error && !loading ? (
<div>There was an error: {error['message']}</div>
) : null}
</div>
);
))}

{error && !loading ? (
<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;
21 changes: 21 additions & 0 deletions nx-dev/feature-ai/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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.
{% /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;
2 changes: 1 addition & 1 deletion tools/documentation/create-embeddings/src/main.mts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ async function generateEmbeddings() {
...getAllFilesWithItemList(manifestsTags),
].filter(
(entry) =>
!entry.path.includes('sitemap') || !entry.path.includes('deprecated')
!entry.path.includes('sitemap') && !entry.path.includes('deprecated')
);

const embeddingSources: EmbeddingSource[] = [
Expand Down

1 comment on commit c85a4d9

@vercel
Copy link

@vercel vercel bot commented on c85a4d9 Aug 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-five.vercel.app
nx-dev-nrwl.vercel.app
nx-dev-git-master-nrwl.vercel.app
nx.dev

Please sign in to comment.