perf: Data Q&A

This commit is contained in:
fit2cloud-chenyw
2025-05-14 13:11:57 +08:00
parent de8bad4225
commit dfdc5a45a6
8 changed files with 389 additions and 66 deletions
+7 -2
View File
@@ -2,8 +2,13 @@ import { request } from '@/utils/request'
export const questionApi = {
pager: (pageNumber: number, pageSize: number) => request.get(`/chat/question/pager/${pageNumber}/${pageSize}`),
// add: (data: any, progress: any) => request.post('/chat/question', data, { responseType: 'stream', onDownloadProgress: progress }),
add: (data: any) => request.post('/chat/question', data),
/* add: (data: any) => new Promise((resolve, reject) => {
request.post('/chat/question', data, { responseType: 'stream', timeout: 0, onDownloadProgress: p => {
resolve(p)
}}).catch(e => reject(e))
}), */
// add: (data: any) => request.post('/chat/question', data),
add: (data: any) => request.fetchStream('/chat/question', data),
edit: (data: any) => request.put('/chat/question', data),
delete: (id: number) => request.delete(`/chat/question/${id}`),
query: (id: number) => request.get(`/chat/question/${id}`)
+17 -2
View File
@@ -49,7 +49,7 @@ class HttpService {
this.cancelTokenSource = axios.CancelToken.source()
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 50000,
timeout: 100000,
headers: {
'Content-Type': 'application/json',
...config?.headers
@@ -79,7 +79,6 @@ class HttpService {
return Promise.reject(error)
}
)
// Response interceptor
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
@@ -207,6 +206,22 @@ class HttpService {
return this.request({ ...config, method: 'POST', url, data })
}
public fetchStream(url: string ,data?: any): Promise<any> {
const token = wsCache.get('user.token')
const heads: any = {
'Content-Type': 'application/json',
}
if (token) {
heads['X-SQLBOT-TOKEN'] = `Bearer ${token}`
}
const real_url = import.meta.env.VITE_API_BASE_URL
return fetch(real_url + url, {
method: 'POST',
headers: heads,
body: JSON.stringify(data),
})
}
// PUT request
public put<T = any>(
url: string,
+111 -40
View File
@@ -48,10 +48,17 @@
v-for="(msg, index) in currentChat.messages"
:key="index"
class="message"
:class="msg.role"
:class="[msg.role, { 'is-typing': msg.isTyping }]"
>
<!-- <div class="message-content">{{ msg.content }}</div> -->
<div class="message-content" v-html="formatMessage(msg.content)"></div>
<div
v-if="msg.role === 'assistant'"
class="message-content"
:class="{ 'typing': msg.isTyping }"
v-html="formatMessage(msg.content)"
></div>
<div v-else class="message-content">
{{ msg.content }}
</div>
</div>
</template>
<div v-else class="empty-state">
@@ -92,10 +99,10 @@ import { ref, computed, nextTick, watch } from 'vue'
import { Plus, Search, More, Position } from '@element-plus/icons-vue'
import { questionApi } from '@/api/chat'
interface ChatMessage {
role: 'user' | 'assistant'
content: string
isTyping?: boolean
}
interface Chat {
@@ -135,7 +142,7 @@ const switchChat = (chatId: string) => {
}
const scrollToBottom = () => {
nextTick(() => {
/* nextTick(() => {
if (messagesRef.value) {
const scrollHeight = messagesRef.value.scrollHeight
const animateScroll = () => {
@@ -148,20 +155,30 @@ const scrollToBottom = () => {
}
animateScroll()
}
})
}) */
}
const sendMessage = () => {
const updateMessageContent = (index: number, content: string) => {
if (currentChat.value) {
currentChat.value.messages[index] = {
...currentChat.value.messages[index],
content
}
}
}
const sendMessage = async () => {
if (!inputMessage.value.trim()) return
let chat = currentChat.value
// if (!chat) return
if (!chat) {
chat = {
id: null,
title: 'test',
id: Date.now().toString(),
title: 'New Chat',
messages: []
} as unknown as Chat
}
chatHistory.value.unshift(chat)
currentChatId.value = chat.id
}
chat.messages.push({
@@ -169,23 +186,67 @@ const sendMessage = () => {
content: inputMessage.value
})
const param = {
question: inputMessage.value
}
inputMessage.value = ''
chatHistory.value.unshift(chat)
currentChatId.value = chat.id
// const index = chat.messages.length - 1
questionApi.add(param).then((result: string) => {
// chat.messages[index]['content'] += result
chat.messages.push({
role: 'assistant',
content: result
})
scrollToBottom()
const defaultMsg = 'Thinking...'
chat.messages.push({
role: 'assistant',
content: defaultMsg,
isTyping: true
})
const userInput = inputMessage.value
inputMessage.value = ''
const index = chat.messages.length - 1
try {
const response = await questionApi.add({ question: userInput })
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) {
chat.messages[index].isTyping = false
break
}
const chunk = decoder.decode(value)
const lines = chunk.split('\n\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.replace('data:', '').trim())
let realContent = data.content
switch (data.type) {
case 'html':
realContent = '\n' + data.content
break
case 'error':
realContent = '\nError: ' + data.content
break
default:
if (data.content) {
const currentContent = chat.messages[index].content
const newContent = currentContent.startsWith(defaultMsg)
? realContent
: currentContent + realContent
updateMessageContent(index, newContent)
await nextTick()
scrollToBottom()
}
}
}
}
}
} catch (error) {
console.error('Error:', error)
updateMessageContent(index, chat.messages[index].content + '\nError: Failed to get response')
chat.messages[index].isTyping = false
}
}
watch(() => currentChat.value?.messages, () => {
@@ -231,7 +292,7 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
<style lang="less" scoped>
.chat-container {
height: 100%;
height: calc(100vh - 61px);
display: flex;
background-color: var(--white);
border-radius: var(--border-radius);
@@ -305,11 +366,16 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
overflow: hidden;
.chat-messages {
flex: 1;
// flex: 1;
height: calc(100% - 188px);
overflow-y: auto;
padding: 24px;
scroll-behavior: smooth;
&::-webkit-scrollbar {
width: 6px;
@@ -317,11 +383,11 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
}
&::-webkit-scrollbar-thumb {
background: #ccc;
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background: #999;
background: rgba(0, 0, 0, 0.3);
}
}
@@ -335,6 +401,7 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
.message {
margin-bottom: 24px;
transition: all 0.3s ease;
&.user {
text-align: right;
@@ -343,9 +410,11 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
}
}
&.assistant .message-content {
background-color: #f5f5f5;
text-align: left;
&.assistant {
.message-content {
background-color: #f5f5f5;
text-align: left;
}
}
.message-content {
@@ -353,6 +422,7 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
padding: 12px 16px;
border-radius: 8px;
max-width: 80%;
transition: all 0.3s ease;
}
}
@@ -383,9 +453,14 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
}
.chat-input {
padding: 16px 24px;
border-top: 1px solid var(--el-border-color-lighter);
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-top: 1px solid var(--el-border-color-lighter);
padding: 16px 24px;
z-index: 10;
.input-wrapper {
position: relative;
@@ -402,8 +477,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
:deep(.el-textarea__inner) {
padding: 16px;
font-size: 14px;
// line-height: 1.6;
// min-height: 80px;
resize: none;
border: none;
background: transparent;
@@ -427,8 +500,6 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
padding: 8px;
}
}
}
}
}