feat: chat style change

This commit is contained in:
ulleo
2025-06-20 16:58:46 +08:00
parent 423484a99e
commit 137b2aadca
6 changed files with 223 additions and 25 deletions
+5 -1
View File
@@ -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",
+4
View File
@@ -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,
}
+27
View File
@@ -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
+25 -10
View File
@@ -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>
+143 -14
View File
@@ -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>