mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-10-03 20:31:52 +02:00
feat: add abort functionality to chat and update UI elements
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { BotIcon, Loader2Icon, SendIcon } from 'lucide-react';
|
import { BotIcon, Loader2Icon, PauseCircleIcon, SendIcon } from 'lucide-react';
|
||||||
import { ChatEditor } from '../ChatEditor/ChatEditor';
|
import { ChatEditor } from '../ChatEditor/ChatEditor';
|
||||||
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
|
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
|
||||||
import {
|
import {
|
||||||
@@ -112,11 +112,20 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [roadmapTreeData, roadmapJSONData, roadmapDetailsData]);
|
}, [roadmapTreeData, roadmapJSONData, roadmapDetailsData]);
|
||||||
|
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const handleChatSubmit = (json: JSONContent) => {
|
const handleChatSubmit = (json: JSONContent) => {
|
||||||
if (!json || isStreamingMessage || !isLoggedIn() || isLoading) {
|
if (
|
||||||
|
!json ||
|
||||||
|
isStreamingMessage ||
|
||||||
|
!isLoggedIn() ||
|
||||||
|
isLoading ||
|
||||||
|
abortControllerRef.current
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
const html = htmlFromTiptapJSON(json);
|
const html = htmlFromTiptapJSON(json);
|
||||||
const newMessages: RoamdapAIChatHistoryType[] = [
|
const newMessages: RoamdapAIChatHistoryType[] = [
|
||||||
...aiChatHistory,
|
...aiChatHistory,
|
||||||
@@ -133,7 +142,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
completeAITutorChat(newMessages);
|
completeAITutorChat(newMessages, abortControllerRef.current);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
@@ -160,7 +169,10 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
};
|
};
|
||||||
}, [roadmapId]);
|
}, [roadmapId]);
|
||||||
|
|
||||||
const completeAITutorChat = async (messages: RoamdapAIChatHistoryType[]) => {
|
const completeAITutorChat = async (
|
||||||
|
messages: RoamdapAIChatHistoryType[],
|
||||||
|
abortController?: AbortController,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
setIsStreamingMessage(true);
|
setIsStreamingMessage(true);
|
||||||
|
|
||||||
@@ -172,6 +184,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
signal: abortController?.signal,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
roadmapId,
|
roadmapId,
|
||||||
messages: messages.slice(-10),
|
messages: messages.slice(-10),
|
||||||
@@ -205,6 +218,10 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
|
|
||||||
await readStream(reader, {
|
await readStream(reader, {
|
||||||
onStream: async (content) => {
|
onStream: async (content) => {
|
||||||
|
if (abortController?.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const jsx = await renderMessage(content, renderer);
|
const jsx = await renderMessage(content, renderer);
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
@@ -214,6 +231,10 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
},
|
},
|
||||||
onStreamEnd: async (content) => {
|
onStreamEnd: async (content) => {
|
||||||
|
if (abortController?.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const jsx = await renderMessage(content, renderer);
|
const jsx = await renderMessage(content, renderer);
|
||||||
const newMessages: RoamdapAIChatHistoryType[] = [
|
const newMessages: RoamdapAIChatHistoryType[] = [
|
||||||
...messages,
|
...messages,
|
||||||
@@ -236,10 +257,25 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setIsStreamingMessage(false);
|
setIsStreamingMessage(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Something went wrong');
|
|
||||||
setIsStreamingMessage(false);
|
setIsStreamingMessage(false);
|
||||||
|
setStreamedMessage(null);
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
|
||||||
|
if (abortController?.signal.aborted) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
toast.error('Something went wrong');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAbort = () => {
|
||||||
|
abortControllerRef.current?.abort();
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
setIsStreamingMessage(false);
|
||||||
|
setStreamedMessage(null);
|
||||||
|
setAiChatHistory([...aiChatHistory].slice(0, aiChatHistory.length - 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -310,6 +346,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
className="relative flex items-start border-t border-gray-200 text-sm"
|
className="relative flex items-start border-t border-gray-200 text-sm"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (isStreamingMessage && abortControllerRef.current) {
|
||||||
|
handleAbort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
handleChatSubmit(editorRef.current?.getJSON() || {});
|
handleChatSubmit(editorRef.current?.getJSON() || {});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -317,6 +358,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
roadmapId={roadmapId}
|
roadmapId={roadmapId}
|
||||||
onSubmit={(content) => {
|
onSubmit={(content) => {
|
||||||
|
if (isStreamingMessage && abortControllerRef.current) {
|
||||||
|
handleAbort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
handleChatSubmit(content);
|
handleChatSubmit(content);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -325,7 +371,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="flex aspect-square size-[36px] items-center justify-center p-2 text-zinc-500 hover:text-black disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex aspect-square size-[36px] items-center justify-center p-2 text-zinc-500 hover:text-black disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
{isStreamingMessage ? (
|
||||||
|
<PauseCircleIcon className="size-4 stroke-[2.5]" />
|
||||||
|
) : (
|
||||||
<SendIcon className="size-4 stroke-[2.5]" />
|
<SendIcon className="size-4 stroke-[2.5]" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -32,9 +32,9 @@ export function RoadmapAIChatCard(props: RoadmapAIChatCardProps) {
|
|||||||
|
|
||||||
{!!jsx && jsx}
|
{!!jsx && jsx}
|
||||||
|
|
||||||
{html && (
|
{!!html && (
|
||||||
<div
|
<div
|
||||||
className="course-content course-ai-content prose prose-sm mt-0.5 max-w-full overflow-hidden text-sm"
|
className="course-content course-ai-content prose prose-sm mt-0.5 max-w-[calc(100%-38px)] overflow-hidden text-sm"
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@@ -113,7 +113,7 @@ export async function renderMessage(
|
|||||||
const parts = await parseMessageParts(content, renderer);
|
const parts = await parseMessageParts(content, renderer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="max-w-[calc(100%-38px)]">
|
||||||
{parts.map((item) => {
|
{parts.map((item) => {
|
||||||
if (
|
if (
|
||||||
(item.type === 'html' || item.type === 'text') &&
|
(item.type === 'html' || item.type === 'text') &&
|
||||||
|
Reference in New Issue
Block a user