1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-25 00:21:28 +02:00

feat: add abort functionality to chat and update UI elements

This commit is contained in:
Arik Chakma
2025-05-22 04:07:25 +06:00
parent aa0833de6f
commit 982898a1a7
3 changed files with 59 additions and 9 deletions

View File

@@ -14,7 +14,7 @@ import {
useRef,
useState,
} from 'react';
import { BotIcon, Loader2Icon, SendIcon } from 'lucide-react';
import { BotIcon, Loader2Icon, PauseCircleIcon, SendIcon } from 'lucide-react';
import { ChatEditor } from '../ChatEditor/ChatEditor';
import { roadmapTreeMappingOptions } from '../../queries/roadmap-tree';
import {
@@ -112,11 +112,20 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
setIsLoading(false);
}, [roadmapTreeData, roadmapJSONData, roadmapDetailsData]);
const abortControllerRef = useRef<AbortController | null>(null);
const handleChatSubmit = (json: JSONContent) => {
if (!json || isStreamingMessage || !isLoggedIn() || isLoading) {
if (
!json ||
isStreamingMessage ||
!isLoggedIn() ||
isLoading ||
abortControllerRef.current
) {
return;
}
abortControllerRef.current = new AbortController();
const html = htmlFromTiptapJSON(json);
const newMessages: RoamdapAIChatHistoryType[] = [
...aiChatHistory,
@@ -133,7 +142,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
});
scrollToBottom();
completeAITutorChat(newMessages);
completeAITutorChat(newMessages, abortControllerRef.current);
};
const scrollToBottom = useCallback(() => {
@@ -160,7 +169,10 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
};
}, [roadmapId]);
const completeAITutorChat = async (messages: RoamdapAIChatHistoryType[]) => {
const completeAITutorChat = async (
messages: RoamdapAIChatHistoryType[],
abortController?: AbortController,
) => {
try {
setIsStreamingMessage(true);
@@ -172,6 +184,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
'Content-Type': 'application/json',
},
credentials: 'include',
signal: abortController?.signal,
body: JSON.stringify({
roadmapId,
messages: messages.slice(-10),
@@ -205,6 +218,10 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
await readStream(reader, {
onStream: async (content) => {
if (abortController?.signal.aborted) {
return;
}
const jsx = await renderMessage(content, renderer);
flushSync(() => {
@@ -214,6 +231,10 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
scrollToBottom();
},
onStreamEnd: async (content) => {
if (abortController?.signal.aborted) {
return;
}
const jsx = await renderMessage(content, renderer);
const newMessages: RoamdapAIChatHistoryType[] = [
...messages,
@@ -236,12 +257,27 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
});
setIsStreamingMessage(false);
abortControllerRef.current = null;
} catch (error) {
toast.error('Something went wrong');
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(() => {
scrollToBottom();
}, []);
@@ -310,6 +346,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
className="relative flex items-start border-t border-gray-200 text-sm"
onSubmit={(e) => {
e.preventDefault();
if (isStreamingMessage && abortControllerRef.current) {
handleAbort();
return;
}
handleChatSubmit(editorRef.current?.getJSON() || {});
}}
>
@@ -317,6 +358,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
editorRef={editorRef}
roadmapId={roadmapId}
onSubmit={(content) => {
if (isStreamingMessage && abortControllerRef.current) {
handleAbort();
return;
}
handleChatSubmit(content);
}}
/>
@@ -325,7 +371,11 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
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"
>
<SendIcon className="size-4 stroke-[2.5]" />
{isStreamingMessage ? (
<PauseCircleIcon className="size-4 stroke-[2.5]" />
) : (
<SendIcon className="size-4 stroke-[2.5]" />
)}
</button>
</form>
</div>

View File

@@ -32,9 +32,9 @@ export function RoadmapAIChatCard(props: RoadmapAIChatCardProps) {
{!!jsx && jsx}
{html && (
{!!html && (
<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 }}
/>
)}

View File

@@ -113,7 +113,7 @@ export async function renderMessage(
const parts = await parseMessageParts(content, renderer);
return (
<div>
<div className="max-w-[calc(100%-38px)]">
{parts.map((item) => {
if (
(item.type === 'html' || item.type === 'text') &&