feat(nx-dev): create a more chat-like interface for ai docs (#18530)
This commit is contained in:
parent
8f82d9c9b4
commit
c85a4d9122
@ -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 { Button } from '@nx/nx-dev/ui-common';
|
||||||
import { sendCustomEvent } from '@nx/nx-dev/feature-analytics';
|
import { sendCustomEvent } from '@nx/nx-dev/feature-analytics';
|
||||||
import { renderMarkdown } from '@nx/nx-dev/ui-markdoc';
|
import { renderMarkdown } from '@nx/nx-dev/ui-markdoc';
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
getProcessedHistory,
|
getProcessedHistory,
|
||||||
ChatItem,
|
ChatItem,
|
||||||
} from '@nx/nx-dev/data-access-ai';
|
} from '@nx/nx-dev/data-access-ai';
|
||||||
|
import { warning, infoBox } from './utils';
|
||||||
|
|
||||||
export function FeatureAi(): JSX.Element {
|
export function FeatureAi(): JSX.Element {
|
||||||
const [chatHistory, setChatHistory] = useState<ChatItem[] | null>([]);
|
const [chatHistory, setChatHistory] = useState<ChatItem[] | null>([]);
|
||||||
@ -16,34 +17,25 @@ export function FeatureAi(): JSX.Element {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [query, setSearchTerm] = useState('');
|
const [query, setSearchTerm] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [feedbackSent, setFeedbackSent] = useState<boolean>(false);
|
const [feedbackSent, setFeedbackSent] = useState<Record<number, boolean>>({});
|
||||||
const [sources, setSources] = useState('');
|
const [sources, setSources] = useState('');
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const lastMessageRef: React.RefObject<HTMLDivElement> | undefined =
|
||||||
|
useRef(null);
|
||||||
|
|
||||||
const warning = renderMarkdown(
|
useEffect(() => {
|
||||||
`
|
if (lastMessageRef.current) {
|
||||||
{% callout type="warning" title="Always double check!" %}
|
lastMessageRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||||
This feature is still in Alpha.
|
}
|
||||||
The results may not be accurate, so please always double check with our documentation.
|
}, [chatHistory]);
|
||||||
{% /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;
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (textResponse) {
|
setInput('');
|
||||||
|
if (query) {
|
||||||
setChatHistory([
|
setChatHistory([
|
||||||
...(chatHistory ?? []),
|
...(chatHistory ?? []),
|
||||||
{ role: 'assistant', content: textResponse },
|
{ role: 'user', content: query },
|
||||||
|
{ role: 'assistant', content: 'Let me think about that...' },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -60,19 +52,20 @@ export function FeatureAi(): JSX.Element {
|
|||||||
JSON.stringify(aiResponse.sources?.map((source) => source.url))
|
JSON.stringify(aiResponse.sources?.map((source) => source.url))
|
||||||
);
|
);
|
||||||
sourcesMarkdown = aiResponse.sourcesMarkdown;
|
sourcesMarkdown = aiResponse.sourcesMarkdown;
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error as any);
|
setError(error as any);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
setChatHistory(getProcessedHistory());
|
|
||||||
sendCustomEvent('ai_query', 'ai', 'query', undefined, {
|
sendCustomEvent('ai_query', 'ai', 'query', undefined, {
|
||||||
query,
|
query,
|
||||||
...usage,
|
...usage,
|
||||||
});
|
});
|
||||||
setFeedbackSent(false);
|
const sourcesMd =
|
||||||
|
sourcesMarkdown.length === 0
|
||||||
const sourcesMd = `
|
? ''
|
||||||
|
: `
|
||||||
{% callout type="info" title="Sources" %}
|
{% callout type="info" title="Sources" %}
|
||||||
${sourcesMarkdown}
|
${sourcesMarkdown}
|
||||||
{% /callout %}`;
|
{% /callout %}`;
|
||||||
@ -80,6 +73,25 @@ export function FeatureAi(): JSX.Element {
|
|||||||
setFinalResult(
|
setFinalResult(
|
||||||
renderMarkdown(completeText + sourcesMd, { filePath: '' }).node
|
renderMarkdown(completeText + sourcesMd, { filePath: '' }).node
|
||||||
);
|
);
|
||||||
|
if (completeText) {
|
||||||
|
setChatHistory([
|
||||||
|
...getProcessedHistory(),
|
||||||
|
{ role: 'assistant', content: completeText + sourcesMd },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFeedback = (type: 'good' | 'bad', index: number) => {
|
||||||
|
try {
|
||||||
|
sendCustomEvent('ai_feedback', 'ai', type, undefined, {
|
||||||
|
query,
|
||||||
|
result: finalResult,
|
||||||
|
sources,
|
||||||
|
});
|
||||||
|
setFeedbackSent((prev) => ({ ...prev, [index]: true }));
|
||||||
|
} catch (error) {
|
||||||
|
setFeedbackSent((prev) => ({ ...prev, [index]: false }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@ -88,37 +100,121 @@ export function FeatureAi(): JSX.Element {
|
|||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
setTextResponse('');
|
setTextResponse('');
|
||||||
setSources('');
|
setSources('');
|
||||||
setFeedbackSent(false);
|
|
||||||
setChatHistory(null);
|
setChatHistory(null);
|
||||||
};
|
setInput('');
|
||||||
|
setFeedbackSent({});
|
||||||
const handleFeedback = (type: 'good' | 'bad') => {
|
|
||||||
try {
|
|
||||||
sendCustomEvent('ai_feedback', 'ai', type, undefined, {
|
|
||||||
query,
|
|
||||||
result: finalResult,
|
|
||||||
sources,
|
|
||||||
});
|
|
||||||
setFeedbackSent(true);
|
|
||||||
} catch (error) {
|
|
||||||
setFeedbackSent(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
id="wrapper"
|
||||||
data-testid="wrapper"
|
data-testid="wrapper"
|
||||||
>
|
>
|
||||||
<div className="w-full flex">
|
<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">
|
||||||
|
{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>
|
||||||
|
|
||||||
|
{chatItem.role === 'assistant' &&
|
||||||
|
chatHistory?.length &&
|
||||||
|
(index === chatHistory.length - 1 && loading ? null : !feedbackSent[
|
||||||
|
index
|
||||||
|
] ? (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleFeedback('good', index)}
|
||||||
|
>
|
||||||
|
Answer was helpful{' '}
|
||||||
|
<span role="img" aria-label="thumbs-up">
|
||||||
|
👍
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleFeedback('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 ? (
|
||||||
|
<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
|
<input
|
||||||
id="search"
|
id="search"
|
||||||
name="search"
|
name="search"
|
||||||
|
value={input}
|
||||||
disabled={loading}
|
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"
|
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?"
|
placeholder="What do you want to know?"
|
||||||
onChange={(event) => setSearchTerm(event.target.value)}
|
onChange={(event) => {
|
||||||
|
setSearchTerm(event.target.value);
|
||||||
|
setInput(event.target.value);
|
||||||
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.keyCode === 13 || event.key === 'Enter') {
|
if (event.keyCode === 13 || event.key === 'Enter') {
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
@ -134,88 +230,15 @@ export function FeatureAi(): JSX.Element {
|
|||||||
>
|
>
|
||||||
Ask
|
Ask
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button variant="secondary" size="small" onClick={() => handleReset()}>
|
||||||
<div>
|
|
||||||
{infoBox}
|
|
||||||
<Button variant="primary" size="small" onClick={() => handleReset()}>
|
|
||||||
Ask new question{' '}
|
Ask new question{' '}
|
||||||
<span role="img" aria-label="thumbs-down">
|
<span role="img" aria-label="new question">
|
||||||
🔄
|
🔄
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
{warning}
|
|
||||||
</div>
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-4 max-w-none">
|
|
||||||
<h1>Thinking...</h1>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{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>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{finalResult && !loading && !error ? (
|
|
||||||
<>
|
|
||||||
<div className="p-4 max-w-none prose prose-slate dark:prose-invert">
|
|
||||||
{finalResult}
|
|
||||||
</div>
|
|
||||||
{!feedbackSent && (
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleFeedback('good')}
|
|
||||||
>
|
|
||||||
Answer was helpful{' '}
|
|
||||||
<span role="img" aria-label="thumbs-up">
|
|
||||||
👍
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleFeedback('bad')}
|
|
||||||
>
|
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default FeatureAi;
|
export default FeatureAi;
|
||||||
|
|||||||
21
nx-dev/feature-ai/src/lib/utils.ts
Normal file
21
nx-dev/feature-ai/src/lib/utils.ts
Normal file
@ -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;
|
||||||
@ -194,7 +194,7 @@ async function generateEmbeddings() {
|
|||||||
...getAllFilesWithItemList(manifestsTags),
|
...getAllFilesWithItemList(manifestsTags),
|
||||||
].filter(
|
].filter(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
!entry.path.includes('sitemap') || !entry.path.includes('deprecated')
|
!entry.path.includes('sitemap') && !entry.path.includes('deprecated')
|
||||||
);
|
);
|
||||||
|
|
||||||
const embeddingSources: EmbeddingSource[] = [
|
const embeddingSources: EmbeddingSource[] = [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user