Skip to content

Commit 137b2aa

Browse files
committed
feat: chat style change
1 parent 423484a commit 137b2aa

File tree

6 files changed

+222
-24
lines changed

6 files changed

+222
-24
lines changed

frontend/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@
1919
"@antv/g2": "^5.3.3",
2020
"@antv/s2": "^2.4.3",
2121
"@eslint/js": "^9.28.0",
22+
"@highlightjs/vue-plugin": "^2.1.0",
2223
"@npkg/tinymce-plugins": "^0.0.7",
2324
"@tinymce/tinymce-vue": "^5.1.0",
2425
"dayjs": "^1.11.13",
2526
"element-plus": "^2.10.1",
2627
"element-plus-secondary": "^1.0.0",
2728
"element-resize-detector": "^1.2.4",
29+
"highlight.js": "^11.11.1",
30+
"html2canvas": "^1.4.1",
2831
"lodash": "^4.17.21",
2932
"lodash-es": "^4.17.21",
33+
"markdown-it": "^14.1.0",
3034
"snowflake-id": "^1.1.0",
3135
"tinymce": "^5.10.9",
32-
"html2canvas": "^1.4.1",
3336
"vue": "^3.5.13",
3437
"vue-dompurify-html": "^5.3.0",
3538
"vue-i18n": "^9.14.4",
@@ -41,6 +44,7 @@
4144
"@eslint/migrate-config": "^1.5.0",
4245
"@types/crypto-js": "^4.2.2",
4346
"@types/element-resize-detector": "^1.1.6",
47+
"@types/markdown-it": "^14.1.2",
4448
"@types/node": "^22.14.1",
4549
"@typescript-eslint/eslint-plugin": "^8.34.0",
4650
"@typescript-eslint/parser": "^8.34.0",

