diff --git a/change/@acedatacloud-nexior-80395631-13c8-47d5-b03c-eb3291e88a29.json b/change/@acedatacloud-nexior-80395631-13c8-47d5-b03c-eb3291e88a29.json new file mode 100644 index 00000000..aeef4460 --- /dev/null +++ b/change/@acedatacloud-nexior-80395631-13c8-47d5-b03c-eb3291e88a29.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "optimize chat experience", + "packageName": "@acedatacloud/nexior", + "email": "germey@acedata.cloud", + "dependentChangeType": "patch" +} diff --git a/src/operators/chat.ts b/src/operators/chat.ts index 9819932f..4c7a1e8a 100644 --- a/src/operators/chat.ts +++ b/src/operators/chat.ts @@ -1,4 +1,4 @@ -import axios, { AxiosProgressEvent, AxiosResponse } from 'axios'; +import axios, { AxiosResponse } from 'axios'; import { IChatConversation, IChatConversationAction, @@ -13,31 +13,126 @@ class ChatOperator { async chatConversation( data: IChatConversationRequest, options: IChatConversationOptions - ): Promise> { - return await axios.post('/aichat/conversations', data, { - headers: { - authorization: `Bearer ${options.token}`, - accept: 'application/x-ndjson', - 'content-type': 'application/json' - }, - baseURL: BASE_URL_API, - signal: options.signal, - responseType: 'stream', - onDownloadProgress: ({ event }: AxiosProgressEvent) => { - const response = event.target.response; - const lines = response.split('\r\n').filter((line: string) => !!line); - const lastLine = lines[lines.length - 1]; - if (lastLine) { - try { - const jsonData = JSON.parse(lastLine); - if (options?.stream) { - options?.stream(jsonData as IChatConversationResponse); - } - } catch (e) { - console.error(e); + ): Promise { + return new Promise((resolve, reject) => { + let finalAnswer = ''; + let id: string | undefined = undefined; + fetch(`${BASE_URL_API}/aichat/conversations`, { + method: 'POST', + headers: { + authorization: `Bearer ${options.token}`, + 'Content-Type': 'application/json', + Accept: 'text/event-stream' + }, + body: JSON.stringify(data) + }) + .then((response) => { + if (!response?.body) { + throw new Error('ReadableStream not yet supported in this browser.'); } - } - } + const reader = response?.body?.getReader(); + const decoder = new TextDecoder(); + + return new ReadableStream({ + start(controller) { + const push = () => { + reader.read().then(({ done, value }) => { + if (done) { + controller.close(); + return; + } + controller.enqueue(decoder.decode(value, { stream: true })); + push(); + }); + }; + push(); + } + }); + }) + .then((stream) => { + const linesStream = stream.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + chunk.split('\n').forEach((line: string) => { + if (line.trim()) { + controller.enqueue(line); + } + }); + } + }) + ); + + const reader = linesStream.getReader(); + let accumulatedData = ''; + let updateTimeout: number | null = null; + + reader.read().then(function processText({ done, value }) { + if (done) { + console.log('Stream complete'); + if (options?.stream) { + options?.stream({ + answer: finalAnswer, + delta_answer: '', + id + }); + } + resolve({ + answer: finalAnswer as string, + delta_answer: '' + }); + return; + } + + if (value.startsWith('data: ')) { + const subValue = value.substring(6); + if (subValue === '[DONE]') { + console.log('finalAnswer', finalAnswer); + console.log('Stream complete'); + if (options?.stream) { + options?.stream({ + answer: finalAnswer, + delta_answer: '', + id + }); + } + resolve({ + answer: finalAnswer as string, + delta_answer: '' + }); + } else { + const json = JSON.parse(subValue); + if (json.delta_answer) { + finalAnswer += json.delta_answer; + } + if (json.id) { + id = json.id; + } + + accumulatedData += json.delta_answer || ''; + + if (!updateTimeout) { + updateTimeout = window.setTimeout(() => { + if (options?.stream) { + options?.stream({ + answer: finalAnswer, + delta_answer: accumulatedData, + id + }); + } + accumulatedData = ''; + updateTimeout = null; + }, 0); + } + } + } + + reader.read().then(processText); + }); + }) + .catch((error) => { + console.error('Error:', error); + reject(error); + }); }); }