14.6.7 ChatGPT聊天主页面
在整个系统框架的右侧是ChatGPT聊天主页面,聊天主页面和ChatGPT官方的聊天页面高度相似,用户在文本框中发送聊天信息,ChatGPT获取聊天信息后返回给用户回答的信息。编写文件frontend/src/views/chat/index.vue实现本项目的ChatGPT聊天主页面,具体实现流程如下所示。
(1)编写方法handleSubmit()处理用户提交的聊天信息,具体实现代码如下所示。
function handleSubmit() {
onConversation()
}
(2)编写异步方法onConversation()在聊天界面中应用户的输入,引用了NAutoComplete、NButton、NInput、NSwitch、useDialog和useMessage等Naive UI组件或钩子,用于在屏幕中央显示对话框、向用户显示提示信息以及监听用户输入事件等。同时,方法onConversation()还通过使用fetchChatAPIProcess()函数调用后端API来获取响应用户输入的聊天消息,并通过updateChat()、addChat()和updateChatSome()等函数修改聊天消息的状态,例如loading、error、conversationOptions和requestOptions等。最后,方法onConversation()还包括了一些自定义变量和函数,例如controller、ModelState、usingContext、scrollRef、prompt、dataSources、conversationList、promptStore、userInfo等,分别用于控制请求的取消、维护ChatGPT-4模型、切换上下文输入、滚动到底部、存储用户输入、获取数据源、过滤数据源、管理联想列表、获取用户信息等。方法onConversation()的具体实现代码如下所示、
async function onConversation() {
let message = prompt.value
if (loading.value)
return
if (!message || message.trim() === '')
return
controller = new AbortController()
addChat(
+uuid,
{
dateTime: new Date().toLocaleString(),
text: message,
inversion: true,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
},
)
scrollToBottom()
loading.value = true
prompt.value = ''
let options: Chat.ConversationRequest = {}
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions
if (lastContext && usingContext.value)
options = { ...lastContext }
addChat(
+uuid,
{
dateTime: new Date().toLocaleString(),
text: '思考中...',
loading: true,
inversion: false,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottom()
try {
let lastText = ''
const fetchChatAPIOnce = async () => {
await fetchChatAPIProcess<Chat.ConversationResponse>({
prompt: message,
options,
baseURI: userInfo.value.baseURI,
accessToken: userInfo.value.accessToken,
isGPT4: ModelState.isGPT4,
signal: controller.signal,
onDownloadProgress: ({ event }) => {
const xhr = event.target
const { responseText } = xhr
// Always process the final line
const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
let chunk = responseText
if (lastIndex !== -1)
chunk = responseText.substring(lastIndex)
try {
const data = JSON.parse(chunk)
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: lastText + (data.text ?? ''),
inversion: false,
error: false,
loading: true,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, options: { ...options } },
},
)
if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
options.parentMessageId = data.id
lastText = data.text
message = ''
return fetchChatAPIOnce()
}
scrollToBottomIfAtBottom()
}
catch (error) {
//
}
},
})
updateChatSome(+uuid, dataSources.value.length - 1, { loading: false })
}
await fetchChatAPIOnce()
}
catch (error: any) {
const errorMessage = error?.message ?? t('common.wrong')
if (error.message === 'canceled') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
loading: false,
},
)
scrollToBottomIfAtBottom()
return
}
const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)
if (currentChat?.text && currentChat.text !== '') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
text: `${currentChat.text}\n[${errorMessage}]`,
error: false,
loading: false,
},
)
return
}
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottomIfAtBottom()
}
finally {
loading.value = false
}
}
(3)编写异步方法onRegenerate(),用于重新生成聊天消息并响应用户输入。方法onRegenerate()参数index表示要重新生成的聊天消息在数据源中的索引位置。方法onRegenerate()使用了多个Vue.js实用工具来管理聊天消息和用户输入状态,例如computed、reactive和ref等。方法onRegenerate()还包括了一些自定义变量和函数,例如loading、userInfo、ModelState、openLongReply、controller等,分别用于保存加载状态、获取用户信息、维护GPT-4模型、开启长回答、控制请求的取消等。同时,方法onRegenerate()通过使用fetchChatAPIProcess()函数调用后端API来生成新的聊天消息,并通过updateChat()、updateChatSome()等函数修改聊天消息的状态,例如loading、error、conversationOptions和requestOptions等。最后,方法onRegenerate()使用try-catch-finally语句块捕获和处理异常情况,例如请求被取消、生成失败以及其他错误等情况。方法onRegenerate()的具体实现代码如下所示。
async function onRegenerate(index: number) {
if (loading.value)
return
controller = new AbortController()
const { requestOptions } = dataSources.value[index]
let message = requestOptions?.prompt ?? ''
let options: Chat.ConversationRequest = {}
if (requestOptions.options)
options = { ...requestOptions.options }
loading.value = true
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: '思考中...',
inversion: false,
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, ...options },
},
)
try {
let lastText = ''
const fetchChatAPIOnce = async () => {
await fetchChatAPIProcess<Chat.ConversationResponse>({
prompt: message,
options,
baseURI: userInfo.value.baseURI,
accessToken: userInfo.value.accessToken,
signal: controller.signal,
isGPT4: ModelState.isGPT4,
onDownloadProgress: ({ event }) => {
const xhr = event.target
const { responseText } = xhr
// Always process the final line
const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
let chunk = responseText
if (lastIndex !== -1)
chunk = responseText.substring(lastIndex)
try {
const data = JSON.parse(chunk)
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: lastText + (data.text ?? ''),
inversion: false,
error: false,
loading: true,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, ...options },
},
)
if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
options.parentMessageId = data.id
lastText = data.text
message = ''
return fetchChatAPIOnce()
}
}
catch (error) {
//
}
},
})
updateChatSome(+uuid, index, { loading: false })
}
await fetchChatAPIOnce()
}
catch (error: any) {
if (error.message === 'canceled') {
updateChatSome(
+uuid,
index,
{
loading: false,
},
)
return
}
const errorMessage = error?.message ?? t('common.wrong')
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, ...options },
},
)
}
finally {
loading.value = false
}
}
(4)编写方法handleExport(),功能是将聊天消息截图并导出为PNG格式的图片。该方法使用了多个Vue.js实用工具和Naive UI组件或钩子来显示对话框和提示信息,例如dialog、useMessage等。方法handleExport()的具体实现代码如下所示。
function handleExport() {
if (loading.value)
return
const d = dialog.warning({
title: t('chat.exportImage'),
content: t('chat.exportImageConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: async () => {
try {
d.loading = true
const ele = document.getElementById('image-wrapper')
const canvas = await html2canvas(ele as HTMLDivElement, {
useCORS: true,
})
const imgUrl = canvas.toDataURL('image/png')
const tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = imgUrl
tempLink.setAttribute('download', 'chat-shot.png')
if (typeof tempLink.download === 'undefined')
tempLink.setAttribute('target', '_blank')
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(imgUrl)
d.loading = false
ms.success(t('chat.exportSuccess'))
Promise.resolve()
}
catch (error: any) {
ms.error(t('chat.exportFailed'))
}
finally {
d.loading = false
}
},
})
}
对上述代码的具体说明如下:
- 方法handleExport()包括了一些自定义变量和函数,例如loading、html2canvas等,分别用于保存加载状态、生成截图等。
- 方法handleExport()使用HTMLCanvasElement API将指定区域的HTML元素转换为canvas,并通过toDataURL方法将canvas转换为Data URI,并使用createElement创建一个a标签,设置其href属性为Data URI,并设置download属性为指定的文件名。
- 最后,方法handleExport()通过将a标签添加到document.body中,并模拟用户点击a标签的click事件来触发下载操作,再移除a标签,并使用window.URL.revokeObjectURL方法释放资源。如果导出成功,则使用ms.success显示成功提示信息;否则使用ms.error显示失败提示信息。