frontend/src/api/chat.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ function deleteChat(id: number | undefined): Promise<string> {
276276
function analysis(record_id: number | undefined) {
277277
return request.fetchStream(`/chat/record/${record_id}/analysis`, {})
278278
}
279+
function predict(record_id: number | undefined) {
280+
return request.fetchStream(`/chat/record/${record_id}/predict`, {})
281+
}
279282

280283
export const chatApi = {
281284
toChatRecord,
@@ -288,4 +291,5 @@ export const chatApi = {
288291
renameChat,
289292
deleteChat,
290293
analysis,
294+
predict,
291295
}

frontend/src/utils/markdown.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import MarkdownIt from 'markdown-it'
2+
import hljs from 'highlight.js'
3+
4+
const md = new MarkdownIt({
5+
html: true,
6+
linkify: true,
7+
highlight: (str, lang): string => {
8+
if (lang && hljs.getLanguage(lang)) {
9+
try {
10+
return `
11+
<pre>
12+
<code class="hljs">
13+
${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
14+
</code>
15+
</pre>
16+
`
17+
} catch (e) {
18+
console.error(e)
19+
return str
20+
}
21+
}
22+
23+
return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code></pre>'
24+
},
25+
})
26+
27+
export default md

frontend/src/views/chat/ChatAnswer.vue

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ChatMessage } from '@/api/chat.ts'
33
import { computed, nextTick, ref } from 'vue'
44
import { Loading } from '@element-plus/icons-vue'
55
import ChartComponent from './component/ChartComponent.vue'
6+
import MdComponent from './component/MdComponent.vue'
67
import type { ChartTypes } from '@/views/chat/component/BaseChart.ts'
78
import { ArrowDown } from '@element-plus/icons-vue'
89
import ICON_BAR from '@/assets/svg/chart/bar.svg'
@@ -27,13 +28,20 @@ const settings = ref<{
2728
})
2829
2930
const renderSqlThinking = computed(() => {
30-
//todo md render?
31-
return props.message?.record?.sql_answer
31+
return props.message?.record?.sql_answer ?? ''
3232
})
3333
3434
const renderChartThinking = computed(() => {
35-
//todo md render?
36-
return props.message?.record?.chart_answer
35+
return props.message?.record?.chart_answer ?? ''
36+
})
37+
38+
const renderSQL = computed(() => {
39+
return props.message?.record?.sql
40+
? `\`\`\`sql
41+
42+
${props.message?.record?.sql}
43+
`
44+
: ''
3745
})
3846
3947
const dataObject = computed<{
@@ -212,7 +220,7 @@ function onTypeChange() {
212220
<el-container direction="vertical">
213221
<template v-if="message.record">
214222
<el-collapse expand-icon-position="left">
215-
<el-collapse-item name="1">
223+
<el-collapse-item name="1" class="md-collapse">
216224
<template #title>
217225
{{ t('chat.inference_process') }}
218226
<el-icon v-if="props.message?.isTyping">
@@ -222,22 +230,23 @@ function onTypeChange() {
222230
<div>
223231
<template v-if="message.record.sql_answer">
224232
<div style="font-weight: 500">{{ t('chat.sql_generation') }}:</div>
225-
<div v-if="message.record.sql_answer" v-dompurify-html="renderSqlThinking"></div>
233+
<MdComponent
234+
v-if="message.record.sql_answer"
235+
:message="renderSqlThinking"
236+
></MdComponent>
226237
</template>
227238
<template v-if="message.record.chart_answer">
228239
<el-divider></el-divider>
229240
<div style="font-weight: 500">{{ t('chat.chart_generation') }}:</div>
230-
<div v-dompurify-html="renderChartThinking"></div>
241+
<MdComponent :message="renderChartThinking"></MdComponent>
231242
</template>
232243
</div>
233244
</el-collapse-item>
234245
</el-collapse>
235246
<div class="answer-content">
236247
<template v-if="settings.type === 'sql'">
237248
<div>
238-
<div v-if="message.record.sql">
239-
{{ message.record.sql }}
240-
</div>
249+
<MdComponent v-if="message.record.sql" :message="renderSQL"></MdComponent>
241250
</div>
242251
</template>
243252
<template v-else-if="settings.type === 'chart'">
@@ -295,6 +304,12 @@ function onTypeChange() {
295304
gap: 4px;
296305
}
297306
307+
.md-collapse {
308+
:deep(.ed-collapse-item__content) {
309+
padding: 16px 22px;
310+
}
311+
}
312+
298313
.base-chart-choose-btn {
299314
cursor: pointer;
300315
color: var(--ed-color-primary);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
import md from '@/utils/markdown.ts'
3+
import 'highlight.js/styles/github-dark-dimmed.css'
4+
import { computed } from 'vue'
5+
6+
const props = defineProps<{
7+
message?: string
8+
}>()
9+
10+
const renderMd = computed(() => {
11+
return md.render(props.message ?? '')
12+
})
13+
</script>
14+
15+
<template>
16+
<div v-dompurify-html="renderMd"></div>
17+
</template>
18+
19+
<style scoped lang="less"></style>

frontend/src/views/chat/index.vue

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,20 @@
3434
<template v-if="message.role === 'assistant'">
3535
<ChatAnswer :message="message">
3636
<template #footer>
37-
<el-button text type="primary" @click="clickAnalysis(message.record?.id)">{{
38-
t('chat.data_analysis')
39-
}}</el-button>
40-
{{ message.record?.analysis }}
37+
<div style="padding: 0 22px; display: flex; justify-content: flex-end">
38+
<el-button text type="primary" @click="clickAnalysis(message.record?.id)">
39+
{{ t('chat.data_analysis') }}
40+
</el-button>
41+
<el-button text type="primary" @click="clickPredict(message.record?.id)">
42+
{{ t('chat.data_predict') }}
43+
</el-button>
44+
</div>
45+
<div class="analysis-container">
46+
<MdComponent
47+
v-if="message.record?.analysis || isAnalysisTyping"
48+
:message="message.record?.analysis"
49+
/>
50+
</div>
4151
</template>
4252
</ChatAnswer>
4353
</template>
@@ -90,6 +100,7 @@ import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi } fr
90100
import ChatList from './ChatList.vue'
91101
import ChatRow from './ChatRow.vue'
92102
import ChatAnswer from './ChatAnswer.vue'
103+
import MdComponent from './component/MdComponent.vue'
93104
import { useI18n } from 'vue-i18n'
94105
import { find } from 'lodash-es'
95106
@@ -115,6 +126,7 @@ const currentChatId = ref<number | undefined>()
115126
const currentChat = ref<ChatInfo>(new ChatInfo())
116127
const isTyping = ref<boolean>(false)
117128
const isAnalysisTyping = ref<boolean>(false)
129+
const isPredictTyping = ref<boolean>(false)
118130
119131
const computedMessages = computed<Array<ChatMessage>>(() => {
120132
const welcome: ChatMessage = {
@@ -306,6 +318,16 @@ const sendMessage = async () => {
306318
throw err
307319
}
308320
321+
if (data.code && data.code !== 200) {
322+
ElMessage({
323+
message: data.msg,
324+
type: 'error',
325+
showClose: true,
326+
})
327+
isTyping.value = false
328+
return
329+
}
330+
309331
switch (data.type) {
310332
case 'id':
311333
currentChat.value.records[currentChat.value.records.length - 1].id = data.id
@@ -412,14 +434,22 @@ async function clickAnalysis(id?: number) {
412434
throw err
413435
}
414436
437+
if (data.code && data.code !== 200) {
438+
ElMessage({
439+
message: data.msg,
440+
type: 'error',
441+
showClose: true,
442+
})
443+
isAnalysisTyping.value = false
444+
return
445+
}
446+
415447
switch (data.type) {
416448
case 'info':
417449
console.log(data.msg)
418450
break
419451
case 'error':
420-
currentRecord.error = data.content
421-
isAnalysisTyping.value = false
422-
break
452+
throw Error(data.content)
423453
case 'analysis-result':
424454
analysis_answer += data.content
425455
currentChat.value.records[_index].analysis = analysis_answer
@@ -432,15 +462,107 @@ async function clickAnalysis(id?: number) {
432462
}
433463
}
434464
} catch (error) {
435-
if (!currentRecord.error) {
436-
currentRecord.error = ''
465+
console.error('Error:', error)
466+
ElMessage({
467+
message: error + '',
468+
type: 'error',
469+
showClose: true,
470+
})
471+
isAnalysisTyping.value = false
472+
}
473+
}
474+
475+
async function clickPredict(id?: number) {
476+
let _index = -1
477+
const currentRecord = find(currentChat.value.records, (value, index) => {
478+
if (id === value.id) {
479+
_index = index
480+
return true
437481
}
438-
if (currentRecord.error.trim().length !== 0) {
439-
currentRecord.error = currentRecord.error + '\n'
482+
return false
483+
})
484+
if (currentRecord == undefined) {
485+
return
486+
}
487+
currentChat.value.records[_index].predict = ''
488+
489+
try {
490+
const response = await chatApi.predict(id)
491+
const reader = response.body.getReader()
492+
const decoder = new TextDecoder()
493+
494+
let predict_answer = ''
495+
496+
while (true) {
497+
const { done, value } = await reader.read()
498+
if (done) {
499+
isPredictTyping.value = false
500+
break
501+
}
502+
503+
const chunk = decoder.decode(value)
504+
505+
let _list = [chunk]
506+
507+
const lines = chunk.trim().split('}\n\n{')
508+
if (lines.length > 1) {
509+
_list = []
510+
for (let line of lines) {
511+
if (!line.trim().startsWith('{')) {
512+
line = '{' + line.trim()
513+
}
514+
if (!line.trim().endsWith('}')) {
515+
line = line.trim() + '}'
516+
}
517+
_list.push(line)
518+
}
519+
}
520+
521+
console.log(_list)
522+
523+
for (const str of _list) {
524+
let data
525+
try {
526+
data = JSON.parse(str)
527+
} catch (err) {
528+
console.error('JSON string:', str)
529+
throw err
530+
}
531+
532+
if (data.code && data.code !== 200) {
533+
ElMessage({
534+
message: data.msg,
535+
type: 'error',
536+
showClose: true,
537+
})
538+
return
539+
}
540+
541+
switch (data.type) {
542+
case 'info':
543+
console.log(data.msg)
544+
break
545+
case 'error':
546+
throw Error(data.content)
547+
case 'predict-result':
548+
predict_answer += data.content
549+
currentChat.value.records[_index].predict = predict_answer
550+
break
551+
case 'predict_finish':
552+
isPredictTyping.value = false
553+
break
554+
}
555+
await nextTick()
556+
}
440557
}
441-
currentRecord.error = currentRecord.error + 'Error:' + error
558+
} catch (error) {
442559
console.error('Error:', error)
443-
isAnalysisTyping.value = false
560+
ElMessage({
561+
message: error + '',
562+
type: 'error',
563+
showClose: true,
564+
})
565+
isPredictTyping.value = false
444566
}
445567
}
446568
@@ -522,4 +644,11 @@ const handleCtrlEnter = (e: KeyboardEvent) => {
522644
min-width: 0;
523645
}
524646
}
647+
648+
.analysis-container {
649+
color: var(--ed-text-color-primary);
650+
font-size: 12px;
651+
line-height: 1.7692307692;
652+
padding: 16px 22px;
653+
}
525654
</style>

0 commit comments

Comments
 (0)