diff --git a/service/package.json b/service/package.json index 054a07b..339a0a3 100644 --- a/service/package.json +++ b/service/package.json @@ -15,6 +15,7 @@ }, "scripts": { "start": "esno ./src/index.ts", + "dev": "esno watch ./src/index.ts", "prod": "esno ./build/index.js", "build": "pnpm clean && tsup", "clean": "rimraf build", diff --git a/service/src/chatgpt.ts b/service/src/chatgpt.ts index 4e8bebc..0057be4 100644 --- a/service/src/chatgpt.ts +++ b/service/src/chatgpt.ts @@ -1,6 +1,6 @@ import * as dotenv from 'dotenv' import 'isomorphic-fetch' -import type { ChatGPTAPI, SendMessageOptions } from 'chatgpt' +import type { ChatGPTAPI, ChatMessage, SendMessageOptions } from 'chatgpt' import { ChatGPTUnofficialProxyAPI } from 'chatgpt' import { sendResponse } from './utils' @@ -8,7 +8,7 @@ dotenv.config() let apiModel: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined -export interface ChatContext { +interface ChatContext { conversationId?: string parentMessageId?: string } @@ -65,6 +65,34 @@ async function chatReply( } } +async function chatReplyProcess( + message: string, + lastContext?: { conversationId?: string; parentMessageId?: string }, + process?: (chat: ChatMessage) => void, +) { + if (!message) + return sendResponse({ type: 'Fail', message: 'Message is empty' }) + + try { + let options: SendMessageOptions = { timeoutMs } + + if (lastContext) + options = { ...lastContext } + + const response = await api.sendMessage(message, { + ...options, + onProgress: (partialResponse) => { + process?.(partialResponse) + }, + }) + + return sendResponse({ type: 'Success', data: response }) + } + catch (error: any) { + return sendResponse({ type: 'Fail', message: error.message }) + } +} + async function chatConfig() { return sendResponse({ type: 'Success', @@ -76,4 +104,6 @@ async function chatConfig() { }) } -export { chatReply, chatConfig } +export type { ChatContext, ChatMessage } + +export { chatReply, chatReplyProcess, chatConfig } diff --git a/service/src/index.ts b/service/src/index.ts index 01faf4a..e623a75 100644 --- a/service/src/index.ts +++ b/service/src/index.ts @@ -1,6 +1,6 @@ import express from 'express' -import type { ChatContext } from './chatgpt' -import { chatConfig, chatReply } from './chatgpt' +import type { ChatContext, ChatMessage } from './chatgpt' +import { chatConfig, chatReply, chatReplyProcess } from './chatgpt' const app = express() const router = express.Router() @@ -26,6 +26,23 @@ router.post('/chat', async (req, res) => { } }) +router.post('/chat-process', async (req, res) => { + res.setHeader('Content-type', 'application/octet-stream') + + try { + const { prompt, options = {} } = req.body as { prompt: string; options?: ChatContext } + await chatReplyProcess(prompt, options, (chat: ChatMessage) => { + res.write(JSON.stringify(chat)) + }) + } + catch (error) { + res.write(JSON.stringify(error)) + } + finally { + res.end() + } +}) + router.post('/config', async (req, res) => { try { const response = await chatConfig() diff --git a/src/api/index.ts b/src/api/index.ts index 1d3811f..1183666 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,4 @@ -import type { GenericAbortSignal } from 'axios' +import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' import { post } from '@/utils/request' export function fetchChatAPI( @@ -13,6 +13,21 @@ export function fetchChatAPI( }) } +export function fetchChatAPIProcess( + params: { + prompt: string + options?: { conversationId?: string; parentMessageId?: string } + signal?: GenericAbortSignal + onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, +) { + return post({ + url: '/chat-process', + data: { prompt: params.prompt, options: params.options }, + signal: params.signal, + onDownloadProgress: params.onDownloadProgress, + }) +} + export function fetchChatConfig() { return post({ url: '/config', diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts index bcce94f..4fc36e7 100644 --- a/src/utils/request/index.ts +++ b/src/utils/request/index.ts @@ -1,4 +1,4 @@ -import type { AxiosResponse, GenericAbortSignal } from 'axios' +import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' import request from './axios' export interface HttpOption { @@ -6,6 +6,7 @@ export interface HttpOption { data?: any method?: string headers?: any + onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void signal?: GenericAbortSignal beforeRequest?: () => void afterRequest?: () => void @@ -17,9 +18,11 @@ export interface Response { status: string } -function http({ url, data, method, headers, signal, beforeRequest, afterRequest }: HttpOption) { +function http( + { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, +) { const successHandler = (res: AxiosResponse>) => { - if (res.data.status === 'Success') + if (res.data.status === 'Success' || typeof res.data === 'string') return res.data return Promise.reject(res.data) @@ -37,17 +40,18 @@ function http({ url, data, method, headers, signal, beforeRequest, afte const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) return method === 'GET' - ? request.get(url, { params, signal }).then(successHandler, failHandler) - : request.post(url, params, { headers, signal }).then(successHandler, failHandler) + ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler) + : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler) } export function get( - { url, data, method = 'GET', signal, beforeRequest, afterRequest }: HttpOption, + { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ): Promise> { return http({ url, method, data, + onDownloadProgress, signal, beforeRequest, afterRequest, @@ -55,13 +59,14 @@ export function get( } export function post( - { url, data, method = 'POST', headers, signal, beforeRequest, afterRequest }: HttpOption, + { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, ): Promise> { return http({ url, method, data, headers, + onDownloadProgress, signal, beforeRequest, afterRequest, diff --git a/src/views/chat/components/Message/Text.vue b/src/views/chat/components/Message/Text.vue index caa827b..2e039d0 100644 --- a/src/views/chat/components/Message/Text.vue +++ b/src/views/chat/components/Message/Text.vue @@ -15,7 +15,7 @@ const props = defineProps() const wrapClass = computed(() => { return [ 'text-wrap', - 'p-2', + 'p-3', 'min-w-[20px]', 'rounded-md', props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]', @@ -51,6 +51,9 @@ const text = computed(() => { max-width: 100%; vertical-align: middle; } + a { + color: #2d5cf6 + } } .hljs { diff --git a/src/views/chat/index.vue b/src/views/chat/index.vue index eeaa92b..a28d8b0 100644 --- a/src/views/chat/index.vue +++ b/src/views/chat/index.vue @@ -8,7 +8,7 @@ import { useChat } from './hooks/useChat' import { HoverButton, SvgIcon } from '@/components/common' import { useBasicLayout } from '@/hooks/useBasicLayout' import { useChatStore } from '@/store' -import { fetchChatAPI } from '@/api' +import { fetchChatAPIProcess } from '@/api' let controller = new AbortController() @@ -80,22 +80,39 @@ async function onConversation() { ) scrollToBottom() + let offset = 0 try { - const { data } = await fetchChatAPI(message, options, controller.signal) - updateChat( - +uuid, - dataSources.value.length - 1, - { - dateTime: new Date().toLocaleString(), - text: data.text ?? '', - inversion: false, - error: false, - loading: false, - conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id }, - requestOptions: { prompt: message, options: { ...options } }, + await fetchChatAPIProcess({ + prompt: message, + options, + signal: controller.signal, + onDownloadProgress: ({ event }) => { + const xhr = event.target + const { responseText } = xhr + const chunk = responseText.substring(offset) + offset = responseText.length + try { + const data = JSON.parse(chunk) + updateChat( + +uuid, + dataSources.value.length - 1, + { + dateTime: new Date().toLocaleString(), + text: data.text ?? '', + inversion: false, + error: false, + loading: false, + conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id }, + requestOptions: { prompt: message, options: { ...options } }, + }, + ) + scrollToBottom() + } + catch (error) { + // + } }, - ) - scrollToBottom() + }) } catch (error: any) { let errorMessage = error?.message ?? 'Something went wrong, please try again later.' @@ -119,6 +136,7 @@ async function onConversation() { scrollToBottom() } finally { + offset = 0 loading.value = false } } @@ -154,24 +172,41 @@ async function onRegenerate(index: number) { }, ) + let offset = 0 try { - const { data } = await fetchChatAPI(message, options, controller.signal) - updateChat( - +uuid, - index, - { - dateTime: new Date().toLocaleString(), - text: data.text ?? '', - inversion: false, - error: false, - loading: false, - conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id }, - requestOptions: { prompt: message, ...options }, + await fetchChatAPIProcess({ + prompt: message, + options, + signal: controller.signal, + onDownloadProgress: ({ event }) => { + const xhr = event.target + const { responseText } = xhr + const chunk = responseText.substring(offset) + offset = responseText.length + try { + const data = JSON.parse(chunk) + updateChat( + +uuid, + index, + { + dateTime: new Date().toLocaleString(), + text: data.text ?? '', + inversion: false, + error: false, + loading: false, + conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id }, + requestOptions: { prompt: message, ...options }, + }, + ) + } + catch (error) { + // + } }, - ) + }) } catch (error: any) { - let errorMessage = 'Something went wrong, please try again later.' + let errorMessage = error?.message ?? 'Something went wrong, please try again later.' if (error.message === 'canceled') errorMessage = 'Request canceled. Please try again.' @@ -192,6 +227,7 @@ async function onRegenerate(index: number) { } finally { loading.value = false + offset = 0 } }