feat: chat view

This commit is contained in:
ulleo
2025-06-06 16:43:35 +08:00
parent d2b7b0cd2e
commit 787d39200a
10 changed files with 166 additions and 93 deletions
+3 -2
View File
@@ -135,7 +135,7 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
# execute sql
result = llm_service.execute_sql(sql=sql)
llm_service.save_sql_data(session=session, data_obj=result)
yield json.dumps({'content': result, 'type': 'sql-data'}, ensure_ascii=False) + '\n\n'
yield json.dumps({'content': json.dumps(result, ensure_ascii=False), 'type': 'sql-data'}) + '\n\n'
# generate chart
chart_res = llm_service.generate_chart(session=session)
@@ -149,7 +149,8 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
print(full_chart_text)
chart = llm_service.check_save_chart(session=session, res=full_chart_text)
print(chart)
yield json.dumps({'content': chart, 'type': 'chart'}, ensure_ascii=False) + '\n\n'
yield json.dumps({'content': json.dumps(chart, ensure_ascii=False), 'type': 'chart'},
ensure_ascii=False) + '\n\n'
llm_service.finish(session=session)
yield json.dumps({'type': 'finish'})
+7 -11
View File
@@ -1,8 +1,9 @@
from sqlmodel import SQLModel, Field
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity, Boolean
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
from typing import List, Optional, Any
from sqlalchemy import Column, Text, BigInteger, DateTime, Integer, Identity, Boolean
from sqlmodel import SQLModel, Field
from apps.template.generate_chart.generator import get_chart_template
from apps.template.generate_sql.generator import get_sql_template
@@ -70,19 +71,14 @@ class AiModelQuestion(BaseModel):
engine: str = ""
db_schema: str = ""
sql: str = ""
rule: str = """
请逐步推理后给出答案:
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
步骤1: [思考内容]
步骤2: [思考内容]
最终答案: [结果]
"""
rule: str = ""
def sql_sys_question(self):
return get_sql_template()['system'].format(engine=self.engine, schema=self.db_schema, question=self.question)
def sql_user_question(self):
return get_sql_template()['user'].format(engine=self.engine, schema=self.db_schema, question=self.question, rule=self.rule)
return get_sql_template()['user'].format(engine=self.engine, schema=self.db_schema, question=self.question,
rule=self.rule)
def chart_sys_question(self):
return get_chart_template()['system'].format(sql=self.sql, question=self.question)
+19 -1
View File
@@ -18,6 +18,14 @@ template:
- 如果问题是图表展示相关且与生成SQL查询无关时,请参考上一次回答的SQL来生成SQL
- 如果问题是图表展示相关,可参考的图表类型为表格(table)、条形图(bar)、折线图(line)或饼图(pie),返回的JSON:
{{"success":true,"sql":"生成的SQL语句","chart-type":"选择的图表类型(table/bar/line/pie)"}}
### 请遵守以下输出要求:
请逐步推理后给出答案
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
步骤1: [思考内容]
步骤2: [思考内容]
……
最终答案: [结果]
user: |
### 表结构:
{schema}
@@ -26,6 +34,7 @@ template:
{question}
### 其他规则:
请使用英语(English)回答
{rule}
chart:
system: |
@@ -55,7 +64,15 @@ template:
### 响应:
根据您的指示,这是我生成的与 问题 和 sql 匹配的 JSON:
```json
user: |
### 请遵守以下输出要求:
请逐步推理后给出答案
推理过程中不需要输出JSON,仅在最终结果内输出符合要求的JSON
步骤1: [思考内容]
步骤2: [思考内容]
……
最终答案: [结果]
user: |
### SQL:
{sql}
@@ -63,4 +80,5 @@ template:
{question}
### 其他规则:
请使用英语(English)回答
{rule}
+1
View File
@@ -12,6 +12,7 @@
"@npkg/tinymce-plugins": "^0.0.7",
"@tinymce/tinymce-vue": "^5.1.0",
"dayjs": "^1.11.13",
"element-plus": "^2.9.11",
"lodash": "^4.17.21",
"snowflake-id": "^1.1.0",
"tinymce": "^5.8.2",
+9 -6
View File
@@ -10,7 +10,7 @@ const props = defineProps<{
}>()
const settings = ref<{
type: "chart" | "sql" | "data"
type: "chart" | "sql"
showAnswer: boolean
}>({
type: "chart",
@@ -74,11 +74,11 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
<div v-if="message">
<div>
<div v-if="message.isTyping">Thinking ...</div>
<div v-if="chartObject.title">{{ chartObject.title }}</div>
<div v-if="chartObject.title && !message.isTyping">{{ chartObject.title }}</div>
<el-tabs v-model="settings.type" class="demo-tabs" @tab-click="handleClick" tab-position="top">
<el-tab-pane label="Chart" name="chart">
<template #label>
<el-select v-model="chartType" style="width: 80px">
<el-select v-model="chartType" style="width: 80px" :disabled="settings.type!== 'chart'">
<el-option value="table">table</el-option>
<el-option value="bar">bar</el-option>
<el-option value="line">line</el-option>
@@ -87,7 +87,6 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
</template>
</el-tab-pane>
<el-tab-pane label="SQL" name="sql"></el-tab-pane>
<el-tab-pane label="Data" name="data"></el-tab-pane>
</el-tabs>
</div>
<template v-if="message.record">
@@ -95,7 +94,7 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
<el-collapse-item name="1">
<template #title>
Inference process
<el-icon v-if="props.message.isTyping">
<el-icon v-if="props.message?.isTyping">
<Loading/>
</el-icon>
</template>
@@ -112,7 +111,7 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
</div>
</el-collapse-item>
</el-collapse>
<div>
<div class="answer-content">
<template v-if="settings.type === 'sql'">
<div>
<div v-if="message.record.sql">
@@ -149,4 +148,8 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
display: flex;
justify-content: flex-end;
}
.answer-content {
padding: 12px;
}
</style>
+8 -2
View File
@@ -12,7 +12,9 @@ defineProps<{
<div class="chat-block-outer">
<div class="chat-block">
<slot>
<div v-html="msg?.content"></div>
<div class="chat-block-content">
<div v-html="msg?.content"></div>
</div>
</slot>
</div>
<div class="chat-block-footer">
@@ -26,9 +28,13 @@ defineProps<{
.chat-block {
border-radius: 2px;
background-color: white;
padding: 12px;
word-wrap: break-word;
white-space: pre-wrap;
.chat-block-content {
padding: 12px;
}
}
.chat-block-footer {
+3 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import {MoreFilled, EditPen, Delete} from '@element-plus/icons-vue'
import {Delete, EditPen, MoreFilled} from '@element-plus/icons-vue'
import {type Chat, chatApi} from "@/api/chat.ts";
import {computed} from "vue";
@@ -105,10 +105,11 @@ function handleCommand(command: string | number | object, chat: Chat) {
}
}
</script>
<template>
<el-scrollbar>
<el-scrollbar ref="chatListRef">
<div class="chat-list-inner">
<template v-for="chat in chatList" :key="chat.id">
<div class="chat-list-item" :class="{'active': currentChatId === chat.id}" @click="onClickHistory(chat)">
+15 -2
View File
@@ -3,6 +3,7 @@ import ChatBlock from './ChatBlock.vue'
import WelcomeBlock from './WelcomeBlock.vue'
import {ChatInfo, type ChatMessage} from "@/api/chat.ts";
import {computed} from "vue";
import {UserFilled} from "@element-plus/icons-vue"
const props = withDefaults(defineProps<{
msg: ChatMessage
@@ -27,8 +28,12 @@ const _datasource = computed({
<template>
<div class="chat-row" :class="{'right-to-left': msg.role === 'user'}">
<el-avatar shape="square" v-if="msg.role === 'assistant'">SQLBot</el-avatar>
<el-avatar shape="square" v-if="msg.role === 'user'"/>
<el-avatar class="ai-avatar" shape="square" v-if="msg.role === 'assistant'">SQLBot</el-avatar>
<el-avatar class="user-avatar" shape="square" v-if="msg.role === 'user'">
<el-icon>
<UserFilled/>
</el-icon>
</el-avatar>
<ChatBlock v-if="!msg.isWelcome" :msg="msg" :class="{'row-full': msg.role === 'assistant'}">
<slot></slot>
<template #footer>
@@ -58,5 +63,13 @@ const _datasource = computed({
}
}
.ai-avatar {
background: var(--el-color-primary);
}
.user-avatar {
background: var(--ed-color-primary)
}
</style>
+32 -26
View File
@@ -63,37 +63,43 @@ onMounted(() => {
<template>
<ChatBlock>
<div>你好,我是SQLBot,很高兴为你服务</div>
<div class="sub">我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~</div>
<template v-if="editable">
<template v-if="dsList.length>0">
<div class="ds-select-row">
<div>选择数据源</div>
<el-button @click="showDs" link type="primary">查看更多</el-button>
</div>
<div class="ds-row-container">
<template v-for="(item, _index) in dsList" :key="_index">
<DatasourceItemCard :ds="item" @click="selectDs(item)" v-if="_index<3 || item?.id===modelValue"
class="ds-card" :class="[item?.id===modelValue? 'ds-card-selected': '']"/>
</template>
<div class="welcome-content">
<div>你好,我是SQLBot,很高兴为你服务</div>
<div class="sub">我可以帮忙查询数据、生成图表、检测数据异常、预测数据等,请选择一个数据源,开启智能问数吧~</div>
<template v-if="editable">
<template v-if="dsList.length>0">
<div class="ds-select-row">
<div>选择数据源</div>
<el-button @click="showDs" link type="primary">查看更多</el-button>
</div>
<div class="ds-row-container">
<template v-for="(item, _index) in dsList" :key="_index">
<DatasourceItemCard :ds="item" @click="selectDs(item)" v-if="_index<3 || item?.id===modelValue"
class="ds-card" :class="[item?.id===modelValue? 'ds-card-selected': '']"/>
</template>
</div>
</template>
<div v-else>
数据源为空,请新建后再开启智能问数!
</div>
</template>
<div v-else>
数据源为空,请新建后再开启智能问数!
</div>
</template>
<template v-else>
<div class="ds-select-row">
<div>已选择数据源</div>
</div>
<div class="ds-row-container">
<DatasourceItemCard :ds="ds"/>
</div>
</template>
<template v-else>
<div class="ds-select-row">
<div>已选择数据源</div>
</div>
<div class="ds-row-container">
<DatasourceItemCard :ds="ds"/>
</div>
</template>
</div>
</ChatBlock>
</template>
<style scoped lang="less">
.welcome-content {
padding: 12px;
}
.sub {
color: grey;
font-size: 0.8em;
@@ -118,7 +124,7 @@ onMounted(() => {
}
.ds-card-selected {
border-color: var(--ed-color-primary-light-5);
box-shadow: 0 1px 3px var(--ed-color-primary-light-5);
}
+69 -41
View File
@@ -24,7 +24,7 @@
</el-aside>
<el-container :loading="loading">
<el-main class="chat-record-list">
<el-scrollbar>
<el-scrollbar ref="chatListRef">
<template v-for="(message, _index) in computedMessages" :key="_index">
<ChatRow
:current-chat="currentChat"
@@ -46,7 +46,7 @@
<template
v-if="currentChat.datasource && currentChat.datasource_name"
>
使用数据源:{{ currentChat.datasource_name }}
Datasource:{{ currentChat.datasource_name }}
</template>
</div>
<div class="input-wrapper">
@@ -81,20 +81,24 @@
<script setup lang="ts">
import {computed, nextTick, onMounted, ref} from "vue";
import {Plus, Position} from "@element-plus/icons-vue";
import {
Chat,
chatApi,
ChatInfo,
type ChatMessage,
ChatRecord,
questionApi,
} from "@/api/chat";
import {Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi,} from "@/api/chat";
import ChatList from "./ChatList.vue";
import ChatRow from "./ChatRow.vue";
import ChatAnswer from "./ChatAnswer.vue";
const inputMessage = ref("");
const chatListRef = ref()
function scrollToBottom() {
nextTick(() => {
chatListRef.value?.scrollTo({
top: chatListRef.value.wrapRef.scrollHeight,
behavior: 'smooth'
})
})
}
const loading = ref<boolean>(false);
const chatList = ref<Array<ChatInfo>>([]);
@@ -163,6 +167,8 @@ function onClickHistory(chat: Chat) {
const info = chatApi.toChatInfo(res);
if (info) {
currentChat.value = info;
scrollToBottom()
}
})
.finally(() => {
@@ -264,39 +270,61 @@ const sendMessage = async () => {
}
const chunk = decoder.decode(value);
console.log(chunk);
const data = JSON.parse(chunk);
switch (data.type) {
case "info":
console.log(data.msg);
break;
case "error":
currentRecord.error = data.content;
isTyping.value = false;
break;
case "sql-result":
sql_answer += data.content;
currentChat.value.records[currentChat.value.records.length - 1].sql_answer = sql_answer;
break;
case "sql":
currentChat.value.records[currentChat.value.records.length - 1].sql = data.content;
break;
case "sql-data":
currentChat.value.records[currentChat.value.records.length - 1].data = data.content;
break;
case "chart-result":
chart_answer += data.content;
currentChat.value.records[currentChat.value.records.length - 1].chart_answer = chart_answer;
break;
case "chart":
currentChat.value.records[currentChat.value.records.length - 1].chart = data.content;
break;
case "finish":
isTyping.value = false;
break;
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
await nextTick()
console.log(_list);
for (const str of _list) {
const data = JSON.parse(str);
switch (data.type) {
case "info":
console.log(data.msg);
break;
case "error":
currentRecord.error = data.content;
isTyping.value = false;
break;
case "sql-result":
sql_answer += data.content;
currentChat.value.records[currentChat.value.records.length - 1].sql_answer = sql_answer;
break;
case "sql":
currentChat.value.records[currentChat.value.records.length - 1].sql = data.content;
break;
case "sql-data":
currentChat.value.records[currentChat.value.records.length - 1].data = data.content;
break;
case "chart-result":
chart_answer += data.content;
currentChat.value.records[currentChat.value.records.length - 1].chart_answer = chart_answer;
break;
case "chart":
currentChat.value.records[currentChat.value.records.length - 1].chart = data.content;
break;
case "finish":
isTyping.value = false;
break;
}
await nextTick()
}
}
} catch (error) {
if (!currentRecord.error) {