From f60e88573afd8bab943d0d9a5d311f8648a8223b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Sat, 24 Feb 2024 21:02:28 +0530 Subject: [PATCH] refactored UI strategy, added prompt response in chat box --- extensions/react-widget/package-lock.json | 165 ++++++++- extensions/react-widget/package.json | 1 + extensions/react-widget/src/assets/cancel.svg | 4 + .../react-widget/src/assets/cute-docsgpt.svg | 9 + .../react-widget/src/assets/message.svg | 7 + .../src/components/DocsGPTWidget.tsx | 333 ++++++++---------- .../src/components/ui/scroll-area.tsx | 48 +++ extensions/react-widget/src/index.css | 2 + .../react-widget/src/requests/streamingApi.ts | 95 +++++ 9 files changed, 472 insertions(+), 192 deletions(-) create mode 100644 extensions/react-widget/src/assets/cancel.svg create mode 100644 extensions/react-widget/src/assets/cute-docsgpt.svg create mode 100644 extensions/react-widget/src/assets/message.svg create mode 100644 extensions/react-widget/src/components/ui/scroll-area.tsx create mode 100644 extensions/react-widget/src/requests/streamingApi.ts diff --git a/extensions/react-widget/package-lock.json b/extensions/react-widget/package-lock.json index f9e5fc41..ad57ba17 100644 --- a/extensions/react-widget/package-lock.json +++ b/extensions/react-widget/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -1064,6 +1065,22 @@ "node": ">=14" } }, + "node_modules/@radix-ui/number": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", + "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -1081,6 +1098,40 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", + "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", @@ -1089,6 +1140,84 @@ "react": "^16.x || ^17.x || ^18.x" } }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz", + "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.1", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -1107,6 +1236,40 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", @@ -1359,7 +1522,7 @@ "version": "18.2.19", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.19.tgz", "integrity": "sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } diff --git a/extensions/react-widget/package.json b/extensions/react-widget/package.json index 58e68a72..d4dd94b5 100644 --- a/extensions/react-widget/package.json +++ b/extensions/react-widget/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/extensions/react-widget/src/assets/cancel.svg b/extensions/react-widget/src/assets/cancel.svg new file mode 100644 index 00000000..5bce4dca --- /dev/null +++ b/extensions/react-widget/src/assets/cancel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/react-widget/src/assets/cute-docsgpt.svg b/extensions/react-widget/src/assets/cute-docsgpt.svg new file mode 100644 index 00000000..6c2492cd --- /dev/null +++ b/extensions/react-widget/src/assets/cute-docsgpt.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/extensions/react-widget/src/assets/message.svg b/extensions/react-widget/src/assets/message.svg new file mode 100644 index 00000000..c2aa8eef --- /dev/null +++ b/extensions/react-widget/src/assets/message.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/extensions/react-widget/src/components/DocsGPTWidget.tsx b/extensions/react-widget/src/components/DocsGPTWidget.tsx index 4ca2f52a..fa1f3b9e 100644 --- a/extensions/react-widget/src/components/DocsGPTWidget.tsx +++ b/extensions/react-widget/src/components/DocsGPTWidget.tsx @@ -1,16 +1,25 @@ "use client"; -import { useEffect, useRef, useState } from 'react' +import { Fragment, useEffect, useRef, useState } from 'react' import { PaperPlaneIcon } from '@radix-ui/react-icons'; import { Input } from './ui/input'; import { Button } from './ui/button'; - +import { ScrollArea, ScrollBar } from './ui/scroll-area' +import Dragon from '../assets/cute-docsgpt.svg' +import MessageIcon from '../assets/message.svg' +import Cancel from '../assets/cancel.svg' +import { Doc, Query } from '@/models/customTypes'; +import { fetchAnswerStreaming } from '@/requests/streamingApi'; //import './style.css' interface HistoryItem { prompt: string; response: string; } - +interface Message { + type: 'PROMPT' | 'RESPONSE' | 'ERROR', + message: string + id: string | null +} interface FetchAnswerStreamingProps { question?: string; apiKey?: string; @@ -21,6 +30,7 @@ interface FetchAnswerStreamingProps { onEvent?: (event: MessageEvent) => void; } +type Status = 'idle' | 'loading' | 'failed'; enum ChatStates { Init = 'init', @@ -30,87 +40,6 @@ enum ChatStates { Minimized = 'minimized', } -function fetchAnswerStreaming({ - question = '', - apiKey = '', - selectedDocs = '', - history = [], - conversationId = null, - apiHost = '', - onEvent = () => { console.log("Event triggered, but no handler provided."); } -}: FetchAnswerStreamingProps): Promise { - let docPath = 'default'; - if (selectedDocs) { - docPath = selectedDocs; - } - - return new Promise((resolve, reject) => { - const body = { - question: question, - api_key: apiKey, - embeddings_key: apiKey, - active_docs: docPath, - history: JSON.stringify(history), - conversation_id: conversationId, - model: 'default' - }; - - fetch(apiHost + '/stream', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) - .then((response) => { - if (!response.body) throw Error('No response body'); - - const reader = response.body.getReader(); - const decoder = new TextDecoder('utf-8'); - let counterrr = 0; - const processStream = ({ - done, - value, - }: ReadableStreamReadResult) => { - if (done) { - console.log(counterrr); - resolve(); - return; - } - - counterrr += 1; - - const chunk = decoder.decode(value); - - const lines = chunk.split('\n'); - - for (let line of lines) { - if (line.trim() == '') { - continue; - } - if (line.startsWith('data:')) { - line = line.substring(5); - } - - const messageEvent = new MessageEvent('message', { - data: line, - }); - - onEvent(messageEvent); // handle each message - } - - reader.read().then(processStream).catch(reject); - }; - - reader.read().then(processStream).catch(reject); - }) - .catch((error) => { - console.error('Connection failed:', error); - reject(error); - }); - }); -} - export const DocsGPTWidget = ({ apiHost = 'https://gptcloud.arc53.com', selectDocs = 'default', apiKey = 'docsgpt-public' }) => { // processing states const [chatState, setChatState] = useState(() => { @@ -119,109 +48,131 @@ export const DocsGPTWidget = ({ apiHost = 'https://gptcloud.arc53.com', selectDo } return ChatStates.Init; }); - - const [answer, setAnswer] = useState(''); - - //const selectDocs = 'local/1706.03762.pdf/' - const answerRef = useRef(null); - - useEffect(() => { - if (answerRef.current) { - const element = answerRef.current; - element.scrollTop = element.scrollHeight; + const [prompt, setPrompt] = useState(''); + const [status, setStatus] = useState('idle'); + const [queries, setQueries] = useState([ + { + prompt: 'dasasfafa fafajfiaf agad gagadjga gadgadgadijgaf', + response: 'dkadfafadfa fadfafa fa df adgdfaeye5uttr sr s srt rssr ' + }, + { + prompt: 'dasasfafa fafajfiaf agad gagadjga gadgadgadijgaf', + response: 'dkadfafadfa fadfafa fa df adgdfaeye5uttr sr s srt rssr ' + }, + { + prompt: 'dasasfafa fafajfiaf agad gagadjga gadgadgadijgaf', + response: 'dkadfafadfa fadfafa fa df adgdfaeye5uttr sr s srt rssr ' + }, + { + prompt: 'dasasfafa fafajfiaf agad gagadjga gadgadgadijgaf', + response: 'dkadfafadfa fadfafa fa df adgdfaeye5uttr sr s srt rssr ' + }, + { + prompt: 'LAST PROMPT', + response: 'dkadfafadfa fadfafa fa df adgdfaeye5uttr sr s srt rssr ' } - }, [answer]); + ]) + const [conversationId, setConversationId] = useState(null) + //const selectDocs = 'local/1706.03762.pdf/' + const scrollRef = useRef(null); + const scrollIntoView = () => { + scrollRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }; + useEffect(() => { + scrollIntoView(); + }, [queries.length, queries[queries.length - 1].response]); useEffect(() => { localStorage.setItem('docsGPTChatState', chatState); }, [chatState]); + async function stream(question: string) { + setStatus('loading'); + try { + await fetchAnswerStreaming( + { + question: question, + apiKey: apiKey, + apiHost: apiHost, + selectedDocs: selectDocs, + history: queries, + conversationId: conversationId, + onEvent: (event: MessageEvent) => { + const data = JSON.parse(event.data); + // check if the 'end' event has been received + if (data.type === 'end') { + // set status to 'idle' + setStatus('idle'); - - - // submit handler - const handleSubmit = (e: React.FormEvent) => { - setAnswer('') - e.preventDefault() - // get question - setChatState(ChatStates.Processing) - setTimeout(() => { - setChatState(ChatStates.Answer) - }, 800) - const inputElement = e.currentTarget[0] as HTMLInputElement; - const questionValue = inputElement.value; - - fetchAnswerStreaming({ - question: questionValue, - apiKey: apiKey, - selectedDocs: selectDocs, - history: [], - conversationId: null, - apiHost: apiHost, - onEvent: (event) => { - const data = JSON.parse(event.data); - - // check if the 'end' event has been received - if (data.type === 'end') { - setChatState(ChatStates.Answer) - } else if (data.type === 'source') { - // check if data.metadata exists - let result; - if (data.metadata && data.metadata.title) { - const titleParts = data.metadata.title.split('/'); - result = { - title: titleParts[titleParts.length - 1], - text: data.doc, - }; - } else { - result = { title: data.doc, text: data.doc }; + } else if (data.type === 'id') { + setConversationId(data.id) + } else { + const result = data.answer; + let streamingResponse = queries[queries.length - 1].response ? queries[queries.length - 1].response : ''; + let updatedQueries = [...queries]; + updatedQueries[updatedQueries.length - 1].response = streamingResponse + result; + setQueries(updatedQueries); + } } - console.log(result) - - } else if (data.type === 'id') { - console.log(data.id); - } else { - const result = data.answer; - // set answer by appending answer - setAnswer(prevAnswer => prevAnswer + result); } - }, - }); + ); + } catch (error) { + console.log(error); + + let updatedQueries = [...queries]; + updatedQueries[updatedQueries.length - 1].error = 'error' + setQueries(updatedQueries); + setStatus('idle') + } + } + // submit handler + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + queries.push({ prompt }) + setPrompt('') + setChatState(ChatStates.Processing) + await stream(prompt) + setChatState(ChatStates.Answer) + + } + return ( <>
setChatState(ChatStates.Init)} className={`${chatState !== 'minimized' ? 'hidden' : ''} cursor-pointer`}> -
+
DocsGPT
-
+
Exit { event.stopPropagation(); setChatState(ChatStates.Minimized); }} />
-
-

