This commit is contained in:
2025-09-08 16:37:49 +08:00
parent d4a4421d72
commit ae66e043f8

View File

@@ -0,0 +1,211 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
import { endsWith, startsWith } from 'lodash-es'
import { useI18n } from 'vue-i18n'
import { chatApi, ChatInfo } from '@/api/chat.ts'
const props = withDefaults(
defineProps<{
recordId?: number
currentChat?: ChatInfo
questions?: string
firstChat?: boolean
disabled?: boolean
}>(),
{
recordId: undefined,
currentChat: () => new ChatInfo(),
questions: '[]',
firstChat: false,
disabled: false,
}
)
const emits = defineEmits(['clickQuestion', 'update:currentChat', 'stop'])
const loading = ref(false)
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const computedQuestions = computed<string>(() => {
if (
props.questions &&
props.questions.length > 0 &&
startsWith(props.questions.trim(), '[') &&
endsWith(props.questions.trim(), ']')
) {
return JSON.parse(props.questions)
}
return []
})
const { t } = useI18n()
function clickQuestion(question: string): void {
if (!props.disabled) {
emits('clickQuestion', question)
}
}
const stopFlag = ref(false)
async function getRecommendQuestions() {
stopFlag.value = false
loading.value = true
try {
const controller: AbortController = new AbortController()
const response = await chatApi.recommendQuestions(props.recordId, controller)
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let tempResult = ''
while (true) {
if (stopFlag.value) {
controller.abort()
loading.value = false
break
}
const { done, value } = await reader.read()
if (done) {
break
}
let chunk = decoder.decode(value, { stream: true })
tempResult += chunk
const split = tempResult.match(/data:.*}\n\n/g)
if (split) {
chunk = split.join('')
tempResult = tempResult.replace(chunk, '')
} else {
continue
}
if (chunk && chunk.startsWith('data:{')) {
if (split) {
for (const str of split) {
let data
try {
data = JSON.parse(str.replace('data:{', '{'))
} 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 'recommended_question':
if (
data.content &&
data.content.length > 0 &&
startsWith(data.content.trim(), '[') &&
endsWith(data.content.trim(), ']')
) {
if (_currentChat.value?.records) {
for (let record of _currentChat.value.records) {
if (record.id === props.recordId) {
record.recommended_question = data.content
await nextTick()
}
}
}
}
}
}
}
}
}
} finally {
loading.value = false
}
}
function stop() {
stopFlag.value = true
loading.value = false
emits('stop')
}
onBeforeUnmount(() => {
stop()
})
defineExpose({ getRecommendQuestions, id: () => props.recordId, stop })
</script>
<template>
<div v-if="computedQuestions.length > 0 || loading" class="recommend-questions">
<div v-if="firstChat" style="margin-bottom: 8px">{{ t('qa.guess_u_ask') }}</div>
<div v-else class="continue-ask">{{ t('qa.continue_to_ask') }}</div>
<div v-if="loading">
<el-button style="min-width: unset" type="primary" link loading />
</div>
<div v-else class="question-grid">
<div
v-for="(question, index) in computedQuestions"
:key="index"
class="question"
:class="{ disabled: disabled }"
@click="clickQuestion(question)"
>
{{ question }}
</div>
</div>
</div>
</template>
<style scoped lang="less">
.recommend-questions {
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, calc(50% - 6px));
}
.question {
font-weight: 400;
cursor: pointer;
background: rgba(245, 246, 247, 1);
min-height: 32px;
border-radius: 6px;
padding: 5px 12px;
line-height: 22px;
&:hover {
background: rgba(31, 35, 41, 0.1);
}
&.disabled {
cursor: not-allowed;
background: rgba(245, 246, 247, 1);
}
}
}
</style>