Add File
This commit is contained in:
211
frontend/src/views/chat/RecommendQuestion.vue
Normal file
211
frontend/src/views/chat/RecommendQuestion.vue
Normal 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>
|
||||
Reference in New Issue
Block a user