mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat: chat style change
This commit is contained in:
@@ -19,17 +19,20 @@
|
||||
"@antv/g2": "^5.3.3",
|
||||
"@antv/s2": "^2.4.3",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@highlightjs/vue-plugin": "^2.1.0",
|
||||
"@npkg/tinymce-plugins": "^0.0.7",
|
||||
"@tinymce/tinymce-vue": "^5.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-plus": "^2.10.1",
|
||||
"element-plus-secondary": "^1.0.0",
|
||||
"element-resize-detector": "^1.2.4",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"snowflake-id": "^1.1.0",
|
||||
"tinymce": "^5.10.9",
|
||||
"html2canvas": "^1.4.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-dompurify-html": "^5.3.0",
|
||||
"vue-i18n": "^9.14.4",
|
||||
@@ -41,6 +44,7 @@
|
||||
"@eslint/migrate-config": "^1.5.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/element-resize-detector": "^1.1.6",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
|
||||
@@ -276,6 +276,9 @@ function deleteChat(id: number | undefined): Promise<string> {
|
||||
function analysis(record_id: number | undefined) {
|
||||
return request.fetchStream(`/chat/record/${record_id}/analysis`, {})
|
||||
}
|
||||
function predict(record_id: number | undefined) {
|
||||
return request.fetchStream(`/chat/record/${record_id}/predict`, {})
|
||||
}
|
||||
|
||||
export const chatApi = {
|
||||
toChatRecord,
|
||||
@@ -288,4 +291,5 @@ export const chatApi = {
|
||||
renameChat,
|
||||
deleteChat,
|
||||
analysis,
|
||||
predict,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
highlight: (str, lang): string => {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return `
|
||||
<pre>
|
||||
<code class="hljs">
|
||||
${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
|
||||
</code>
|
||||
</pre>
|
||||
`
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code></pre>'
|
||||
},
|
||||
})
|
||||
|
||||
export default md
|
||||
@@ -3,6 +3,7 @@ import type { ChatMessage } from '@/api/chat.ts'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import ChartComponent from './component/ChartComponent.vue'
|
||||
import MdComponent from './component/MdComponent.vue'
|
||||
import type { ChartTypes } from '@/views/chat/component/BaseChart.ts'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import ICON_BAR from '@/assets/svg/chart/bar.svg'
|
||||
@@ -27,13 +28,20 @@ const settings = ref<{
|
||||
})
|
||||
|
||||
const renderSqlThinking = computed(() => {
|
||||
//todo md render?
|
||||
return props.message?.record?.sql_answer
|
||||
return props.message?.record?.sql_answer ?? ''
|
||||
})
|
||||
|
||||
const renderChartThinking = computed(() => {
|
||||
//todo md render?
|
||||
return props.message?.record?.chart_answer
|
||||
return props.message?.record?.chart_answer ?? ''
|
||||
})
|
||||
|
||||
const renderSQL = computed(() => {
|
||||
return props.message?.record?.sql
|
||||
? `\`\`\`sql
|
||||
|
||||
${props.message?.record?.sql}
|
||||
`
|
||||
: ''
|
||||
})
|
||||
|
||||
const dataObject = computed<{
|
||||
@@ -212,7 +220,7 @@ function onTypeChange() {
|
||||
<el-container direction="vertical">
|
||||
<template v-if="message.record">
|
||||
<el-collapse expand-icon-position="left">
|
||||
<el-collapse-item name="1">
|
||||
<el-collapse-item name="1" class="md-collapse">
|
||||
<template #title>
|
||||
{{ t('chat.inference_process') }}
|
||||
<el-icon v-if="props.message?.isTyping">
|
||||
@@ -222,12 +230,15 @@ function onTypeChange() {
|
||||
<div>
|
||||
<template v-if="message.record.sql_answer">
|
||||
<div style="font-weight: 500">{{ t('chat.sql_generation') }}:</div>
|
||||
<div v-if="message.record.sql_answer" v-dompurify-html="renderSqlThinking"></div>
|
||||
<MdComponent
|
||||
v-if="message.record.sql_answer"
|
||||
:message="renderSqlThinking"
|
||||
></MdComponent>
|
||||
</template>
|
||||
<template v-if="message.record.chart_answer">
|
||||
<el-divider></el-divider>
|
||||
<div style="font-weight: 500">{{ t('chat.chart_generation') }}:</div>
|
||||
<div v-dompurify-html="renderChartThinking"></div>
|
||||
<MdComponent :message="renderChartThinking"></MdComponent>
|
||||
</template>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
@@ -235,9 +246,7 @@ function onTypeChange() {
|
||||
<div class="answer-content">
|
||||
<template v-if="settings.type === 'sql'">
|
||||
<div>
|
||||
<div v-if="message.record.sql">
|
||||
{{ message.record.sql }}
|
||||
</div>
|
||||
<MdComponent v-if="message.record.sql" :message="renderSQL"></MdComponent>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="settings.type === 'chart'">
|
||||
@@ -295,6 +304,12 @@ function onTypeChange() {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.md-collapse {
|
||||
:deep(.ed-collapse-item__content) {
|
||||
padding: 16px 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.base-chart-choose-btn {
|
||||
cursor: pointer;
|
||||
color: var(--ed-color-primary);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import md from '@/utils/markdown.ts'
|
||||
import 'highlight.js/styles/github-dark-dimmed.css'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
message?: string
|
||||
}>()
|
||||
|
||||
const renderMd = computed(() => {
|
||||
return md.render(props.message ?? '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-dompurify-html="renderMd"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -34,10 +34,20 @@
|
||||
<template v-if="message.role === 'assistant'">
|
||||
<ChatAnswer :message="message">
|
||||
<template #footer>
|
||||
<el-button text type="primary" @click="clickAnalysis(message.record?.id)">{{
|
||||
t('chat.data_analysis')
|
||||
}}</el-button>
|
||||
{{ message.record?.analysis }}
|
||||
<div style="padding: 0 22px; display: flex; justify-content: flex-end">
|
||||
<el-button text type="primary" @click="clickAnalysis(message.record?.id)">
|
||||
{{ t('chat.data_analysis') }}
|
||||
</el-button>
|
||||
<el-button text type="primary" @click="clickPredict(message.record?.id)">
|
||||
{{ t('chat.data_predict') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="analysis-container">
|
||||
<MdComponent
|
||||
v-if="message.record?.analysis || isAnalysisTyping"
|
||||
:message="message.record?.analysis"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ChatAnswer>
|
||||
</template>
|
||||
@@ -90,6 +100,7 @@ import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi } fr
|
||||
import ChatList from './ChatList.vue'
|
||||
import ChatRow from './ChatRow.vue'
|
||||
import ChatAnswer from './ChatAnswer.vue'
|
||||
import MdComponent from './component/MdComponent.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { find } from 'lodash-es'
|
||||
|
||||
@@ -115,6 +126,7 @@ 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 welcome: ChatMessage = {
|
||||
@@ -306,6 +318,16 @@ const sendMessage = async () => {
|
||||
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':
|
||||
currentChat.value.records[currentChat.value.records.length - 1].id = data.id
|
||||
@@ -412,14 +434,22 @@ async function clickAnalysis(id?: number) {
|
||||
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':
|
||||
currentRecord.error = data.content
|
||||
isAnalysisTyping.value = false
|
||||
break
|
||||
throw Error(data.content)
|
||||
case 'analysis-result':
|
||||
analysis_answer += data.content
|
||||
currentChat.value.records[_index].analysis = analysis_answer
|
||||
@@ -432,18 +462,110 @@ async function clickAnalysis(id?: number) {
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
ElMessage({
|
||||
message: error + '',
|
||||
type: 'error',
|
||||
showClose: true,
|
||||
})
|
||||
isAnalysisTyping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function clickPredict(id?: number) {
|
||||
let _index = -1
|
||||
const currentRecord = find(currentChat.value.records, (value, index) => {
|
||||
if (id === value.id) {
|
||||
_index = index
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (currentRecord == undefined) {
|
||||
return
|
||||
}
|
||||
currentChat.value.records[_index].predict = ''
|
||||
|
||||
try {
|
||||
const response = await chatApi.predict(id)
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
let predict_answer = ''
|
||||
|
||||
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()
|
||||
}
|
||||
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.content
|
||||
currentChat.value.records[_index].predict = predict_answer
|
||||
break
|
||||
case 'predict_finish':
|
||||
isPredictTyping.value = false
|
||||
break
|
||||
}
|
||||
await nextTick()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
ElMessage({
|
||||
message: error + '',
|
||||
type: 'error',
|
||||
showClose: true,
|
||||
})
|
||||
isPredictTyping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
const textarea = e.target as HTMLTextAreaElement
|
||||
const start = textarea.selectionStart
|
||||
@@ -522,4 +644,11 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-container {
|
||||
color: var(--ed-text-color-primary);
|
||||
font-size: 12px;
|
||||
line-height: 1.7692307692;
|
||||
padding: 16px 22px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user