|
|
|
@ -1,30 +1,22 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { useConversationStore, useGuideStore, usePageStore, useSettingsStore, useUserStore } from '@/stores'
|
|
|
|
|
import { createOutline, ellipsisHorizontalCircle, heart, heartOutline, paperPlane, sparkles } from 'ionicons/icons'
|
|
|
|
|
import { ellipsisHorizontalCircle, heart, heartOutline, paperPlane, sparkles } from 'ionicons/icons'
|
|
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import { api } from '@/api'
|
|
|
|
|
import {
|
|
|
|
|
IonContent,
|
|
|
|
|
loadingController,
|
|
|
|
|
toastController,
|
|
|
|
|
InfiniteScrollCustomEvent,
|
|
|
|
|
useIonRouter
|
|
|
|
|
} from '@ionic/vue'
|
|
|
|
|
import { showConfirm, showError, showHelpToast } from '@/utils/dialog'
|
|
|
|
|
import { ChatCompleteChunkInfo, ChatCompleteMessage } from '@/types/base'
|
|
|
|
|
import { DEFAULT_BOT_AVATAR, secondTimestamp, setPageTitle } from '@/utils/other'
|
|
|
|
|
import { ChatCompleteChunkInfo } from '@/types/base'
|
|
|
|
|
import { setPageTitle } from '@/utils/other'
|
|
|
|
|
import { pinConversation } from '@/utils/actions'
|
|
|
|
|
import { moduleConversationRoute } from '@/types/enum'
|
|
|
|
|
import { IonRouterOutletInstance } from '@/types/instance'
|
|
|
|
|
import { IndexPageParams } from '@/api/chatcomplete'
|
|
|
|
|
import { ChatCompleteController, ChatCompletePageState, FullTitlePopupState } from './ChatCompleteController'
|
|
|
|
|
|
|
|
|
|
export type ConversationChunkInfo = {
|
|
|
|
|
id: number,
|
|
|
|
|
message_data: ChatCompleteChunkInfo,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MODULE_NAME = 'chatcomplete'
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const router = useIonRouter()
|
|
|
|
|
|
|
|
|
@ -36,10 +28,10 @@ const settingsStore = useSettingsStore()
|
|
|
|
|
|
|
|
|
|
const mainRouterOutlet = inject<Ref<IonRouterOutletInstance>>('mainRouterOutlet')
|
|
|
|
|
|
|
|
|
|
const state = reactive({
|
|
|
|
|
const state = reactive<ChatCompletePageState>({
|
|
|
|
|
conversationLoading: false,
|
|
|
|
|
errorMessage: null as string | null,
|
|
|
|
|
chatCompleteError: null as null | string,
|
|
|
|
|
errorMessage: null,
|
|
|
|
|
chatCompleteError: null,
|
|
|
|
|
conversationTitle: '加载中...',
|
|
|
|
|
conversationId: 0,
|
|
|
|
|
pinned: false,
|
|
|
|
@ -49,16 +41,16 @@ const state = reactive({
|
|
|
|
|
botName: '',
|
|
|
|
|
botAvatar: '',
|
|
|
|
|
|
|
|
|
|
conversationChunkIdList: [] as number[],
|
|
|
|
|
messageChunkList: [] as ConversationChunkInfo[],
|
|
|
|
|
conversationChunkIdList: [],
|
|
|
|
|
messageChunkList: [],
|
|
|
|
|
loadedChunkSeek: 0,
|
|
|
|
|
|
|
|
|
|
lastUserMsgId: [] as [number, string] | [],
|
|
|
|
|
lastAssistantMsgId: [] as [number, string] | [],
|
|
|
|
|
lastUserMsgId: [],
|
|
|
|
|
lastAssistantMsgId: [],
|
|
|
|
|
|
|
|
|
|
streamingMessage: null as null | string,
|
|
|
|
|
streamingMessage: null,
|
|
|
|
|
|
|
|
|
|
editingId: null as [number, string] | null,
|
|
|
|
|
editingId: null,
|
|
|
|
|
formMessage: '',
|
|
|
|
|
pointUsage: '0',
|
|
|
|
|
pointUsageLoading: false,
|
|
|
|
@ -72,704 +64,30 @@ const contentRef = ref<InstanceType<typeof IonContent> | null>(null)
|
|
|
|
|
|
|
|
|
|
const conversationInfo = computed(() => conversationStore.list.find((conversation) => conversation.id === state.conversationId))
|
|
|
|
|
|
|
|
|
|
let editingToast: HTMLIonToastElement | null = null
|
|
|
|
|
|
|
|
|
|
let autoScrollTimer: NodeJS.Timer | null = null
|
|
|
|
|
let ignoreAutoScroll = false
|
|
|
|
|
|
|
|
|
|
if (route.params.id) {
|
|
|
|
|
state.conversationId = parseInt(Array.isArray(route.params.id) ? route.params.id[0] : route.params.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isNewConversaion = computed(() => state.conversationId === 0)
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 帮助信息
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const showPinHelp = () => {
|
|
|
|
|
if (!guideStore.helpForPin) {
|
|
|
|
|
showHelpToast('未收藏的对话将会在30天后或消息列表满100条时自动删除,如果想要永久保留当前对话,请点击收藏按钮。')
|
|
|
|
|
guideStore.helpForPin = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 发送消息
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const onMessageInputKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
|
|
|
|
onSendMessage()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onSendMessage = async () => {
|
|
|
|
|
if (state.formMessage === '') return
|
|
|
|
|
|
|
|
|
|
// 滚动到底部,并启动自动滚动
|
|
|
|
|
let contentEl = contentRef.value?.$el
|
|
|
|
|
contentEl?.scrollToBottom()
|
|
|
|
|
ignoreAutoScroll = false
|
|
|
|
|
|
|
|
|
|
state.conversationLoading = true
|
|
|
|
|
|
|
|
|
|
let latestChunk: ConversationChunkInfo | null = null
|
|
|
|
|
let newUserMessage: ChatCompleteMessage | null = null
|
|
|
|
|
try {
|
|
|
|
|
let question = state.formMessage
|
|
|
|
|
|
|
|
|
|
let paramEditingId: [number, string] | undefined = undefined
|
|
|
|
|
if (state.editingId?.length && state.editingId[1] !== 'error-user-message') {
|
|
|
|
|
paramEditingId = state.editingId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const startRes = await api.chat.startChatComplete(pageStore.title, question, {
|
|
|
|
|
conversationId: state.conversationId,
|
|
|
|
|
editMessageId: paramEditingId,
|
|
|
|
|
botId: state.botId,
|
|
|
|
|
inCollection: settingsStore.collectionMode,
|
|
|
|
|
extractLimit: settingsStore.docExtractLimit
|
|
|
|
|
})
|
|
|
|
|
state.formMessage = ''
|
|
|
|
|
await editingToast?.dismiss()
|
|
|
|
|
|
|
|
|
|
// 删除所有遇到错误的消息
|
|
|
|
|
if (state.chatCompleteError !== null) {
|
|
|
|
|
state.messageChunkList.forEach((chunk) => {
|
|
|
|
|
chunk.message_data = chunk.message_data.filter((message) => message.id !== 'error-user-message')
|
|
|
|
|
})
|
|
|
|
|
state.chatCompleteError = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state.messageChunkList.length === 0) {
|
|
|
|
|
state.messageChunkList.push({
|
|
|
|
|
id: startRes.chunk_id,
|
|
|
|
|
message_data: []
|
|
|
|
|
})
|
|
|
|
|
latestChunk = state.messageChunkList[0]
|
|
|
|
|
} else {
|
|
|
|
|
if (state.editingId) {
|
|
|
|
|
// 删除编辑的消息之前的消息
|
|
|
|
|
let editingChunkIndex = state.messageChunkList.findIndex((chunk) => chunk.id === state.editingId![0])
|
|
|
|
|
if (editingChunkIndex >= 0) {
|
|
|
|
|
state.messageChunkList = state.messageChunkList.slice(editingChunkIndex)
|
|
|
|
|
latestChunk = state.messageChunkList[0]
|
|
|
|
|
let editingMessageIndex = latestChunk.message_data.findIndex((message) => message.id === state.editingId![1])
|
|
|
|
|
|
|
|
|
|
if (editingMessageIndex >= 0) {
|
|
|
|
|
latestChunk.message_data = latestChunk.message_data.slice(0, editingMessageIndex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
latestChunk = state.messageChunkList[0] // 反向列表,最新对话在最前面
|
|
|
|
|
if (startRes.chunk_id !== latestChunk.id) {
|
|
|
|
|
// 新建chunk
|
|
|
|
|
latestChunk = {
|
|
|
|
|
id: startRes.chunk_id,
|
|
|
|
|
message_data: []
|
|
|
|
|
}
|
|
|
|
|
state.messageChunkList.unshift(latestChunk)
|
|
|
|
|
latestChunk = state.messageChunkList[0] // 获取reactive对象
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startAutoScroll() // 开始自动滚动
|
|
|
|
|
|
|
|
|
|
latestChunk.message_data.push({
|
|
|
|
|
role: 'user',
|
|
|
|
|
content: question,
|
|
|
|
|
time: secondTimestamp(),
|
|
|
|
|
tokens: 0
|
|
|
|
|
})
|
|
|
|
|
let userMessage = latestChunk.message_data[latestChunk.message_data.length - 1]
|
|
|
|
|
newUserMessage = userMessage
|
|
|
|
|
|
|
|
|
|
state.streamingMessage = ''
|
|
|
|
|
const chatCompleteRes = await api.chat.chatCompleteStreamOutput(startRes.task_id, (message) => {
|
|
|
|
|
state.streamingMessage += message
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let responseMessage = state.streamingMessage
|
|
|
|
|
|
|
|
|
|
latestChunk.message_data.push({
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
id: chatCompleteRes.response_message_id,
|
|
|
|
|
content: state.streamingMessage,
|
|
|
|
|
time: secondTimestamp(),
|
|
|
|
|
tokens: chatCompleteRes.message_tokens,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
state.streamingMessage = null
|
|
|
|
|
|
|
|
|
|
userMessage.id = chatCompleteRes.question_message_id
|
|
|
|
|
state.lastUserMsgId = [startRes.chunk_id, chatCompleteRes.question_message_id]
|
|
|
|
|
state.lastAssistantMsgId = [startRes.chunk_id, chatCompleteRes.response_message_id]
|
|
|
|
|
|
|
|
|
|
let description = responseMessage.substring(0, 150)
|
|
|
|
|
if (description.length < responseMessage.length) {
|
|
|
|
|
description += '...'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (startRes.conversation_id !== state.conversationId) {
|
|
|
|
|
if (isNewConversaion.value) {
|
|
|
|
|
// 新建对话
|
|
|
|
|
conversationStore.list.push({
|
|
|
|
|
id: startRes.conversation_id,
|
|
|
|
|
module: MODULE_NAME,
|
|
|
|
|
title: '未命名对话',
|
|
|
|
|
page_title: pageStore.title,
|
|
|
|
|
pinned: false,
|
|
|
|
|
description: description,
|
|
|
|
|
updated_at: secondTimestamp(),
|
|
|
|
|
extra: {}
|
|
|
|
|
})
|
|
|
|
|
setPageTitle('未命名对话')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.conversationId = startRes.conversation_id
|
|
|
|
|
|
|
|
|
|
let newUrl = route.path.replace(/\/\d+\/?$/, '') + '/' + startRes.conversation_id
|
|
|
|
|
history.replaceState({}, '', newUrl)
|
|
|
|
|
|
|
|
|
|
conversationStore.currentId = startRes.conversation_id
|
|
|
|
|
|
|
|
|
|
showPinHelp()
|
|
|
|
|
} else {
|
|
|
|
|
conversationStore.updateById(startRes.conversation_id, {
|
|
|
|
|
description,
|
|
|
|
|
updated_at: secondTimestamp(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (chatCompleteRes.delta_data.title) {
|
|
|
|
|
state.conversationTitle = chatCompleteRes.delta_data.title
|
|
|
|
|
conversationStore.updateById(startRes.conversation_id, {
|
|
|
|
|
title: chatCompleteRes.delta_data.title
|
|
|
|
|
})
|
|
|
|
|
setPageTitle(chatCompleteRes.delta_data.title)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conversationStore.sort() // 排序对话列表
|
|
|
|
|
|
|
|
|
|
state.conversationLoading = false
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
stopAutoScroll()
|
|
|
|
|
}, 150)
|
|
|
|
|
|
|
|
|
|
// 更新用户积分
|
|
|
|
|
let pointUsage = chatCompleteRes.point_usage
|
|
|
|
|
if (typeof pointUsage === 'number') {
|
|
|
|
|
if (userStore.points > 0) {
|
|
|
|
|
userStore.points = Math.max(0, userStore.points - pointUsage)
|
|
|
|
|
} else {
|
|
|
|
|
userStore.load()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
userStore.refresh()
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
state.conversationLoading = false
|
|
|
|
|
state.streamingMessage = null
|
|
|
|
|
state.chatCompleteError = err.message
|
|
|
|
|
|
|
|
|
|
if (latestChunk && newUserMessage) {
|
|
|
|
|
if (!newUserMessage.id) {
|
|
|
|
|
newUserMessage.id = 'error-user-message'
|
|
|
|
|
}
|
|
|
|
|
state.lastUserMsgId = [latestChunk.id, newUserMessage.id]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 更新索引
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const updatePageIndex = async () => {
|
|
|
|
|
// 索引页面
|
|
|
|
|
const loading = await loadingController.create({
|
|
|
|
|
message: '正在更新页面索引...',
|
|
|
|
|
spinner: 'lines',
|
|
|
|
|
translucent: true,
|
|
|
|
|
backdropDismiss: false,
|
|
|
|
|
keyboardClose: false
|
|
|
|
|
})
|
|
|
|
|
await loading.present()
|
|
|
|
|
try {
|
|
|
|
|
let params: IndexPageParams = {
|
|
|
|
|
title: pageStore.title,
|
|
|
|
|
}
|
|
|
|
|
if (settingsStore.collectionMode) {
|
|
|
|
|
params.collection = true
|
|
|
|
|
params.title = pageStore.collectionTitle
|
|
|
|
|
}
|
|
|
|
|
await api.chat.indexPage(params, (current, total, currentPage, totalPage) => {
|
|
|
|
|
if (totalPage > 1) {
|
|
|
|
|
loading.message = `正在更新页面索引... (段落:${current}/${total},页面:${currentPage}/${totalPage})`
|
|
|
|
|
} else {
|
|
|
|
|
loading.message = `正在更新页面索引... (段落:${current}/${total})`
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error(err)
|
|
|
|
|
showError(err.message)
|
|
|
|
|
} finally {
|
|
|
|
|
await loading.dismiss()
|
|
|
|
|
state.conversationLoading = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 加载消息
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const loadPrevConversationChunk = (): Promise<void> => {
|
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
|
try {
|
|
|
|
|
if (state.loadedChunkSeek >= state.conversationChunkIdList.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let chunkCount = state.conversationChunkIdList.length
|
|
|
|
|
|
|
|
|
|
let prevChunkId = state.conversationChunkIdList[chunkCount - state.loadedChunkSeek - 1]
|
|
|
|
|
|
|
|
|
|
let conversationChunk = await api.chat.getConversationChunkInfo({
|
|
|
|
|
chunk_id: prevChunkId
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
let contentEl = contentRef.value?.$el
|
|
|
|
|
let scrollEl = contentEl?.scrollEl
|
|
|
|
|
let oldScrollBottom: number | null = null
|
|
|
|
|
if (scrollEl) {
|
|
|
|
|
oldScrollBottom = scrollEl.scrollHeight - scrollEl.scrollTop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.messageChunkList.push(conversationChunk)
|
|
|
|
|
state.loadedChunkSeek += 1
|
|
|
|
|
|
|
|
|
|
nextTick(() => requestAnimationFrame(() => {
|
|
|
|
|
if (!ignoreAutoScroll) {
|
|
|
|
|
contentEl!.scrollToBottom()
|
|
|
|
|
} else if (scrollEl && oldScrollBottom !== null) { // 滚动到旧的位置
|
|
|
|
|
let newScrollTop = scrollEl.scrollHeight - oldScrollBottom
|
|
|
|
|
scrollEl.scrollTo({
|
|
|
|
|
top: newScrollTop
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
resolve()
|
|
|
|
|
}))
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
state.errorMessage = err.message
|
|
|
|
|
resolve()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadConversation = async () => {
|
|
|
|
|
try {
|
|
|
|
|
if (isNewConversaion.value) {
|
|
|
|
|
// 新建对话
|
|
|
|
|
state.conversationTitle = '未命名对话'
|
|
|
|
|
state.conversationLoading = true
|
|
|
|
|
state.botId = route.query.botId as string ?? 'default'
|
|
|
|
|
await loadBotPersona()
|
|
|
|
|
} else {
|
|
|
|
|
state.messageChunkList = []
|
|
|
|
|
state.errorMessage = null
|
|
|
|
|
state.chatCompleteError = null
|
|
|
|
|
state.formMessage = ''
|
|
|
|
|
|
|
|
|
|
conversationStore.currentId = state.conversationId
|
|
|
|
|
// 加载对话信息
|
|
|
|
|
state.conversationLoading = true
|
|
|
|
|
|
|
|
|
|
let conversationInfo = await api.base.getConversationInfo({
|
|
|
|
|
id: state.conversationId
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (conversationInfo.module !== MODULE_NAME) {
|
|
|
|
|
showError('当前对话不是聊天对话,请从对话列表重新进入', '提示')
|
|
|
|
|
throw new Error('Module mismatch')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.conversationTitle = conversationInfo.title ?? '未命名对话'
|
|
|
|
|
state.pinned = conversationInfo.pinned
|
|
|
|
|
state.botId = conversationInfo.extra.bot_id || 'default'
|
|
|
|
|
|
|
|
|
|
await loadBotPersona()
|
|
|
|
|
|
|
|
|
|
state.conversationChunkIdList = await api.chat.getConversationChunkList({
|
|
|
|
|
id: state.conversationId
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (state.conversationChunkIdList.length > 0) {
|
|
|
|
|
let contentEl = contentRef.value?.$el
|
|
|
|
|
let scrollEl = contentEl?.scrollEl
|
|
|
|
|
|
|
|
|
|
await loadPrevConversationChunk()
|
|
|
|
|
// 加载全屏chunks
|
|
|
|
|
while (state.loadedChunkSeek < state.conversationChunkIdList.length && scrollEl.scrollHeight <= scrollEl.clientHeight) {
|
|
|
|
|
await loadPrevConversationChunk()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置最新消息ID
|
|
|
|
|
for (let chunk of state.messageChunkList) {
|
|
|
|
|
chunk.message_data.forEach((message) => {
|
|
|
|
|
if (message.id) {
|
|
|
|
|
if (message.role === 'user') {
|
|
|
|
|
state.lastUserMsgId = [chunk.id, message.id]
|
|
|
|
|
} else {
|
|
|
|
|
state.lastAssistantMsgId = [chunk.id, message.id]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (state.lastUserMsgId.length > 0 && state.lastAssistantMsgId.length > 0) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
state.errorMessage = err.message
|
|
|
|
|
} finally {
|
|
|
|
|
state.conversationLoading = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onInfiniteScrollLoad = async (event: InfiniteScrollCustomEvent) => {
|
|
|
|
|
console.log('load by infinite scroll')
|
|
|
|
|
await loadPrevConversationChunk()
|
|
|
|
|
await event.target.complete()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 加载机器人信息
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const loadBotPersona = async () => {
|
|
|
|
|
if (state.personaLoaded) return
|
|
|
|
|
|
|
|
|
|
state.personaLoaded = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let personaInfo = await api.chat.getBotPersonaInfo({
|
|
|
|
|
bot_id: state.botId
|
|
|
|
|
})
|
|
|
|
|
if (isNewConversaion.value) {
|
|
|
|
|
// 新对话,加载机器人预设值
|
|
|
|
|
state.messageChunkList.push({
|
|
|
|
|
id: 0,
|
|
|
|
|
message_data: personaInfo.message_log?.map((message, index) => ({
|
|
|
|
|
...message,
|
|
|
|
|
id: '',
|
|
|
|
|
time: secondTimestamp()
|
|
|
|
|
})) ?? []
|
|
|
|
|
})
|
|
|
|
|
state.formMessage = personaInfo.default_question ?? ''
|
|
|
|
|
}
|
|
|
|
|
state.botName = personaInfo.bot_name
|
|
|
|
|
state.botAvatar = personaInfo.bot_avatar || DEFAULT_BOT_AVATAR
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 修改消息、创建分支
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const onEditMessage = async (chunkId: number, messageId?: string) => {
|
|
|
|
|
if (!messageId) {
|
|
|
|
|
showError('无法编辑这个提问,请等待当前生成完成', '提示')
|
|
|
|
|
} else {
|
|
|
|
|
let chunk = state.messageChunkList.find((chunk) => chunk.id === chunkId)
|
|
|
|
|
if (chunk) {
|
|
|
|
|
let message = chunk.message_data.find((message) => message.id === messageId)
|
|
|
|
|
if (message) {
|
|
|
|
|
state.editingId = [chunkId, messageId]
|
|
|
|
|
state.formMessage = message.content
|
|
|
|
|
|
|
|
|
|
editingToast = await toastController.create({
|
|
|
|
|
message: '正在编辑历史消息',
|
|
|
|
|
position: 'middle',
|
|
|
|
|
icon: createOutline,
|
|
|
|
|
color: 'medium',
|
|
|
|
|
buttons: [
|
|
|
|
|
{
|
|
|
|
|
text: '取消',
|
|
|
|
|
role: 'cancel',
|
|
|
|
|
handler: async () => {
|
|
|
|
|
state.editingId = null
|
|
|
|
|
state.formMessage = ''
|
|
|
|
|
await editingToast?.dismiss()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await editingToast.present()
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
showError('需要编辑的提问不存在', '提示')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onChangeResponse = async (chunkId: number, messageId?: string) => {
|
|
|
|
|
if (!messageId) {
|
|
|
|
|
showError('无法重新生成这个提问,请等待当前生成完成', '提示')
|
|
|
|
|
} else {
|
|
|
|
|
if (await showConfirm('重新生成回复将会覆盖原有的内容,且会再次消耗积分,你要继续吗?')) {
|
|
|
|
|
let chunk = state.messageChunkList.find((chunk) => chunk.id === chunkId)
|
|
|
|
|
if (chunk) {
|
|
|
|
|
let messagePos = chunk.message_data.findIndex((message) => message.id === messageId)
|
|
|
|
|
if (messagePos > 0) {
|
|
|
|
|
let questionMsg = chunk.message_data[messagePos - 1]
|
|
|
|
|
if (questionMsg.id) {
|
|
|
|
|
state.editingId = [chunkId, questionMsg.id]
|
|
|
|
|
state.formMessage = questionMsg.content
|
|
|
|
|
onSendMessage()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
showError('需要重新生成的提问不存在', '提示')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onChangeLastResponse = async () => {
|
|
|
|
|
let lastUserMessageIdx = state.messageChunkList[0]?.message_data
|
|
|
|
|
.findLastIndex((message) => message.role === 'user')
|
|
|
|
|
|
|
|
|
|
if (lastUserMessageIdx === undefined || lastUserMessageIdx < 0) {
|
|
|
|
|
showError('未找到先前的提问,请刷新页面重试', '提示')
|
|
|
|
|
} else {
|
|
|
|
|
if (await showConfirm('你确定要重新生成这段对话吗?')) {
|
|
|
|
|
let lastUserMessage = state.messageChunkList[0].message_data[lastUserMessageIdx]
|
|
|
|
|
// 删除最后一个用户提问
|
|
|
|
|
state.messageChunkList[0].message_data.splice(lastUserMessageIdx, 1)
|
|
|
|
|
state.formMessage = lastUserMessage.content
|
|
|
|
|
onSendMessage()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onCreateBranch = async (chunkId: number, messageId?: string) => {
|
|
|
|
|
if (!messageId) {
|
|
|
|
|
showError('无法在这个回答上创建分支,请等待当前生成完成', '提示')
|
|
|
|
|
} else {
|
|
|
|
|
if (await showConfirm('创建分支后不会复制全部的对话记录,但是机器人的记忆会完整复制。你要继续吗?')) {
|
|
|
|
|
let loading = await loadingController.create({
|
|
|
|
|
message: '正在创建分支...',
|
|
|
|
|
spinner: 'lines',
|
|
|
|
|
translucent: true,
|
|
|
|
|
backdropDismiss: false,
|
|
|
|
|
keyboardClose: false
|
|
|
|
|
})
|
|
|
|
|
await loading.present()
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 生成新分支的标题
|
|
|
|
|
let newTitle = `分支:${state.conversationTitle}`
|
|
|
|
|
for (var i = 1; i <= 99; i++) {
|
|
|
|
|
newTitle = `分支${i.toString().padStart(2, '0')}:${state.conversationTitle}`
|
|
|
|
|
if (!conversationStore.list.find((conversation) => conversation.title === newTitle)) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let res = await api.chat.forkConversation({
|
|
|
|
|
id: state.conversationId,
|
|
|
|
|
message_id: `${chunkId},${messageId}`,
|
|
|
|
|
new_title: newTitle
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await conversationStore.load()
|
|
|
|
|
|
|
|
|
|
router.replace({
|
|
|
|
|
name: moduleConversationRoute[MODULE_NAME],
|
|
|
|
|
params: {
|
|
|
|
|
id: res.conversation_id
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error(err)
|
|
|
|
|
} finally {
|
|
|
|
|
await loading.dismiss()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 自动滚动相关
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const startAutoScroll = () => {
|
|
|
|
|
if (autoScrollTimer) {
|
|
|
|
|
clearInterval(autoScrollTimer)
|
|
|
|
|
}
|
|
|
|
|
if (contentRef.value) {
|
|
|
|
|
let contentEl = contentRef.value.$el
|
|
|
|
|
let scrollEl = contentEl.scrollEl
|
|
|
|
|
|
|
|
|
|
autoScrollTimer = setInterval(() => {
|
|
|
|
|
if (!ignoreAutoScroll) {
|
|
|
|
|
contentEl.scrollToBottom()
|
|
|
|
|
}
|
|
|
|
|
}, 100)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stopAutoScroll = () => {
|
|
|
|
|
if (autoScrollTimer) {
|
|
|
|
|
clearInterval(autoScrollTimer)
|
|
|
|
|
autoScrollTimer = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 用户滚动时禁用自动滚动
|
|
|
|
|
const updateAutoScrollEnabled = () => {
|
|
|
|
|
if (contentRef.value) {
|
|
|
|
|
let contentEl = contentRef.value.$el
|
|
|
|
|
let scrollEl = contentEl.scrollEl
|
|
|
|
|
if (scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 30) {
|
|
|
|
|
ignoreAutoScroll = false
|
|
|
|
|
} else {
|
|
|
|
|
ignoreAutoScroll = true
|
|
|
|
|
}
|
|
|
|
|
console.log('ignoreAutoScroll', ignoreAutoScroll)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let isUserScroll = false
|
|
|
|
|
let scrollDebounceTimer: any = null
|
|
|
|
|
|
|
|
|
|
const onMainContentWheel = (ev: WheelEvent) => {
|
|
|
|
|
isUserScroll = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onMainContentTouchStart = (ev: TouchEvent) => {
|
|
|
|
|
isUserScroll = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onMainContentTouchEnd = (ev: TouchEvent) => {
|
|
|
|
|
scrollDebounceTimer = setTimeout(() => {
|
|
|
|
|
updateAutoScrollEnabled()
|
|
|
|
|
isUserScroll = false
|
|
|
|
|
}, 100)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onMainContentScroll = (ev: Event) => {
|
|
|
|
|
if (!isUserScroll) return
|
|
|
|
|
|
|
|
|
|
ignoreAutoScroll = true
|
|
|
|
|
if (scrollDebounceTimer) {
|
|
|
|
|
clearTimeout(scrollDebounceTimer)
|
|
|
|
|
}
|
|
|
|
|
scrollDebounceTimer = setTimeout(() => {
|
|
|
|
|
updateAutoScrollEnabled()
|
|
|
|
|
isUserScroll = false
|
|
|
|
|
}, 100)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 计算积分
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const updatePointUsage = async () => {
|
|
|
|
|
if (userStore.unlimitedUsage) {
|
|
|
|
|
state.pointUsage = '0'
|
|
|
|
|
state.pointUsageLoading = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state.formMessage === '') {
|
|
|
|
|
state.pointUsage = '0'
|
|
|
|
|
state.pointUsageLoading = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const pointUsage = await api.chat.getEstimatedPointUsage({
|
|
|
|
|
bot_id: state.botId,
|
|
|
|
|
question: state.formMessage,
|
|
|
|
|
})
|
|
|
|
|
if (pointUsage === -1) {
|
|
|
|
|
state.pointUsage = '???'
|
|
|
|
|
} else {
|
|
|
|
|
state.pointUsage = pointUsage.toString()
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err)
|
|
|
|
|
state.pointUsage = '???'
|
|
|
|
|
}
|
|
|
|
|
state.pointUsageLoading = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let inComposition = false
|
|
|
|
|
let formMessageInputThrottleTimer: any = null
|
|
|
|
|
|
|
|
|
|
const onInput = (event: any) => {
|
|
|
|
|
state.pointUsageLoading = true
|
|
|
|
|
|
|
|
|
|
if (event.isComposing || inComposition) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (formMessageInputThrottleTimer) {
|
|
|
|
|
clearTimeout(formMessageInputThrottleTimer)
|
|
|
|
|
}
|
|
|
|
|
formMessageInputThrottleTimer = setTimeout(() => {
|
|
|
|
|
updatePointUsage()
|
|
|
|
|
}, 300)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onCompositionStart = () => {
|
|
|
|
|
inComposition = true
|
|
|
|
|
if (formMessageInputThrottleTimer) {
|
|
|
|
|
clearTimeout(formMessageInputThrottleTimer)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onCompositionEnd = () => {
|
|
|
|
|
inComposition = false
|
|
|
|
|
if (formMessageInputThrottleTimer) {
|
|
|
|
|
clearTimeout(formMessageInputThrottleTimer)
|
|
|
|
|
}
|
|
|
|
|
formMessageInputThrottleTimer = setTimeout(() => {
|
|
|
|
|
updatePointUsage()
|
|
|
|
|
}, 400)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 点击标题显示完整标题
|
|
|
|
|
// ============================================================================
|
|
|
|
|
const fullTitlePopupState = reactive({
|
|
|
|
|
const fullTitlePopupState = reactive<FullTitlePopupState>({
|
|
|
|
|
isOpen: false,
|
|
|
|
|
event: null as any
|
|
|
|
|
event: null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const showFullTitlePopup = (ev: any) => {
|
|
|
|
|
fullTitlePopupState.isOpen = true
|
|
|
|
|
fullTitlePopupState.event = ev
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 设置
|
|
|
|
|
// ============================================================================
|
|
|
|
|
let oldSettings: any = {}
|
|
|
|
|
|
|
|
|
|
const openSettingsModal = () => {
|
|
|
|
|
state.settingsModalOpened = true
|
|
|
|
|
oldSettings = Object.assign({}, settingsStore)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onSettingsModalClose = () => {
|
|
|
|
|
state.settingsModalOpened = false
|
|
|
|
|
|
|
|
|
|
if (settingsStore.collectionMode !== oldSettings.collectionMode) {
|
|
|
|
|
updatePageIndex()
|
|
|
|
|
}
|
|
|
|
|
updatePointUsage()
|
|
|
|
|
}
|
|
|
|
|
const controller = new ChatCompleteController(
|
|
|
|
|
state,
|
|
|
|
|
contentRef,
|
|
|
|
|
isNewConversaion,
|
|
|
|
|
fullTitlePopupState,
|
|
|
|
|
route,
|
|
|
|
|
router,
|
|
|
|
|
userStore,
|
|
|
|
|
pageStore,
|
|
|
|
|
conversationStore,
|
|
|
|
|
guideStore,
|
|
|
|
|
settingsStore,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// 初始化加载
|
|
|
|
@ -777,33 +95,20 @@ const onSettingsModalClose = () => {
|
|
|
|
|
let mainContentResizeObserver: ResizeObserver | null = null
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await updatePageIndex()
|
|
|
|
|
await loadConversation()
|
|
|
|
|
await controller.updatePageIndex()
|
|
|
|
|
await controller.loadConversation()
|
|
|
|
|
|
|
|
|
|
setPageTitle(state.conversationTitle, '写作助手')
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
if (contentRef.value) {
|
|
|
|
|
let contentEl = contentRef.value.$el;
|
|
|
|
|
(<any>window)._contentEl = contentEl
|
|
|
|
|
contentEl.scrollEl.addEventListener('scroll', onMainContentScroll, { passive: true })
|
|
|
|
|
|
|
|
|
|
mainContentResizeObserver = new ResizeObserver(() => {
|
|
|
|
|
if (!ignoreAutoScroll) {
|
|
|
|
|
contentEl.scrollEl.scrollTo({
|
|
|
|
|
top: contentEl.scrollEl.scrollHeight,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
mainContentResizeObserver.observe(contentEl.scrollEl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (contentRef.value) {
|
|
|
|
|
let contentEl = contentRef.value.$el
|
|
|
|
|
contentEl.scrollEl?.removeEventListener('scroll', onMainContentScroll)
|
|
|
|
|
contentEl.scrollEl?.removeEventListener('scroll', controller.onMainContentScroll)
|
|
|
|
|
if (mainContentResizeObserver) {
|
|
|
|
|
mainContentResizeObserver.disconnect()
|
|
|
|
|
mainContentResizeObserver = null
|
|
|
|
@ -832,7 +137,7 @@ watch(() => conversationInfo.value, (currentConversation) => {
|
|
|
|
|
<ion-buttons slot="start">
|
|
|
|
|
<ion-menu-button color="primary"></ion-menu-button>
|
|
|
|
|
</ion-buttons>
|
|
|
|
|
<ion-title @click="showFullTitlePopup">
|
|
|
|
|
<ion-title @click="controller.showFullTitlePopup">
|
|
|
|
|
<loading-title :loading="state.conversationLoading" class="text-selectable">
|
|
|
|
|
{{ state.conversationTitle || '无标题' }}
|
|
|
|
|
</loading-title>
|
|
|
|
@ -855,33 +160,32 @@ watch(() => conversationInfo.value, (currentConversation) => {
|
|
|
|
|
</ion-popover>
|
|
|
|
|
</ion-header>
|
|
|
|
|
|
|
|
|
|
<ion-content :fullscreen="true" ref="contentRef" @wheel.passive="onMainContentWheel"
|
|
|
|
|
@touchstart.passive="onMainContentTouchStart" @touchend.passive="onMainContentTouchEnd">
|
|
|
|
|
<ion-content :fullscreen="true" ref="contentRef" @wheel.passive="controller.onMainContentWheel"
|
|
|
|
|
@touchstart.passive="controller.onMainContentTouchStart" @touchend.passive="controller.onMainContentTouchEnd">
|
|
|
|
|
<ion-infinite-scroll position="top" v-if="state.loadedChunkSeek < state.conversationChunkIdList.length"
|
|
|
|
|
@ion-infinite="onInfiniteScrollLoad">
|
|
|
|
|
@ion-infinite="controller.onInfiniteScrollLoad">
|
|
|
|
|
<ion-infinite-scroll-content loadingText="加载中..." loadingSpinner="lines"></ion-infinite-scroll-content>
|
|
|
|
|
</ion-infinite-scroll>
|
|
|
|
|
<!-- 消息列表 -->
|
|
|
|
|
<transition-group name="fade">
|
|
|
|
|
<div v-for="chunk in ascMessageChunkList" :key="chunk.id" class="message-chunk">
|
|
|
|
|
<template v-for="message, index in chunk.message_data" :key="index">
|
|
|
|
|
<chat-message v-if="message.role == 'user'" :sender="userStore.displayName" :avatar="userStore.displayAvatar"
|
|
|
|
|
:content="message.content" :time="message.time" :msg-id="message.id"
|
|
|
|
|
<chat-message v-if="message.role == 'user'" :sender="userStore.displayName"
|
|
|
|
|
:avatar="userStore.displayAvatar" :content="message.content" :time="message.time" :msg-id="message.id"
|
|
|
|
|
:edit="chunk.id == state.lastUserMsgId[0] && message.id == state.lastUserMsgId[1]"
|
|
|
|
|
@click-edit="onEditMessage(chunk.id, message.id)"></chat-message>
|
|
|
|
|
@click-edit="controller.onEditMessage(chunk.id, message.id)"></chat-message>
|
|
|
|
|
<chat-message v-else-if="message.role == 'assistant'" :sender="state.botName" :avatar="state.botAvatar"
|
|
|
|
|
:content="message.content" :time="message.time" assistant :cursor="message.streaming" :msg-id="message.id"
|
|
|
|
|
:change="chunk.id == state.lastAssistantMsgId[0] && message.id == state.lastAssistantMsgId[1]"
|
|
|
|
|
@click-create-branch="onCreateBranch(chunk.id, message.id)"
|
|
|
|
|
@click-change="onChangeResponse(chunk.id, message.id)"></chat-message>
|
|
|
|
|
@click-create-branch="controller.onCreateBranch(chunk.id, message.id)"
|
|
|
|
|
@click-change="controller.onChangeResponse(chunk.id, message.id)"></chat-message>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 流式输出消息 -->
|
|
|
|
|
<div v-if="state.streamingMessage !== null || state.chatCompleteError !== null" class="message-chunk">
|
|
|
|
|
<chat-message :sender="state.botName" :avatar="state.botAvatar"
|
|
|
|
|
:content="state.streamingMessage ?? undefined" :error="state.chatCompleteError ?? undefined" assistant cursor
|
|
|
|
|
:change="state.chatCompleteError !== null"
|
|
|
|
|
@click-change="onChangeLastResponse()"></chat-message>
|
|
|
|
|
<chat-message :sender="state.botName" :avatar="state.botAvatar" :content="state.streamingMessage ?? undefined"
|
|
|
|
|
:error="state.chatCompleteError ?? undefined" assistant cursor :change="state.chatCompleteError !== null"
|
|
|
|
|
@click-change="controller.onChangeLastResponse()"></chat-message>
|
|
|
|
|
</div>
|
|
|
|
|
</transition-group>
|
|
|
|
|
</ion-content>
|
|
|
|
@ -890,7 +194,7 @@ watch(() => conversationInfo.value, (currentConversation) => {
|
|
|
|
|
<ion-toolbar class="message-input-toolbar">
|
|
|
|
|
<div class="container message-input-container">
|
|
|
|
|
<ion-buttons class="message-action-btn-container">
|
|
|
|
|
<ion-button class="message-action-btn" title="选项" @click="openSettingsModal">
|
|
|
|
|
<ion-button class="message-action-btn" title="选项" @click="controller.openSettingsModal">
|
|
|
|
|
<ion-icon :icon="ellipsisHorizontalCircle" slot="icon-only"></ion-icon>
|
|
|
|
|
</ion-button>
|
|
|
|
|
</ion-buttons>
|
|
|
|
@ -899,16 +203,16 @@ watch(() => conversationInfo.value, (currentConversation) => {
|
|
|
|
|
<ion-label color="medium" v-show="!state.pointUsageLoading">{{ state.pointUsage }}</ion-label>
|
|
|
|
|
<ion-spinner v-show="state.pointUsageLoading" color="medium" name="dots"></ion-spinner>
|
|
|
|
|
</ion-chip>
|
|
|
|
|
<ion-textarea class="message-input" aria-label="输入问题" :rows="1" :maxlength="768" placeholder="输入问题"
|
|
|
|
|
auto-grow v-model="state.formMessage" @keydown="onMessageInputKeyDown" @ion-input="onInput"
|
|
|
|
|
@compositionstart="onCompositionStart" @compositionend="onCompositionEnd"
|
|
|
|
|
<ion-textarea class="message-input" aria-label="输入问题" :rows="1" :maxlength="768" placeholder="输入问题" auto-grow
|
|
|
|
|
v-model="state.formMessage" @keydown="controller.onMessageInputKeyDown" @ion-input="controller.onInput"
|
|
|
|
|
@compositionstart="controller.onCompositionStart" @compositionend="controller.onCompositionEnd"
|
|
|
|
|
:disabled="state.conversationLoading"></ion-textarea>
|
|
|
|
|
<ion-button shape="round" class="message-send-btn" title="发送" @click="onSendMessage"
|
|
|
|
|
<ion-button shape="round" class="message-send-btn" title="发送" @click="controller.onSendMessage"
|
|
|
|
|
:disabled="state.conversationLoading || state.formMessage.length === 0 || state.pointUsageLoading">
|
|
|
|
|
<ion-icon :icon="paperPlane" slot="start"></ion-icon>
|
|
|
|
|
发送
|
|
|
|
|
</ion-button>
|
|
|
|
|
<ion-button fill="clear" class="message-send-btn-mobile" title="发送" @click="onSendMessage"
|
|
|
|
|
<ion-button fill="clear" class="message-send-btn-mobile" title="发送" @click="controller.onSendMessage"
|
|
|
|
|
:disabled="state.conversationLoading || state.formMessage.length === 0 || state.pointUsageLoading">
|
|
|
|
|
<ion-icon :icon="paperPlane" slot="icon-only" size="default"></ion-icon>
|
|
|
|
|
</ion-button>
|
|
|
|
@ -916,8 +220,9 @@ watch(() => conversationInfo.value, (currentConversation) => {
|
|
|
|
|
</ion-toolbar>
|
|
|
|
|
</ion-footer>
|
|
|
|
|
|
|
|
|
|
<chat-complete-settings-modal v-model:visible="state.settingsModalOpened" @close="onSettingsModalClose"
|
|
|
|
|
@manual-update-index="updatePageIndex" :presenting-element="mainRouterOutlet?.$el"></chat-complete-settings-modal>
|
|
|
|
|
<chat-complete-settings-modal v-model:visible="state.settingsModalOpened" @close="controller.onSettingsModalClose"
|
|
|
|
|
@manual-update-index="controller.updatePageIndex"
|
|
|
|
|
:presenting-element="mainRouterOutlet?.$el"></chat-complete-settings-modal>
|
|
|
|
|
</ion-page>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|