feat: chat

This commit is contained in:
ulleo
2025-07-16 20:12:34 +08:00
parent 5a674022fb
commit a76ebee61a
12 changed files with 1025 additions and 449 deletions
+1
View File
@@ -24,6 +24,7 @@ export interface ChatMessage {
isTyping?: boolean
first_chat?: boolean
recommended_question?: string
index: number
}
export class ChatRecord {
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 10.5759L13.4212 5.15476C13.5514 5.02459 13.7624 5.02459 13.8926 5.15476L14.364 5.62616C14.4942 5.75634 14.4942 5.96739 14.364 6.09757L8.47145 11.9901C8.2111 12.2505 7.78899 12.2505 7.52864 11.9901L1.63608 6.09757C1.50591 5.96739 1.50591 5.75634 1.63608 5.62616L2.10749 5.15476C2.23766 5.02459 2.44872 5.02459 2.57889 5.15476L8.00004 10.5759Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 6.6666L13.4212 12.0878C13.5514 12.2179 13.7624 12.2179 13.8926 12.0878L14.364 11.6164C14.4942 11.4862 14.4942 11.2751 14.364 11.1449L8.47145 5.25239C8.2111 4.99204 7.78899 4.99204 7.52864 5.25239L1.63608 11.1449C1.50591 11.2751 1.50591 11.4862 1.63608 11.6164L2.10749 12.0878C2.23766 12.2179 2.44872 12.2179 2.57889 12.0878L8.00004 6.6666Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 479 B

+4 -2
View File
@@ -82,7 +82,9 @@
"continue_to_ask": "继续提问:",
"data_analysis": "数据预测",
"data_predict": "数据分析",
"chat_search": "搜索"
"chat_search": "搜索",
"thinking": "思考中",
"thinking_step": "思考过程"
},
"ds": {
"title": "数据源",
@@ -354,4 +356,4 @@
"back_community": "还原至社区版",
"confirm_tips": "确定还原至社区版?"
}
}
}
-42
View File
@@ -1,42 +0,0 @@
<script setup lang="ts">
import type { ChatMessage } from '@/api/chat.ts'
defineProps<{
msg?: ChatMessage
}>()
</script>
<template>
<div class="chat-block-outer">
<div class="chat-block">
<slot>
<div class="chat-block-content">
<!-- <div v-dompurify-html="msg?.content"></div> -->
<div v-if="msg?.role === 'user'" v-html="msg.content"></div>
<div v-else v-dompurify-html="msg?.content"></div>
</div>
</slot>
</div>
<div class="chat-block-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<style scoped lang="less">
.chat-block-outer {
.chat-block {
border-radius: 2px;
background-color: white;
word-wrap: break-word;
white-space: pre-wrap;
.chat-block-content {
padding: 12px;
}
}
.chat-block-footer {
}
}
</style>
+35 -28
View File
@@ -1,8 +1,5 @@
<script setup lang="ts">
// import ChatBlock from './ChatBlock.vue'
// import ChatRecordFirst from './ChatRecordFirst.vue'
import { ChatInfo, type ChatMessage } from '@/api/chat.ts'
// import { UserFilled } from '@element-plus/icons-vue'
import logo_fold from '@/assets/LOGO-fold.svg'
withDefaults(
@@ -18,42 +15,52 @@ withDefaults(
</script>
<template>
<div class="chat-row" :class="{ 'right-to-left': msg.role === 'user' }">
<el-avatar v-if="msg.role === 'assistant'" class="ai-avatar" shape="square">
<el-icon>
<logo_fold v-if="!hideAvatar" />
</el-icon>
</el-avatar>
<div :class="{ 'row-full': msg.role === 'assistant' }">
<slot></slot>
<div class="chat-row-container">
<div class="chat-row" :class="{ 'right-to-left': msg.role === 'user' }">
<div v-if="msg.role === 'assistant'" class="ai-avatar">
<el-icon>
<logo_fold v-if="!hideAvatar" />
</el-icon>
</div>
<div :class="{ 'row-full': msg.role === 'assistant' }">
<slot></slot>
</div>
</div>
<slot name="footer"></slot>
</div>
</template>
<style scoped lang="less">
.chat-row {
.chat-row-container {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-direction: column;
gap: 8px;
padding: 20px 0 0;
max-width: 800px;
width: 100%;
max-width: 800px;
&.right-to-left {
flex-direction: row-reverse;
}
.chat-row {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
padding: 20px 0 0;
.row-full {
flex: 1;
width: 0;
}
width: 100%;
.ai-avatar {
font-size: 28px;
background: transparent;
width: 28px;
&.right-to-left {
flex-direction: row-reverse;
}
.row-full {
flex: 1;
width: 0;
}
.ai-avatar {
font-size: 28px;
background: transparent;
width: 28px;
}
}
}
</style>
+31 -11
View File
@@ -37,11 +37,16 @@ function clickQuestion(question: string): void {
<template>
<div v-if="computedQuestions.length > 0" class="recommend-questions">
<div v-if="firstChat">{{ t('qa.guess_u_ask') }}</div>
<div v-else>{{ t('qa.continue_to_ask') }}</div>
<div v-for="(question, index) in computedQuestions" :key="index">
<span class="question" @click="clickQuestion(question)">
<div v-else class="continue-ask">{{ t('qa.continue_to_ask') }}</div>
<div class="question-grid">
<div
v-for="(question, index) in computedQuestions"
:key="index"
class="question"
@click="clickQuestion(question)"
>
{{ question }}
</span>
</div>
</div>
</div>
</template>
@@ -49,19 +54,34 @@ function clickQuestion(question: string): void {
<style scoped lang="less">
.recommend-questions {
padding: 8px;
font-size: 12px;
font-weight: 400;
font-size: 14px;
font-weight: 500;
line-height: 22px;
display: flex;
flex-direction: column;
gap: 4px;
.continue-ask {
color: rgba(100, 106, 115, 1);
font-weight: 400;
}
.question-grid {
display: grid;
grid-gap: 12px;
grid-template-columns: repeat(2, 50%);
}
.question {
font-weight: 400;
cursor: pointer;
background: lightgray;
border-radius: 4px;
padding: 4px;
line-height: 20px;
background: rgba(245, 246, 247, 1);
min-height: 46px;
border-radius: 6px;
padding: 12px;
line-height: 22px;
&:hover {
background: grey;
background: rgba(31, 35, 41, 0.1);
}
}
}
@@ -0,0 +1,199 @@
<script setup lang="ts">
import BaseAnswer from './BaseAnswer.vue'
import { chatApi, ChatInfo, type ChatMessage, ChatRecord } from '@/api/chat.ts'
import { computed, nextTick } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
message?: ChatMessage
loading?: boolean
}>(),
{
chatList: () => [],
currentChatId: undefined,
currentChat: () => new ChatInfo(),
message: undefined,
loading: false,
}
)
const emits = defineEmits([
'finish',
'error',
'update:loading',
'update:chatList',
'update:currentChat',
'update:currentChatId',
])
const index = computed(() => {
if (props.message?.index) {
return props.message.index
}
if (props.message?.index === 0) {
return 0
}
return -1
})
const _currentChatId = computed({
get() {
return props.currentChatId
},
set(v) {
emits('update:currentChatId', v)
},
})
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const _chatList = computed({
get() {
return props.chatList
},
set(v) {
emits('update:chatList', v)
},
})
const _loading = computed({
get() {
return props.loading
},
set(v) {
emits('update:loading', v)
},
})
const sendMessage = async () => {
_loading.value = true
if (index.value < 0) {
_loading.value = false
return
}
const currentRecord: ChatRecord = _currentChat.value.records[index.value]
let error: boolean = false
if (_currentChatId.value === undefined || currentRecord.analysis_record_id === undefined) {
error = true
}
if (error) return
try {
const response = await chatApi.analysis(currentRecord.analysis_record_id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let analysis_answer = ''
let analysis_answer_thinking = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
_loading.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
_loading.value = false
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
currentRecord.error = data.content
emits('error')
break
case 'analysis-result':
analysis_answer += data.content
analysis_answer_thinking += data.reasoning_content
_currentChat.value.records[index.value].analysis = analysis_answer
_currentChat.value.records[index.value].analysis_thinking = analysis_answer_thinking
break
case 'analysis_finish':
emits('finish', currentRecord.id)
break
}
await nextTick()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
emits('error')
} finally {
_loading.value = false
}
}
defineExpose({ sendMessage, index: index.value, chatList: _chatList })
</script>
<template>
<BaseAnswer
v-if="message"
:message="message"
:reasoning-name="['analysis_thinking']"
:loading="_loading"
>
<MdComponent :message="message.record?.analysis" style="margin-top: 12px" />
<slot name="footer"></slot>
</BaseAnswer>
</template>
<style scoped lang="less"></style>
@@ -0,0 +1,159 @@
<script setup lang="ts">
import { type ChatMessage } from '@/api/chat.ts'
import { computed, onMounted, ref } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
import icon_up_outlined from '@/assets/svg/icon_up_outlined.svg'
import icon_down_outlined from '@/assets/svg/icon_down_outlined.svg'
import { useI18n } from 'vue-i18n'
const props = withDefaults(
defineProps<{
message: ChatMessage
loading?: boolean
reasoningName:
| 'sql_answer'
| 'chart_answer'
| 'analysis_thinking'
| 'predict'
| Array<'sql_answer' | 'chart_answer' | 'analysis_thinking' | 'predict'>
}>(),
{
loading: false,
}
)
const { t } = useI18n()
const show = ref<boolean>(false)
const reasoningContent = computed<Array<string>>(() => {
const names: Array<'sql_answer' | 'chart_answer' | 'analysis_thinking' | 'predict'> = []
if (typeof props.reasoningName === 'string') {
names.push(props.reasoningName)
} else {
props.reasoningName.forEach((item) => {
names.push(item)
})
}
const result: Array<string> = []
names.forEach((item) => {
if (props.message?.record) {
if (props.message?.record[item]) {
result.push(props.message?.record[item] ?? '')
}
}
})
return result
})
const hasReasoning = computed<boolean>(() => {
if (reasoningContent.value.length > 0) {
for (let i = 0; i < reasoningContent.value.length; i++) {
if (reasoningContent.value[i] && reasoningContent.value[i].trim() !== '') {
return true
}
}
}
return false
})
function clickShow() {
show.value = !show.value
}
onMounted(() => {
if (props.loading) {
show.value = true
}
})
</script>
<template>
<div class="base-answer-block">
<el-button v-if="loading || hasReasoning" class="thinking-btn" @click="clickShow">
<div class="thinking-btn-inner">
<span v-if="loading">{{ t('qa.thinking') }}</span>
<span v-else>{{ t('qa.thinking_step') }}</span>
<span class="btn-icon">
<el-icon v-if="show">
<icon_up_outlined />
</el-icon>
<el-icon v-else>
<icon_down_outlined />
</el-icon>
</span>
</div>
</el-button>
<div v-if="hasReasoning && show" class="reasoning-content">
<div v-for="(reason, _index) in reasoningContent" :key="_index" class="reasoning">
<MdComponent :message="reason" />
</div>
</div>
<div class="answer-container">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="less">
.base-answer-block {
.thinking-btn {
height: 32px;
padding: 5px 12px;
--ed-button-text-color: rgba(31, 35, 41, 1);
--ed-button-hover-text-color: var(--ed-button-text-color);
--ed-button-active-text-color: var(--ed-button-text-color);
--ed-button-bg-color: rgba(255, 255, 255, 1);
--ed-button-hover-bg-color: rgba(245, 246, 247, 1);
--ed-button-active-bg-color: rgba(239, 240, 241, 1);
--ed-button-border-color: rgba(217, 220, 223, 1);
--ed-button-hover-border-color: var(--ed-button-border-color);
--ed-button-active-border-color: var(--ed-button-border-color);
--ed-button-font-weight: 400;
.thinking-btn-inner {
display: flex;
flex-direction: row;
align-items: center;
line-height: 22px;
font-weight: 400;
font-size: 14px;
}
.btn-icon {
margin-left: 4px;
}
}
.reasoning-content {
margin-top: 8px;
display: flex;
flex-direction: column;
padding-left: 9px;
border-left: 1px solid rgba(31, 35, 41, 0.15);
gap: 8px;
.reasoning {
width: 100%;
line-height: 22px;
font-weight: 400;
font-size: 14px;
color: rgba(143, 149, 158, 1);
padding-bottom: 8px;
border-bottom: 1px solid rgba(31, 35, 41, 0.15);
&:last-child {
padding-bottom: unset;
border-bottom: unset;
}
}
}
.answer-container {
width: 100%;
}
}
</style>
@@ -0,0 +1,230 @@
<script setup lang="ts">
import BaseAnswer from './BaseAnswer.vue'
import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi } from '@/api/chat.ts'
import { computed, nextTick } from 'vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
message?: ChatMessage
loading?: boolean
reasoningName: 'sql_answer' | 'chart_answer' | Array<'sql_answer' | 'chart_answer'>
}>(),
{
chatList: () => [],
currentChatId: undefined,
currentChat: () => new ChatInfo(),
message: undefined,
loading: false,
}
)
const emits = defineEmits([
'finish',
'error',
'update:loading',
'update:chatList',
'update:currentChat',
'update:currentChatId',
])
const index = computed(() => {
if (props.message?.index) {
return props.message.index
}
if (props.message?.index === 0) {
return 0
}
return -1
})
const _currentChatId = computed({
get() {
return props.currentChatId
},
set(v) {
emits('update:currentChatId', v)
},
})
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const _chatList = computed({
get() {
return props.chatList
},
set(v) {
emits('update:chatList', v)
},
})
const _loading = computed({
get() {
return props.loading
},
set(v) {
emits('update:loading', v)
},
})
const sendMessage = async () => {
_loading.value = true
if (index.value < 0) {
_loading.value = false
return
}
const currentRecord: ChatRecord = _currentChat.value.records[index.value]
let error: boolean = false
if (_currentChatId.value === undefined) {
error = true
}
if (error) return
try {
const response = await questionApi.add({
question: currentRecord.question,
chat_id: _currentChatId.value,
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let sql_answer = ''
let chart_answer = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
_loading.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
_loading.value = false
return
}
switch (data.type) {
case 'id':
currentRecord.id = data.id
_currentChat.value.records[index.value].id = data.id
break
case 'info':
console.log(data.msg)
break
case 'brief':
_currentChat.value.brief = data.brief
_chatList.value.forEach((c: Chat) => {
if (c.id === _currentChat.value.id) {
c.brief = _currentChat.value.brief
}
})
break
case 'error':
currentRecord.error = data.content
emits('error')
break
case 'sql-result':
sql_answer += data.reasoning_content
_currentChat.value.records[index.value].sql_answer = sql_answer
break
case 'sql':
_currentChat.value.records[index.value].sql = data.content
break
case 'sql-data':
getChatData(_currentChat.value.records[index.value].id)
break
case 'chart-result':
chart_answer += data.reasoning_content
_currentChat.value.records[index.value].chart_answer = chart_answer
break
case 'chart':
_currentChat.value.records[index.value].chart = data.content
break
case 'finish':
emits('finish', currentRecord.id)
break
}
await nextTick()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
emits('error')
} finally {
_loading.value = false
}
}
function getChatData(recordId?: number) {
chatApi.get_chart_data(recordId).then((response) => {
_currentChat.value.records.forEach((record) => {
if (record.id === recordId) {
record.data = response
}
})
})
}
defineExpose({ sendMessage, index: index.value })
</script>
<template>
<BaseAnswer v-if="message" :message="message" :reasoning-name="reasoningName" :loading="_loading">
<slot></slot>
<slot name="footer"></slot>
</BaseAnswer>
</template>
<style scoped lang="less"></style>
@@ -0,0 +1,200 @@
<script setup lang="ts">
import BaseAnswer from './BaseAnswer.vue'
import { chatApi, ChatInfo, type ChatMessage, ChatRecord } from '@/api/chat.ts'
import { computed, nextTick } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
message?: ChatMessage
loading?: boolean
}>(),
{
chatList: () => [],
currentChatId: undefined,
currentChat: () => new ChatInfo(),
message: undefined,
loading: false,
}
)
const emits = defineEmits([
'finish',
'error',
'update:loading',
'update:chatList',
'update:currentChat',
'update:currentChatId',
])
const index = computed(() => {
if (props.message?.index) {
return props.message.index
}
if (props.message?.index === 0) {
return 0
}
return -1
})
const _currentChatId = computed({
get() {
return props.currentChatId
},
set(v) {
emits('update:currentChatId', v)
},
})
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const _chatList = computed({
get() {
return props.chatList
},
set(v) {
emits('update:chatList', v)
},
})
const _loading = computed({
get() {
return props.loading
},
set(v) {
emits('update:loading', v)
},
})
const sendMessage = async () => {
_loading.value = true
if (index.value < 0) {
_loading.value = false
return
}
const currentRecord: ChatRecord = _currentChat.value.records[index.value]
let error: boolean = false
if (_currentChatId.value === undefined || currentRecord.predict_record_id === undefined) {
error = true
}
if (error) return
try {
const response = await chatApi.predict(currentRecord.predict_record_id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let predict_answer = ''
let predict_content = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
_loading.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
currentRecord.error = data.content
emits('error')
break
case 'predict-result':
predict_answer += data.reasoning_content
predict_content += data.content
_currentChat.value.records[index.value].predict = predict_answer
_currentChat.value.records[index.value].predict_content = predict_content
break
case 'predict-failed':
emits('error')
break
case 'predict-success':
//currentChat.value.records[_index].predict_data = data.content
emits('finish', currentRecord.id)
break
case 'predict_finish':
_loading.value = false
break
}
await nextTick()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
emits('error')
} finally {
_loading.value = false
}
}
defineExpose({ sendMessage, index: index.value, chatList: _chatList })
</script>
<template>
<BaseAnswer v-if="message" :message="message" :reasoning-name="['predict']" :loading="_loading">
<MdComponent :message="message.record?.predict_content" style="margin-top: 12px" />
<slot name="footer"></slot>
</BaseAnswer>
</template>
<style scoped lang="less"></style>
+160 -366
View File
@@ -99,70 +99,77 @@
/>
<UserChat v-if="message.role === 'user'" :message="message" />
<template v-if="message.role === 'assistant' && !message.first_chat">
<ChatAnswer
<ChartAnswer
v-if="
message?.record?.analysis_record_id == undefined &&
message?.record?.predict_record_id == undefined
(message?.record?.analysis_record_id === undefined ||
message?.record?.analysis_record_id === null) &&
(message?.record?.predict_record_id === undefined ||
message?.record?.predict_record_id === null)
"
ref="chartAnswerRef"
:chat-list="chatList"
:current-chat="currentChat"
:current-chat-id="currentChatId"
:loading="isTyping"
:message="message"
:reasoning-name="['sql_answer', 'chart_answer']"
@finish="onChartAnswerFinish"
@error="onChartAnswerError"
>
<template v-if="message.record?.chart">
<div style="padding: 0 22px; display: flex; justify-content: flex-end">
<el-button
text
type="primary"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
@click="clickAnalysis(message.record?.id)"
>
{{ t('chat.data_analysis') }}
</el-button>
<el-button
text
type="primary"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
@click="clickPredict(message.record?.id)"
>
{{ t('chat.data_predict') }}
</el-button>
</div>
<div style="height: 100px; width: 100%; background: grey">todo</div>
<div style="padding: 0 22px; display: flex; justify-content: flex-end">
<el-button
text
type="primary"
:disabled="isTyping"
@click="clickAnalysis(message.record?.id)"
>
{{ t('chat.data_analysis') }}
</el-button>
<el-button
text
type="primary"
:disabled="isTyping"
@click="clickPredict(message.record?.id)"
>
{{ t('chat.data_predict') }}
</el-button>
</div>
<template #footer>
<RecommendQuestion
:questions="message.recommended_question"
:first-chat="message.first_chat"
@click-question="quickAsk"
/>
</template>
</ChatAnswer>
<div
</ChartAnswer>
<AnalysisAnswer
v-if="
message?.record?.analysis_record_id != undefined ||
message?.record?.predict_record_id != undefined
message?.record?.analysis_record_id !== undefined &&
message?.record?.analysis_record_id !== null
"
class="analysis-container"
>
<template v-if="message.record?.analysis || isAnalysisTyping">
<MdComponent :message="message.record?.analysis_thinking" />
<MdComponent :message="message.record?.analysis" />
</template>
<el-divider
v-if="
(message.record?.analysis || isAnalysisTyping) &&
(message.record?.predict || isPredictTyping)
"
/>
<template v-if="message.record?.predict || isPredictTyping">
<MdComponent :message="message.record?.predict" />
<MdComponent :message="message.record?.predict_content" />
<!-- <PredictChartBlock-->
<!-- :id="message.record?.id + '-predict'"-->
<!-- :data="message.record?.predict_data ?? '[]'"-->
<!-- :message="message"-->
<!-- :chart-type="data.chartType"-->
<!-- />-->
</template>
</div>
</template>
<template v-if="message.role === 'assistant'" #footer>
<RecommendQuestion
:questions="message.recommended_question"
:first-chat="message.first_chat"
@click-question="quickAsk"
ref="analysisAnswerRef"
:chat-list="chatList"
:current-chat="currentChat"
:current-chat-id="currentChatId"
:loading="isTyping"
:message="message"
@finish="onAnalysisAnswerFinish"
@error="onAnalysisAnswerError"
/>
<PredictAnswer
v-if="
message?.record?.predict_record_id !== undefined &&
message?.record?.predict_record_id !== null
"
ref="predictAnswerRef"
:chat-list="chatList"
:current-chat="currentChat"
:current-chat-id="currentChatId"
:loading="isTyping"
:message="message"
@finish="onPredictAnswerFinish"
@error="onPredictAnswerError"
/>
</template>
</ChatRow>
@@ -182,7 +189,7 @@
<div class="input-wrapper">
<el-input
v-model="inputMessage"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
:disabled="isTyping"
class="input-area"
type="textarea"
:rows="1"
@@ -195,7 +202,7 @@
link
type="primary"
class="input-icon"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
:disabled="isTyping"
@click="sendMessage"
>
<el-icon size="20">
@@ -213,11 +220,12 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'
import { Position } from '@element-plus/icons-vue'
import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi } from '@/api/chat'
import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord } from '@/api/chat'
import ChatRow from './ChatRow.vue'
import ChatAnswer from './ChatAnswer.vue'
import ChartAnswer from './answer/ChartAnswer.vue'
import AnalysisAnswer from './answer/AnalysisAnswer.vue'
import PredictAnswer from './answer/PredictAnswer.vue'
import UserChat from './chat-block/UserChat.vue'
import MdComponent from './component/MdComponent.vue'
import RecommendQuestion from './RecommendQuestion.vue'
import ChatListContainer from './ChatListContainer.vue'
import ChatCreator from '@/views/chat/ChatCreator.vue'
@@ -255,8 +263,6 @@ const chatList = ref<Array<ChatInfo>>([])
const currentChatId = ref<number | undefined>()
const currentChat = ref<ChatInfo>(new ChatInfo())
const isTyping = ref<boolean>(false)
const isAnalysisTyping = ref<boolean>(false)
const isPredictTyping = ref<boolean>(false)
const computedMessages = computed<Array<ChatMessage>>(() => {
const messages: Array<ChatMessage> = []
@@ -271,6 +277,7 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
create_time: record.create_time,
record: record,
content: record.question,
index: i,
})
}
messages.push({
@@ -280,9 +287,11 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
isTyping: i === currentChat.value.records.length - 1 && isTyping.value,
first_chat: record.first_chat,
recommended_question: record.recommended_question,
index: i,
})
}
console.log(messages)
return messages
})
@@ -378,7 +387,6 @@ async function getRecommendQuestions(record_id?: number) {
while (true) {
const { done, value } = await reader.read()
if (done) {
isTyping.value = false
break
}
@@ -415,7 +423,6 @@ async function getRecommendQuestions(record_id?: number) {
type: 'error',
showClose: true,
})
isTyping.value = false
return
}
@@ -451,6 +458,19 @@ function quickAsk(question: string) {
})
}
const chartAnswerRef = ref()
async function onChartAnswerFinish(id: number) {
loading.value = false
isTyping.value = false
await getRecommendQuestions(id)
}
function onChartAnswerError() {
loading.value = false
isTyping.value = false
}
const sendMessage = async () => {
if (!inputMessage.value.trim()) return
@@ -469,138 +489,21 @@ const sendMessage = async () => {
currentChat.value.records.push(currentRecord)
inputMessage.value = ''
let error: boolean = false
if (currentChatId.value === undefined) {
error = true
}
if (error) return
try {
const response = await questionApi.add({
question: currentRecord.question,
chat_id: currentChatId.value,
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let sql_answer = ''
let chart_answer = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
isTyping.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
nextTick(async () => {
const index = currentChat.value.records.length - 1
if (chartAnswerRef.value) {
if (chartAnswerRef.value instanceof Array) {
for (let i = 0; i < chartAnswerRef.value.length; i++) {
const _index = chartAnswerRef.value[i].index
if (index === _index) {
await chartAnswerRef.value[i].sendMessage()
break
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
isTyping.value = false
return
}
switch (data.type) {
case 'id':
currentRecord.id = data.id
currentChat.value.records[currentChat.value.records.length - 1].id = data.id
break
case 'info':
console.log(data.msg)
break
case 'brief':
currentChat.value.brief = data.brief
chatList.value.forEach((c: Chat) => {
if (c.id === currentChat.value.id) {
c.brief = currentChat.value.brief
}
})
break
case 'error':
currentRecord.error = data.content
isTyping.value = false
break
case 'sql-result':
sql_answer += data.reasoning_content
currentChat.value.records[currentChat.value.records.length - 1].sql_answer = sql_answer
break
case 'sql':
currentChat.value.records[currentChat.value.records.length - 1].sql = data.content
break
case 'sql-data':
//currentChat.value.records[currentChat.value.records.length - 1].data = data.content
getChatData(currentChat.value.records[currentChat.value.records.length - 1].id)
break
case 'chart-result':
chart_answer += data.reasoning_content
currentChat.value.records[currentChat.value.records.length - 1].chart_answer =
chart_answer
break
case 'chart':
currentChat.value.records[currentChat.value.records.length - 1].chart = data.content
break
case 'finish':
isTyping.value = false
await getRecommendQuestions(currentRecord.id)
break
}
await nextTick()
} else {
await chartAnswerRef.value.sendMessage()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
isTyping.value = false
} finally {
loading.value = false
}
}
function getChatData(recordId?: number) {
chatApi.get_chart_data(recordId).then((response) => {
currentChat.value.records.forEach((record) => {
if (record.id === recordId) {
record.data = response
}
})
})
}
@@ -614,12 +517,28 @@ function getChatPredictData(recordId?: number) {
})
}
const analysisAnswerRef = ref()
async function onAnalysisAnswerFinish(id: number) {
loading.value = false
isTyping.value = false
console.log(id)
//await getRecommendQuestions(id)
}
function onAnalysisAnswerError() {
loading.value = false
isTyping.value = false
}
async function clickAnalysis(id?: number) {
const baseRecord = find(currentChat.value.records, (value) => id === value.id)
if (baseRecord == undefined) {
return
}
loading.value = true
isTyping.value = true
const currentRecord = new ChatRecord()
currentRecord.create_time = new Date()
currentRecord.chat_id = baseRecord.chat_id
@@ -627,96 +546,41 @@ async function clickAnalysis(id?: number) {
currentRecord.chart = baseRecord.chart
currentRecord.data = baseRecord.data
currentRecord.analysis_record_id = id
currentRecord.analysis = ''
currentChat.value.records.push(currentRecord)
let _index = currentChat.value.records.length - 1
isAnalysisTyping.value = true
currentChat.value.records[_index].analysis = ''
try {
const response = await chatApi.analysis(id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let analysis_answer = ''
let analysis_answer_thinking = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
isAnalysisTyping.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
nextTick(async () => {
const index = currentChat.value.records.length - 1
if (analysisAnswerRef.value) {
if (analysisAnswerRef.value instanceof Array) {
for (let i = 0; i < analysisAnswerRef.value.length; i++) {
const _index = analysisAnswerRef.value[i].index
if (index === _index) {
await analysisAnswerRef.value[i].sendMessage()
break
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
isAnalysisTyping.value = false
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
throw Error(data.content)
case 'analysis-result':
analysis_answer += data.content
analysis_answer_thinking += data.reasoning_content
currentChat.value.records[_index].analysis = analysis_answer
currentChat.value.records[_index].analysis_thinking = analysis_answer_thinking
break
case 'analysis_finish':
isAnalysisTyping.value = false
break
}
await nextTick()
} else {
await analysisAnswerRef.value.sendMessage()
}
}
} catch (error) {
console.error('Error:', error)
ElMessage({
message: error + '',
type: 'error',
showClose: true,
})
isAnalysisTyping.value = false
}
})
return
}
const predictAnswerRef = ref()
async function onPredictAnswerFinish(id: number) {
loading.value = false
isTyping.value = false
getChatPredictData(id)
//await getRecommendQuestions(id)
}
function onPredictAnswerError() {
loading.value = false
isTyping.value = false
}
async function clickPredict(id?: number) {
@@ -725,6 +589,9 @@ async function clickPredict(id?: number) {
return
}
loading.value = true
isTyping.value = true
const currentRecord = new ChatRecord()
currentRecord.create_time = new Date()
currentRecord.chat_id = baseRecord.chat_id
@@ -732,102 +599,29 @@ async function clickPredict(id?: number) {
currentRecord.chart = baseRecord.chart
currentRecord.data = baseRecord.data
currentRecord.predict_record_id = id
currentRecord.predict = ''
currentRecord.predict_data = ''
currentChat.value.records.push(currentRecord)
let _index = currentChat.value.records.length - 1
isPredictTyping.value = true
currentChat.value.records[_index].predict = ''
currentChat.value.records[_index].predict_data = ''
try {
const response = await chatApi.predict(id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let predict_answer = ''
let predict_content = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
isPredictTyping.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
nextTick(async () => {
const index = currentChat.value.records.length - 1
if (predictAnswerRef.value) {
if (predictAnswerRef.value instanceof Array) {
for (let i = 0; i < predictAnswerRef.value.length; i++) {
const _index = predictAnswerRef.value[i].index
if (index === _index) {
await predictAnswerRef.value[i].sendMessage()
break
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
throw Error(data.content)
case 'predict-result':
predict_answer += data.reasoning_content
predict_content += data.content
currentChat.value.records[_index].predict = predict_answer
currentChat.value.records[_index].predict_content = predict_content
break
case 'predict-failed':
break
case 'predict-success':
//currentChat.value.records[_index].predict_data = data.content
getChatPredictData(currentChat.value.records[_index].id)
break
case 'predict_finish':
isPredictTyping.value = false
break
}
await nextTick()
} else {
await predictAnswerRef.value.sendMessage()
}
}
} catch (error) {
console.error('Error:', error)
ElMessage({
message: error + '',
type: 'error',
showClose: true,
})
isPredictTyping.value = false
}
})
return
}
const handleCtrlEnter = (e: KeyboardEvent) => {