Need help with documentation?

-

DocsGPT AI assistant will help you with docs

-
-
-

{answer}

+
+ +
+ +

Get AI assistance

+

DocsGPT's AI Chatbot is here to help

+ +
@@ -230,47 +181,47 @@ export const DocsGPTWidget = ({ apiHost = 'https://gptcloud.arc53.com', selectDo className={`flex w-full justify-center px-5 py-3 text-sm text-gray-800 font-bold dark:text-white transition duration-300 hover:bg-gray-100 rounded-b dark:hover:bg-gray-800/70 ${chatState !== 'init' ? 'hidden' : ''}`}> Ask DocsGPT - {(chatState === 'typing' || chatState === 'answer') && ( -
-
-
-

- Hi, How can I help you today ? -

-
- -
-

- Hey, I am having trouble with my account ! -

-
- -
-

- What seems to be the problem ? -

-
- -
-

- I can't login -

-
-
+ {(chatState === 'typing' || chatState === 'answer' || chatState === 'processing') && ( +
+ + { + queries?.map((query, index) => { + return ( + + { + query.prompt &&
+

+ {query.prompt} +

+
+ } + { + query.response &&
+

+ {query.response} +

+
+ } +
) + }) + } +
- - + setPrompt(event.target.value)} + type='text' + className="w-[85%] border border-[#686877] h-8 bg-transparent px-5 py-4 text-sm text-gray-700 dark:text-white focus:outline-none" placeholder="What do you want to do?" /> +
)} -

- Processing... -

+
diff --git a/extensions/react-widget/src/components/ui/scroll-area.tsx b/extensions/react-widget/src/components/ui/scroll-area.tsx new file mode 100644 index 00000000..0b4a48d8 --- /dev/null +++ b/extensions/react-widget/src/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/extensions/react-widget/src/index.css b/extensions/react-widget/src/index.css index 17622344..094fa073 100644 --- a/extensions/react-widget/src/index.css +++ b/extensions/react-widget/src/index.css @@ -15,6 +15,8 @@ z-index: 1000; /* to ensure it appears on top of other content, if any */ display: flex; flex-direction: column; align-items: center; +width: 355px; +height: 405px; } @keyframes dotBounce { diff --git a/extensions/react-widget/src/requests/streamingApi.ts b/extensions/react-widget/src/requests/streamingApi.ts new file mode 100644 index 00000000..3623e8f2 --- /dev/null +++ b/extensions/react-widget/src/requests/streamingApi.ts @@ -0,0 +1,95 @@ +import { Answer } from "@/models/customTypes"; +import { Doc } from "@/models/customTypes"; +interface HistoryItem { + prompt: string; + response?: string; + } +interface FetchAnswerStreamingProps { + question?: string; + apiKey?: string; + selectedDocs?: string; + history?: HistoryItem[]; + conversationId?: string | null; + apiHost?: string; + onEvent?: (event: MessageEvent) => void; + } +export function fetchAnswerStreaming({ + question = '', + apiKey = '', + selectedDocs = '', + history = [], + conversationId = null, + apiHost = '', + onEvent = () => {console.log("Event triggered, but no handler provided.");} + }: FetchAnswerStreamingProps): Promise { + let docPath = 'default'; + if (selectedDocs) { + docPath = selectedDocs; + } + + return new Promise((resolve, reject) => { + const body = { + question: question, + api_key: apiKey, + embeddings_key: apiKey, + active_docs: docPath, + history: JSON.stringify(history), + conversation_id: conversationId, + model: 'default' + }; + + fetch(apiHost + '/stream', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + .then((response) => { + if (!response.body) throw Error('No response body'); + + const reader = response.body.getReader(); + const decoder = new TextDecoder('utf-8'); + let counterrr = 0; + const processStream = ({ + done, + value, + }: ReadableStreamReadResult) => { + if (done) { + console.log(counterrr); + resolve(); + return; + } + + counterrr += 1; + + const chunk = decoder.decode(value); + + const lines = chunk.split('\n'); + + for (let line of lines) { + if (line.trim() == '') { + continue; + } + if (line.startsWith('data:')) { + line = line.substring(5); + } + + const messageEvent = new MessageEvent('message', { + data: line, + }); + + onEvent(messageEvent); // handle each message + } + + reader.read().then(processStream).catch(reject); + }; + + reader.read().then(processStream).catch(reject); + }) + .catch((error) => { + console.error('Connection failed:', error); + reject(error); + }); + }); + } \ No newline at end of file