Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions backend/apps/chat/curd/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,16 @@ def list_chats(session: SessionDep, current_user: CurrentUser) -> List[Chat]:

def list_recent_questions(session: SessionDep, current_user: CurrentUser, datasource_id: int) -> List[str]:
chat_records = (
session.query(ChatRecord.question)
session.query(
ChatRecord.question,
func.count(ChatRecord.question).label('question_count')
)
.filter(
ChatRecord.datasource == datasource_id,
ChatRecord.question.isnot(None)
)
.group_by(ChatRecord.question)
.order_by(desc(func.max(ChatRecord.create_time)))
.order_by(desc('question_count'), desc(func.max(ChatRecord.create_time)))
.limit(10)
.all()
)
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/views/chat/QuickQuestion.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import icon_quick_question from '@/assets/svg/icon_quick_question.svg'
import icon_close from '@/assets/svg/operate/ope-close.svg'
import icon_replace_outlined from '@/assets/svg/icon_replace_outlined.svg'
import RecommendQuestion from '@/views/chat/RecommendQuestion.vue'
import { ChatInfo } from '@/api/chat.ts'
import RecentQuestion from '@/views/chat/RecentQuestion.vue'
import RecommendQuestionQuick from '@/views/chat/RecommendQuestionQuick.vue'
const activeName = ref('recommend')
const recommendQuestionRef = ref()
const recentQuestionRef = ref()
Expand All @@ -14,6 +14,7 @@ const getRecommendQuestions = () => {
recommendQuestionRef.value.getRecommendQuestions(10)
}

const questions = '[]'
const retrieveQuestions = () => {
getRecommendQuestions()
recentQuestionRef.value.getRecentQuestions()
Expand All @@ -37,6 +38,10 @@ const loadingOver = () => {
emits('loadingOver')
}

onMounted(() => {
getRecommendQuestions()
})

const emits = defineEmits(['quickAsk', 'loadingOver', 'stop'])
defineExpose({ getRecommendQuestions, id: () => props.recordId, stop })

Expand All @@ -45,15 +50,13 @@ const props = withDefaults(
recordId?: number
datasourceId?: number
currentChat?: ChatInfo
questions?: string
firstChat?: boolean
disabled?: boolean
}>(),
{
recordId: undefined,
datasourceId: undefined,
currentChat: () => new ChatInfo(),
questions: '[]',
firstChat: false,
disabled: false,
}
Expand Down Expand Up @@ -83,7 +86,7 @@ const props = withDefaults(
</el-tooltip>
<el-tabs v-model="activeName" class="quick_question_tab">
<el-tab-pane :label="$t('qa.recommend')" name="recommend">
<RecommendQuestion
<RecommendQuestionQuick
ref="recommendQuestionRef"
:current-chat="currentChat"
:record-id="recordId"
Expand Down
13 changes: 11 additions & 2 deletions frontend/src/views/chat/RecommendQuestion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,23 @@ const _currentChat = computed({
},
})

const computedQuestions = computed<string>(() => {
const computedQuestions = computed<string[]>(() => {
if (
props.questions &&
props.questions.length > 0 &&
startsWith(props.questions.trim(), '[') &&
endsWith(props.questions.trim(), ']')
) {
return JSON.parse(props.questions)
try {
const parsedQuestions = JSON.parse(props.questions)
if (Array.isArray(parsedQuestions)) {
return parsedQuestions.length > 4 ? parsedQuestions.slice(0, 4) : parsedQuestions
}
return []
} catch (error) {
console.error('Failed to parse questions:', error)
return []
}
}
return []
})
Expand Down
209 changes: 209 additions & 0 deletions frontend/src/views/chat/RecommendQuestionQuick.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
import { endsWith, startsWith } from 'lodash-es'
import { chatApi } from '@/api/chat.ts'

const props = withDefaults(
defineProps<{
recordId?: number
disabled?: boolean
}>(),
{
recordId: undefined,
disabled: false,
}
)

const emits = defineEmits(['clickQuestion', 'stop', 'loadingOver'])

const loading = ref(false)

const questions = ref('[]')

const computedQuestions = computed<string>(() => {
if (
questions.value &&
questions.value.length > 0 &&
startsWith(questions.value.trim(), '[') &&
endsWith(questions.value.trim(), ']')
) {
return JSON.parse(questions.value)
}
return []
})

function clickQuestion(question: string): void {
if (!props.disabled) {
emits('clickQuestion', question)
}
}

const stopFlag = ref(false)

async function getRecommendQuestions(articles_number: number) {
stopFlag.value = false
loading.value = true
try {
const controller: AbortController = new AbortController()
const params = articles_number ? '?articles_number=' + articles_number : ''
const response = await chatApi.recommendQuestions(props.recordId, controller, params)
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(), ']')
) {
questions.value = data.content
await nextTick()
}
}
}
}
}
}
} finally {
loading.value = false
emits('loadingOver')
}
}

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="loading">
<el-button style="min-width: unset" type="primary" link loading />
</div>
<div v-else class="question-grid-input">
<div
v-for="(question, index) in computedQuestions"
:key="index"
class="question"
:class="{ disabled: disabled }"
@click="clickQuestion(question)"
>
{{ question }}
</div>
</div>
</div>
<div v-else class="recommend-questions-error">
{{ $t('qa.retrieve_error') }}
</div>
</template>

<style scoped lang="less">
.recommend-questions {
width: 100%;
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-input {
display: grid;
grid-gap: 12px;
grid-template-columns: repeat(1, calc(100% - 6px));
}

.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);
}
}
}

.recommend-questions-error {
font-size: 12px;
font-weight: 500;
color: rgba(100, 106, 115, 1);
margin-top: 70px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
</style>
6 changes: 6 additions & 0 deletions frontend/src/views/chat/answer/ChartAnswer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ const sendMessage = async () => {
case 'chart':
_currentChat.value.records[index.value].chart = data.content
break
case 'datasource':
if (!_currentChat.value.datasource) {
_currentChat.value.datasource = data.id
}
_currentChat.value.records[index.value].chart = data.content
break
case 'finish':
emits('finish', currentRecord.id)
break
Expand Down
9 changes: 3 additions & 6 deletions frontend/src/views/chat/index.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template>
<el-icon
v-if="assistantStore.assistant && !assistantStore.pageEmbedded && assistantStore.type != 4"
class="show-history_icon"
:class="{ 'embedded-history-hidden': embeddedHistoryHidden }"
v-if="assistantStore.assistant && !assistantStore.pageEmbedded && assistantStore.type != 4"
size="20"
@click="showFloatPopover"
>
Expand Down Expand Up @@ -371,13 +371,12 @@
</span>
</template>
</div>
<div v-if="computedMessages.length > 0" class="quick_question">
<div v-if="computedMessages.length > 0 && currentChat.datasource" class="quick_question">
<quick-question
ref="quickQuestionRef"
:datasource-id="currentChat.datasource"
:current-chat="currentChat"
:record-id="computedMessages[0].record?.id"
:questions="computedMessages[0].recommended_question"
:disabled="isTyping"
:first-chat="true"
@quick-ask="quickAsk"
Expand Down Expand Up @@ -698,9 +697,7 @@ const quickQuestionRef = ref()

function onChatCreated(chat: ChatInfo) {
if (chat.records.length === 1 && !chat.records[0].recommended_question) {
nextTick(() => {
quickQuestionRef.value.getRecommendQuestions()
})
// do nothing
}
}

Expand Down