mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat:Support quick questioning, no need to input questions, can also quickly ask common questions (#505)
This commit is contained in:
@@ -10,7 +10,7 @@ from sqlalchemy import and_, select
|
||||
|
||||
from apps.chat.curd.chat import list_chats, get_chat_with_records, create_chat, rename_chat, \
|
||||
delete_chat, get_chat_chart_data, get_chat_predict_data, get_chat_with_records_with_data, get_chat_record_by_id, \
|
||||
format_json_data, format_json_list_data, get_chart_config
|
||||
format_json_data, format_json_list_data, get_chart_config, list_recent_questions
|
||||
from apps.chat.models.chat_model import CreateChat, ChatRecord, RenameChat, ChatQuestion, AxisObj
|
||||
from apps.chat.task.llm import LLMService
|
||||
from common.core.deps import CurrentAssistant, SessionDep, CurrentUser, Trans
|
||||
@@ -132,6 +132,10 @@ async def recommend_questions(session: SessionDep, current_user: CurrentUser, ch
|
||||
|
||||
return StreamingResponse(llm_service.await_result(), media_type="text/event-stream")
|
||||
|
||||
@router.get("/recent_questions/{datasource_id}")
|
||||
async def recommend_questions(session: SessionDep, current_user: CurrentUser, datasource_id: int):
|
||||
return list_recent_questions(session=session, current_user=current_user, datasource_id=datasource_id)
|
||||
|
||||
|
||||
@router.post("/question")
|
||||
async def stream_sql(session: SessionDep, current_user: CurrentUser, request_question: ChatQuestion,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
from typing import List
|
||||
from sqlalchemy import desc, func
|
||||
|
||||
import orjson
|
||||
import sqlparse
|
||||
@@ -35,6 +36,21 @@ def list_chats(session: SessionDep, current_user: CurrentUser) -> List[Chat]:
|
||||
return chart_list
|
||||
|
||||
|
||||
def list_recent_questions(session: SessionDep, current_user: CurrentUser, datasource_id: int) -> List[str]:
|
||||
chat_records = (
|
||||
session.query(ChatRecord.question)
|
||||
.filter(
|
||||
ChatRecord.datasource == datasource_id,
|
||||
ChatRecord.question.isnot(None)
|
||||
)
|
||||
.group_by(ChatRecord.question)
|
||||
.order_by(desc(func.max(ChatRecord.create_time)))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
return [record[0] for record in chat_records] if chat_records else []
|
||||
|
||||
|
||||
def rename_chat(session: SessionDep, rename_object: RenameChat) -> str:
|
||||
chat = session.get(Chat, rename_object.id)
|
||||
if not chat:
|
||||
|
||||
@@ -331,6 +331,9 @@ export const chatApi = {
|
||||
recommendQuestions: (record_id: number | undefined, controller?: AbortController) => {
|
||||
return request.fetchStream(`/chat/recommend_questions/${record_id}`, {}, controller)
|
||||
},
|
||||
recentQuestions: (datasource_id?: number): Promise<any> => {
|
||||
return request.get(`/chat/recent_questions/${datasource_id}`)
|
||||
},
|
||||
checkLLMModel: () => request.get('/system/aimodel/default', { requestOptions: { silent: true } }),
|
||||
export2Excel: (record_id: number | undefined) =>
|
||||
request.get(`/chat/record/${record_id}/excel/export`, {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764210062188" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7442" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M271.973 508.68l-0.467 0.516c-15.161 17.134-6.178 44.443 16.396 49.075l162.817 33.411-147.137 354.746c-12.63 30.449 26.067 55.938 49.027 32.294l410.916-423.155 0.486-0.511c15.79-16.999 6.974-44.925-15.926-49.613l-152.962-31.315L718.788 109.69c10.37-30.56-28.32-53.605-50.226-29.915L271.972 508.68z m345.075-285.15l-89.404 263.48-0.215 0.657c-5.298 16.85 5.123 34.684 22.518 38.245l132.675 27.162L418.71 824.845l101.086-243.71 0.25-0.621c6.7-17.199-3.668-36.397-21.844-40.127l-146.367-30.033L617.048 223.53z" p-id="7443"></path></svg>
|
||||
|
After Width: | Height: | Size: 868 B |
@@ -184,6 +184,9 @@
|
||||
"chart_selected": "Selected {0}"
|
||||
},
|
||||
"qa": {
|
||||
"recently": "recently",
|
||||
"recommend": "recommend",
|
||||
"quick_question": "quick question",
|
||||
"new_chat": "New Chat",
|
||||
"start_sqlbot": "New Chat",
|
||||
"title": "Data Q&A",
|
||||
|
||||
@@ -184,6 +184,9 @@
|
||||
"chart_selected": "{0}개 선택됨"
|
||||
},
|
||||
"qa": {
|
||||
"recently": "최근",
|
||||
"recommend": "추천",
|
||||
"quick_question": "빠른 질문",
|
||||
"new_chat": "새 대화 생성",
|
||||
"start_sqlbot": "데이터 조회 시작",
|
||||
"title": "스마트 데이터 조회",
|
||||
|
||||
@@ -184,6 +184,9 @@
|
||||
"chart_selected": "已选{0}"
|
||||
},
|
||||
"qa": {
|
||||
"recently": "最近",
|
||||
"recommend": "推荐",
|
||||
"quick_question": "快捷提问",
|
||||
"new_chat": "新建对话",
|
||||
"start_sqlbot": "开启问数",
|
||||
"title": "智能问数",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import icon_quick_question from '@/assets/svg/icon_quick_question.svg'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import RecommendQuestion from '@/views/chat/RecommendQuestion.vue'
|
||||
import { ChatInfo } from '@/api/chat.ts'
|
||||
import RecentQuestion from '@/views/chat/RecentQuestion.vue'
|
||||
const visible = ref(false)
|
||||
const activeName = ref('recommend')
|
||||
const recommendQuestionRef = ref()
|
||||
|
||||
const getRecommendQuestions = () => {
|
||||
recommendQuestionRef.value.getRecommendQuestions()
|
||||
}
|
||||
const quickAsk = (question: string) => {
|
||||
emits('quickAsk', question)
|
||||
}
|
||||
|
||||
const onChatStop = () => {
|
||||
emits('stop')
|
||||
}
|
||||
|
||||
const loadingOver = () => {
|
||||
emits('loadingOver')
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const emits = defineEmits(['quickAsk', 'loadingOver', 'stop'])
|
||||
defineExpose({ getRecommendQuestions, id: () => props.recordId, stop })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
recordId?: number
|
||||
datasourceId?: number
|
||||
currentChat?: ChatInfo
|
||||
questions?: string
|
||||
firstChat?: boolean
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
recordId: undefined,
|
||||
datasourceId: undefined,
|
||||
currentChat: () => new ChatInfo(),
|
||||
questions: '[]',
|
||||
firstChat: false,
|
||||
disabled: false,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover
|
||||
:title="$t('qa.quick_question')"
|
||||
:visible="visible"
|
||||
popper-class="quick_question_popover"
|
||||
placement="top-start"
|
||||
:width="320"
|
||||
>
|
||||
<el-icon class="close_icon"><Close @click="visible = false" /></el-icon>
|
||||
<el-tabs v-model="activeName" class="quick_question_tab">
|
||||
<el-tab-pane :label="$t('qa.recommend')" name="recommend">
|
||||
<RecommendQuestion
|
||||
ref="recommendQuestionRef"
|
||||
:current-chat="currentChat"
|
||||
:record-id="recordId"
|
||||
:questions="questions"
|
||||
:disabled="disabled"
|
||||
:first-chat="firstChat"
|
||||
position="input"
|
||||
@click-question="quickAsk"
|
||||
@stop="onChatStop"
|
||||
@loading-over="loadingOver"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="datasourceId" :label="$t('qa.recently')" name="recently">
|
||||
<RecentQuestion v-if="visible" :datasource-id="datasourceId" @click-question="quickAsk">
|
||||
</RecentQuestion>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #reference>
|
||||
<el-button plain size="small" @click="visible = true">
|
||||
<el-icon size="16" class="el-icon--left">
|
||||
<icon_quick_question />
|
||||
</el-icon>
|
||||
{{ $t('qa.quick_question') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.quick_question_popover {
|
||||
.quick_question_tab {
|
||||
height: 230px;
|
||||
}
|
||||
.ed-tabs__content {
|
||||
overflow: auto;
|
||||
}
|
||||
.ed-popover__title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.close_icon {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
.ed-tabs__item {
|
||||
font-size: 14px;
|
||||
height: 38px;
|
||||
}
|
||||
.ed-tabs__active-bar {
|
||||
height: 2px;
|
||||
}
|
||||
.ed-tabs__nav-wrap:after {
|
||||
height: 0;
|
||||
}
|
||||
.ed-tabs__content {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { chatApi } from '@/api/chat.ts'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
datasourceId?: number
|
||||
}>(),
|
||||
{
|
||||
datasourceId: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
const emits = defineEmits(['clickQuestion'])
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const computedQuestions = ref([])
|
||||
|
||||
function clickQuestion(question: string): void {
|
||||
emits('clickQuestion', question)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getRecentQuestions()
|
||||
})
|
||||
async function getRecentQuestions() {
|
||||
chatApi.recentQuestions(props.datasourceId).then((res) => {
|
||||
computedQuestions.value = res
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="computedQuestions.length > 0 || loading" class="recent-questions">
|
||||
<div class="question-grid-input">
|
||||
<div
|
||||
v-for="(question, index) in computedQuestions"
|
||||
:key="index"
|
||||
class="question"
|
||||
@click="clickQuestion(question)"
|
||||
>
|
||||
{{ question }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.recent-questions {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,7 @@ const props = withDefaults(
|
||||
questions?: string
|
||||
firstChat?: boolean
|
||||
disabled?: boolean
|
||||
position?: string
|
||||
}>(),
|
||||
{
|
||||
recordId: undefined,
|
||||
@@ -18,6 +19,7 @@ const props = withDefaults(
|
||||
questions: '[]',
|
||||
firstChat: false,
|
||||
disabled: false,
|
||||
position: 'chat',
|
||||
}
|
||||
)
|
||||
|
||||
@@ -153,11 +155,24 @@ defineExpose({ getRecommendQuestions, id: () => props.recordId, stop })
|
||||
|
||||
<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>
|
||||
<template v-if="position === 'chat'">
|
||||
<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>
|
||||
</template>
|
||||
<div v-if="loading">
|
||||
<el-button style="min-width: unset" type="primary" link loading />
|
||||
</div>
|
||||
<div v-else-if="position === 'input'" 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 v-else class="question-grid">
|
||||
<div
|
||||
v-for="(question, index) in computedQuestions"
|
||||
@@ -186,6 +201,12 @@ defineExpose({ getRecommendQuestions, id: () => props.recordId, stop })
|
||||
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;
|
||||
|
||||
@@ -221,18 +221,18 @@
|
||||
:msg="message"
|
||||
:hide-avatar="message.first_chat"
|
||||
>
|
||||
<RecommendQuestion
|
||||
v-if="message.role === 'assistant' && message.first_chat"
|
||||
ref="recommendQuestionRef"
|
||||
:current-chat="currentChat"
|
||||
:record-id="message.record?.id"
|
||||
:questions="message.recommended_question"
|
||||
:disabled="isTyping"
|
||||
:first-chat="message.first_chat"
|
||||
@click-question="quickAsk"
|
||||
@stop="onChatStop"
|
||||
@loading-over="loadingOver"
|
||||
/>
|
||||
<!-- <RecommendQuestion-->
|
||||
<!-- v-if="message.role === 'assistant' && message.first_chat"-->
|
||||
<!-- ref="recommendQuestionRef"-->
|
||||
<!-- :current-chat="currentChat"-->
|
||||
<!-- :record-id="message.record?.id"-->
|
||||
<!-- :questions="message.recommended_question"-->
|
||||
<!-- :disabled="isTyping"-->
|
||||
<!-- :first-chat="message.first_chat"-->
|
||||
<!-- @click-question="quickAsk"-->
|
||||
<!-- @stop="onChatStop"-->
|
||||
<!-- @loading-over="loadingOver"-->
|
||||
<!-- />-->
|
||||
<UserChat v-if="message.role === 'user'" :message="message" />
|
||||
<template v-if="message.role === 'assistant' && !message.first_chat">
|
||||
<ChartAnswer
|
||||
@@ -396,6 +396,20 @@
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="computedMessages.length > 0" 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"
|
||||
@stop="onChatStop"
|
||||
@loading-over="loadingOver"
|
||||
></quick-question>
|
||||
</div>
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model="inputMessage"
|
||||
@@ -462,6 +476,7 @@ import { useUserStore } from '@/stores/user'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { isMobile } from '@/utils/utils'
|
||||
import router from '@/router'
|
||||
import QuickQuestion from '@/views/chat/QuickQuestion.vue'
|
||||
const userStore = useUserStore()
|
||||
const props = defineProps<{
|
||||
startChatDsId?: number
|
||||
@@ -703,14 +718,17 @@ function onChatCreatedQuick(chat: ChatInfo) {
|
||||
onChatCreated(chat)
|
||||
}
|
||||
|
||||
const recommendQuestionRef = ref()
|
||||
const quickQuestionRef = ref()
|
||||
|
||||
function onChatCreated(chat: ChatInfo) {
|
||||
if (chat.records.length === 1 && !chat.records[0].recommended_question) {
|
||||
getRecommendQuestions(chat.records[0].id)
|
||||
nextTick(() => {
|
||||
quickQuestionRef.value.getRecommendQuestions()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const recommendQuestionRef = ref()
|
||||
|
||||
function getRecommendQuestions(id?: number) {
|
||||
nextTick(() => {
|
||||
if (recommendQuestionRef.value) {
|
||||
@@ -1178,6 +1196,31 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.quick_question {
|
||||
width: calc(100% - 2px);
|
||||
position: absolute;
|
||||
margin-left: 1px;
|
||||
margin-top: 1px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
line-height: 22px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
border-top-right-radius: 16px;
|
||||
border-top-left-radius: 16px;
|
||||
color: rgba(100, 106, 115, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
color: rgba(31, 35, 41, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.input-area {
|
||||
border-color: #d9dcdf;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user