修正问题

master
落雨楓 10 months ago
parent d4d858eaca
commit 86d1b9920a

@ -50,8 +50,7 @@ const settingsStore = useSettingsStore()
<template>
<ion-modal ref="modal" :is-open="props.visible" backdrop-dismiss :presenting-element="props.presentingElement"
@will-present="onOpen()"
@did-dismiss="onClose()">
@will-present="onOpen()" @did-dismiss="onClose()">
<ion-header>
<ion-toolbar>
<ion-title>
@ -74,7 +73,7 @@ const settingsStore = useSettingsStore()
<div class="help-block list-help-block">开启此功能后将会在整个设定集中抓取设定</div>
<ion-list :inset="true" lines="full" class="with-help-block">
<ion-item>
<ion-select label="设定抓取数量" v-model="settingsStore.docExtractLimit">
<ion-select label="设定抓取数量" v-model="settingsStore.docExtractLimit" ok-text="确定" cancel-text="取消">
<ion-select-option :value="0">不抓取设定</ion-select-option>
<ion-select-option :value="1">1</ion-select-option>
<ion-select-option :value="3">3</ion-select-option>

@ -11,7 +11,8 @@ const props = defineProps<{
msgId?: string,
sender: string,
avatar: string,
content: string,
content?: string,
error?: string,
time?: number,
assistant?: boolean,
cursor?: boolean,
@ -51,27 +52,30 @@ const displayTime = computed(() => {
</div>
<div class="message-content">
<div class="message-header">
<div class="header-left">
<div class="header-left text-selectable">
<span class="message-sender">{{ props.sender }}</span>
<span class="message-time">{{ displayTime }}</span>
</div>
<div class="header-right" v-if="props.msgId">
<div class="header-right" v-if="props.msgId || props.error">
<ion-button fill="clear" size="small" color="primary" v-if="props.edit" @click="emit('clickEdit')">
<ion-icon slot="start" :icon="createOutline"></ion-icon>
<ion-label>编辑</ion-label>
</ion-button>
<ion-button fill="clear" size="small" color="primary" v-if="props.change" @click="emit('clickChange')">
<ion-icon slot="start" :icon="reloadOutline"></ion-icon>
<ion-label>换一个</ion-label>
<ion-label>{{ props.error ? "重试" : "换一个" }}</ion-label>
</ion-button>
<ion-button fill="clear" size="small" color="primary" v-if="props.assistant" @click="emit('clickCreateBranch')">
<ion-button fill="clear" size="small" color="primary" v-if="props.assistant && !props.error" @click="emit('clickCreateBranch')">
<ion-icon slot="start" :icon="gitNetworkOutline"></ion-icon>
<ion-label>创建分支</ion-label>
</ion-button>
</div>
</div>
<div class="message-body">
<markdown-parser :md="props.content" :cursor="props.cursor"></markdown-parser>
<markdown-parser v-if="props.content !== undefined" :md="props.content" :cursor="props.cursor"></markdown-parser>
<div class="message-error text-selectable" v-else-if="props.error">
{{ props.error }}
</div>
</div>
</div>
<div class="avatar-fill"></div>
@ -150,6 +154,22 @@ const displayTime = computed(() => {
flex: 1 1;
width: 100%;
}
.message-error {
position: relative;
padding: .75rem 1.25rem;
margin-top: 1rem;
border: 1px solid transparent;
border-radius: 0.25rem;
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
a {
font-weight: 700;
color: #491217;
}
}
}
}
</style>

@ -17,8 +17,6 @@ const props = defineProps<{
const outputRef = ref<HTMLDivElement>()
const textTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DIV']
let currentChunkDom: HTMLDivElement | null = null
let buffer = '';
let inCodeBlock = false;
@ -43,14 +41,33 @@ const md = new MarkdownIt({
}
})
const moveCursor = () => {
const moveCursor = (dom?: HTMLElement) => {
if (outputRef.value) {
//
let lastMarkdownChild = outputRef.value.lastElementChild?.lastElementChild
if (!lastMarkdownChild) {
lastMarkdownChild = outputRef.value.lastElementChild
let lastMarkdownChunk = dom ?? outputRef.value.querySelector('.markdown-chunk:last-child')
let lastMarkdownChild: Element | null = null
if (lastMarkdownChunk) {
lastMarkdownChild = lastMarkdownChunk.lastElementChild
if (lastMarkdownChild) {
switch (lastMarkdownChild.tagName.toLowerCase()) {
case 'ul':
case 'ol':
let liElem = lastMarkdownChild.lastElementChild
if (liElem?.tagName.toLowerCase() === 'li') {
lastMarkdownChild = liElem
}
break
case 'table':
let lastTdElem = lastMarkdownChild.querySelector('tr:last-child > td:last-child')
if (lastTdElem) {
lastMarkdownChild = lastTdElem
}
break
}
}
}
if (lastMarkdownChild && textTags.includes(lastMarkdownChild.tagName)) {
if (lastMarkdownChild) {
lastMarkdownChild.appendChild(cursorDom)
} else {
outputRef.value.appendChild(cursorDom)
@ -58,33 +75,19 @@ const moveCursor = () => {
}
}
onMounted(() => {
nextTick(() => {
if (outputRef.value) {
let chunkDom = document.createElement('div')
chunkDom.classList.add('markdown-chunk')
chunkDom.innerHTML = md.render(props.md)
outputRef.value.appendChild(chunkDom)
currentChunkDom = chunkDom
let rendering = false
let renderedContent = ''
const renderDeltaContent = () => {
if (!rendering && outputRef.value) {
try {
rendering = true
let newVal = props.md
let lastNewBlock = props.md.lastIndexOf('\n\n')
if (lastNewBlock !== -1) {
buffer = props.md.substring(lastNewBlock + 2)
} else {
buffer = ''
}
moveCursor()
}
})
})
// In order to avoid markdown output errors, we need to manually append the cursor to the end of the output
outputRef.value.appendChild(cursorDom)
watch(() => props.md, (newVal, oldVal) => {
if (outputRef.value) {
if (oldVal === newVal.substring(0, oldVal.length)) {
// append
let deltaText = newVal.substring(oldVal.length)
// append new contents
let deltaText = newVal.substring(renderedContent.length)
buffer += deltaText
// Detect is in code block
@ -120,7 +123,53 @@ watch(() => props.md, (newVal, oldVal) => {
} else {
currentChunkDom!.innerHTML = md.render(buffer)
}
moveCursor(currentChunkDom!)
rendering = false
renderedContent = newVal
if (props.md.length > renderedContent.length && renderedContent === props.md.substring(0, renderedContent.length)) {
// content changed, continue render
setTimeout(() => {
renderDeltaContent()
}, 0)
}
} catch(err: any) {
rendering = false
throw err
}
}
}
onMounted(() => {
nextTick(() => {
if (outputRef.value) {
let chunkDom = document.createElement('div')
chunkDom.classList.add('markdown-chunk')
chunkDom.innerHTML = md.render(props.md)
outputRef.value.appendChild(chunkDom)
currentChunkDom = chunkDom
let lastNewBlock = props.md.lastIndexOf('\n\n')
if (lastNewBlock !== -1) {
buffer = props.md.substring(lastNewBlock + 2)
} else {
buffer = ''
}
moveCursor()
}
})
})
watch(() => props.md, (newVal, oldVal) => {
if (outputRef.value) {
if (newVal.length > oldVal.length && oldVal === newVal.substring(0, oldVal.length)) {
if (!rendering) {
renderDeltaContent()
}
} else {
outputRef.value.appendChild(cursorDom)
// replace
outputRef.value.innerHTML = ''
let chunkDom = document.createElement('div')
@ -135,8 +184,8 @@ watch(() => props.md, (newVal, oldVal) => {
} else {
buffer = ''
}
moveCursor()
}
moveCursor()
}
})

@ -364,7 +364,10 @@ onMounted(async () => {
<ion-label @click="onSelectConversation(p)">
<p class="conversation-page-title">{{ p.page_title || '' }}</p>
<div class="conversation-list-item-header">
<ion-icon v-if="p.icon" :icon="p.icon" size="small"></ion-icon>
<div class="header-icons">
<ion-icon v-if="p.pinned" :icon="heart" size="small"></ion-icon>
<ion-icon v-if="p.icon" :icon="p.icon" size="small"></ion-icon>
</div>
<h2>{{ p.title || '无标题' }}</h2>
</div>
<p>{{ p.description || ' ' }}</p>
@ -572,8 +575,10 @@ ion-menu {
white-space: nowrap;
}
>ion-icon {
flex: 0 0 24px;
>.header-icons {
display: flex;
align-items: center;
flex: 0 0;
}
}

@ -37,7 +37,8 @@ pinia.use(piniaPersist)
const app = createApp(App)
.use(pinia)
.use(IonicVue, {
mode: 'ios'
mode: 'ios',
backButtonText: '返回',
})
.use(router)

@ -3,6 +3,7 @@ import { ConversationInfo } from "@/types/base";
import { defineStore } from "pinia";
import { usePageStore } from "./page";
import { useUserStore } from ".";
import { secondTimestamp } from "@/utils/other";
export type ConversationState = {
list: ConversationInfo[],
@ -32,9 +33,12 @@ export const useConversationStore = defineStore('conversation', {
title,
module,
pinned: false,
updated_at: new Date().getTime(),
updated_at: secondTimestamp(),
extra: {}
})
let newItem = this.list[this.list.length - 1]
this.sort()
return newItem
},
updateById(id: number, data: Partial<ConversationInfo>) {
let currentItem = this.list.find((item) => item.id === id);
@ -71,7 +75,7 @@ export const useConversationStore = defineStore('conversation', {
})
this.updateById(id, {
title: newTitle,
updated_at: Math.ceil(Date.now() / 1000)
updated_at: secondTimestamp()
})
this.sort()
},
@ -99,7 +103,7 @@ export const useConversationStore = defineStore('conversation', {
})
this.updateById(id, {
pinned,
updated_at: Math.ceil(Date.now() / 1000)
updated_at: secondTimestamp()
})
this.sort()
},

@ -11,7 +11,7 @@ import {
useIonRouter
} from '@ionic/vue'
import { showConfirm, showError, showHelpToast } from '@/utils/dialog'
import { ChatCompleteChunkInfo } from '@/types/base'
import { ChatCompleteChunkInfo, ChatCompleteMessage } from '@/types/base'
import { DEFAULT_BOT_AVATAR, secondTimestamp, setPageTitle } from '@/utils/other'
import { pinConversation } from '@/utils/actions'
import { moduleConversationRoute } from '@/types/enum'
@ -39,6 +39,7 @@ const mainRouterOutlet = inject<Ref<IonRouterOutletInstance>>('mainRouterOutlet'
const state = reactive({
conversationLoading: false,
errorMessage: null as string | null,
chatCompleteError: null as null | string,
conversationTitle: '加载中...',
conversationId: 0,
pinned: false,
@ -55,6 +56,8 @@ const state = reactive({
lastUserMsgId: [] as [number, string] | [],
lastAssistantMsgId: [] as [number, string] | [],
streamingMessage: null as null | string,
editingId: null as [number, string] | null,
formMessage: '',
pointCost: '0',
@ -108,11 +111,20 @@ const onSendMessage = async () => {
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: state.editingId || undefined,
editMessageId: paramEditingId,
botId: state.botId,
inCollection: settingsStore.collectionMode,
extractLimit: settingsStore.docExtractLimit
@ -120,7 +132,14 @@ const onSendMessage = async () => {
state.formMessage = ''
await editingToast?.dismiss()
let latestChunk: ConversationChunkInfo
//
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,
@ -150,6 +169,7 @@ const onSendMessage = async () => {
message_data: []
}
state.messageChunkList.unshift(latestChunk)
latestChunk = state.messageChunkList[0] // reactive
}
}
@ -158,33 +178,35 @@ const onSendMessage = async () => {
latestChunk.message_data.push({
role: 'user',
content: question,
time: Date.now(),
time: secondTimestamp(),
tokens: 0
})
let userMessage = latestChunk.message_data[latestChunk.message_data.length - 1]
newUserMessage = userMessage
latestChunk.message_data.push({
role: 'assistant',
content: '',
tokens: 0,
streaming: true
state.streamingMessage = ''
const chatCompleteRes = await api.chat.chatCompleteStreamOutput(startRes.task_id, (message) => {
state.streamingMessage += message
})
let userMessage = latestChunk.message_data[latestChunk.message_data.length - 2]
let assistantMessage = latestChunk.message_data[latestChunk.message_data.length - 1]
let responseMessage = state.streamingMessage
const chatCompleteRes = await api.chat.chatCompleteStreamOutput(startRes.task_id, (message) => {
assistantMessage.content += message
latestChunk.message_data.push({
role: 'assistant',
id: chatCompleteRes.response_message_id,
content: state.streamingMessage,
time: secondTimestamp(),
tokens: chatCompleteRes.message_tokens,
})
assistantMessage.streaming = false
state.streamingMessage = null
userMessage.id = chatCompleteRes.question_message_id
assistantMessage.id = chatCompleteRes.response_message_id
state.lastUserMsgId = [startRes.chunk_id, chatCompleteRes.question_message_id]
state.lastAssistantMsgId = [startRes.chunk_id, chatCompleteRes.response_message_id]
let description = assistantMessage.content.substring(0, 150)
if (description.length < assistantMessage.content.length) {
let description = responseMessage.substring(0, 150)
if (description.length < responseMessage.length) {
description += '...'
}
@ -198,7 +220,7 @@ const onSendMessage = async () => {
page_title: pageStore.title,
pinned: false,
description: description,
updated_at: Math.floor(Date.now() / 1000),
updated_at: secondTimestamp(),
extra: {}
})
setPageTitle('未命名对话')
@ -206,7 +228,7 @@ const onSendMessage = async () => {
state.conversationId = startRes.conversation_id
let newUrl = route.fullPath.replace(/\/\d+$/, '') + '/' + startRes.conversation_id
let newUrl = route.path.replace(/\/\d+\/?$/, '') + '/' + startRes.conversation_id
history.replaceState({}, '', newUrl)
conversationStore.currentId = startRes.conversation_id
@ -215,7 +237,7 @@ const onSendMessage = async () => {
} else {
conversationStore.updateById(startRes.conversation_id, {
description,
updated_at: Math.floor(Date.now() / 1000),
updated_at: secondTimestamp(),
})
}
@ -226,12 +248,14 @@ const onSendMessage = async () => {
})
setPageTitle(chatCompleteRes.delta_data.title)
}
conversationStore.sort() //
state.conversationLoading = false
nextTick(() => {
setTimeout(() => {
stopAutoScroll()
})
}, 150)
//
let pointCost = chatCompleteRes.point_cost
@ -245,6 +269,15 @@ const onSendMessage = async () => {
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]
}
}
}
@ -342,6 +375,7 @@ const loadConversation = async () => {
} else {
state.messageChunkList = []
state.errorMessage = null
state.chatCompleteError = null
state.formMessage = ''
conversationStore.currentId = state.conversationId
@ -379,16 +413,21 @@ const loadConversation = async () => {
}
// ID
let latestChunk = state.messageChunkList[0]
latestChunk.message_data.forEach((message) => {
if (message.id) {
if (message.role === 'user') {
state.lastUserMsgId = [latestChunk.id, message.id]
} else {
state.lastAssistantMsgId = [latestChunk.id, message.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
@ -464,7 +503,7 @@ const onEditMessage = async (chunkId: number, messageId?: string) => {
}
},
]
});
})
await editingToast.present()
@ -498,6 +537,23 @@ const onChangeResponse = async (chunkId: number, messageId?: string) => {
}
}
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('无法在这个回答上创建分支,请等待当前生成完成', '提示')
@ -514,7 +570,7 @@ const onCreateBranch = async (chunkId: number, messageId?: string) => {
try {
//
let newTitle = `分支:${state.conversationTitle}`;
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)) {
@ -741,7 +797,7 @@ onMounted(async () => {
onBeforeUnmount(() => {
if (contentRef.value) {
let contentEl = contentRef.value.$el
contentEl.scrollEl.removeEventListener('scroll', onMainContentScroll)
contentEl.scrollEl?.removeEventListener('scroll', onMainContentScroll)
if (mainContentResizeObserver) {
mainContentResizeObserver.disconnect()
mainContentResizeObserver = null
@ -814,6 +870,13 @@ watch(() => conversationInfo.value, (currentConversation) => {
@click-change="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>
</div>
</transition-group>
</ion-content>

Loading…
Cancel
Save