From 90df14c545125a4c6fe369f2fa0ac02e0a9a6c1f Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 27 Feb 2024 23:30:41 +0600 Subject: [PATCH] feat: add roadmap stream --- .../GenerateRoadmap/GenerateRoadmap.tsx | 47 ++++++++++++++----- src/helper/read-stream.ts | 36 ++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 src/helper/read-stream.ts diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx index f1eea024d..6147d3eaf 100644 --- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx +++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx @@ -2,9 +2,11 @@ import { useRef, useState, type FormEvent } from 'react'; import './GenerateRoadmap.css'; import { httpPost } from '../../lib/http'; import { useToast } from '../../hooks/use-toast'; -import { generateRoadmapFromJSON } from '../../../editor/utils/roadmap-generator'; +import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator'; import { renderFlowJSON } from '../../../editor/renderer/renderer'; import { replaceChildren } from '../../lib/dom'; +import { readAIRoadmapStream } from '../../helper/read-stream'; +import { removeAuthToken } from '../../lib/jwt'; export function GenerateRoadmap() { const roadmapContainerRef = useRef(null); @@ -18,24 +20,47 @@ export function GenerateRoadmap() { e.preventDefault(); setIsLoading(true); - const { response, error } = await httpPost( - `${import.meta.env.PUBLIC_API_URL}/v1-generate-roadmap`, + const response = await fetch( + `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`, { - title: roadmapName, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ title: roadmapName }), }, ); - if (error || !response) { + if (!response.ok) { + const data = await response.json(); + + toast.error(data?.message || 'Something went wrong'); setIsLoading(false); - toast.error(error?.message || 'Something went wrong'); - return; + + // Logout user if token is invalid + if (data.status === 401) { + removeAuthToken(); + window.location.reload(); + } } - const { nodes, edges } = generateRoadmapFromJSON(response as any); - const svg = await renderFlowJSON({ nodes, edges }); - if (roadmapContainerRef?.current) { - replaceChildren(roadmapContainerRef?.current, svg); + const reader = response.body?.getReader(); + + if (!reader) { + setIsLoading(false); + toast.error('Something went wrong'); + return; } + + await readAIRoadmapStream(reader, async (result) => { + const { nodes, edges } = generateAIRoadmapFromText(result); + const svg = await renderFlowJSON({ nodes, edges }); + if (roadmapContainerRef?.current) { + replaceChildren(roadmapContainerRef?.current, svg); + } + }); + setIsLoading(false); }; diff --git a/src/helper/read-stream.ts b/src/helper/read-stream.ts new file mode 100644 index 000000000..08aceebf7 --- /dev/null +++ b/src/helper/read-stream.ts @@ -0,0 +1,36 @@ +const NEW_LINE = '\n'.charCodeAt(0); + +export async function readAIRoadmapStream( + reader: ReadableStreamDefaultReader, + renderRoadmap: (roadmap: string) => void, +) { + const decoder = new TextDecoder('utf-8'); + let result = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + + // We will call the renderRoadmap callback whenever we encounter + // a new line with the result until the new line + // otherwise, we will keep appending the result to the previous result + if (value) { + let start = 0; + for (let i = 0; i < value.length; i++) { + if (value[i] === NEW_LINE) { + result += decoder.decode(value.slice(start, i + 1)); + renderRoadmap(result); + start = i + 1; + } + } + if (start < value.length) { + result += decoder.decode(value.slice(start)); + } + } + } + + reader.releaseLock(); + renderRoadmap(result); +}