Merge remote-tracking branch 'origin/main'

# Conflicts:
#	frontend/src/views/dashboard/utils/canvasUtils.ts
This commit is contained in:
wangjiahao
2025-06-09 22:46:16 +08:00
22 changed files with 485 additions and 71 deletions
+1
View File
@@ -41,6 +41,7 @@ class OpenAILLM(BaseLLM):
model=self.config.model_name,
api_key=self.config.api_key,
base_url=self.config.api_base_url,
stream_usage=True,
**self.config.additional_params
)
+3
View File
@@ -118,6 +118,9 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
def run_task():
try:
# return id
yield json.dumps({'type': 'id', 'id': llm_service.get_record().id}) + '\n\n'
# generate sql
sql_res = llm_service.generate_sql(session=session)
full_sql_text = ''
+3
View File
@@ -105,6 +105,9 @@ class LLMService:
self.record = save_question(session=session, current_user=current_user, question=self.chat_question)
return self.record
def get_record(self):
return self.record
def generate_sql(self, session: SessionDep):
# append current question
self.sql_message.append(HumanMessage(self.chat_question.sql_user_question()))
+3 -3
View File
@@ -34,7 +34,7 @@ template:
{question}
### 其他规则:
请使用英语(English)回答
请使用英语(English)输出
{rule}
chart:
system: |
@@ -51,7 +51,7 @@ template:
{{"type":"line", "title": "标题", "axis": {{"x": {{"name":"x轴的中文名称","value": "x轴的SQL查询列(有别名用别名)"}}, "y": {{"name":"y轴的中文名称","value": "y轴的SQL查询列(有别名用别名)"}}}}
其中“x”和“y”必须从SQL查询列中提取。
- 如果需要饼图,则生成的 JSON 格式应为:
{{"type":"pie", "title": "标题", "column": [{{"name":"中文字段名1","value":"SQL查询列1(有别名用别名)"}},{{"name":"中文字段名2","value":"SQL查询列2(有别名用别名)"}}]}}
{{"type":"pie", "title": "标题", "columns": [{{"name":"中文字段名1","value":"SQL查询列1(有别名用别名)"}},{{"name":"中文字段名2","value":"SQL查询列2(有别名用别名)"}}]}}
其中“column”必须从SQL查询列中提取。
- 如果答案未知或者与生成JSON无关,则生成的 JSON 格式应为:
{{"type":"error", "reason": "抱歉,我无法回答您的问题。"}}
@@ -80,5 +80,5 @@ template:
{question}
### 其他规则:
请使用英语(English)回答
请使用英语(English)输出
{rule}
+3 -2
View File
@@ -9,14 +9,16 @@
"preview": "vite preview"
},
"dependencies": {
"@antv/g2": "^5.3.3",
"@npkg/tinymce-plugins": "^0.0.7",
"@tinymce/tinymce-vue": "^5.1.0",
"dayjs": "^1.11.13",
"element-plus": "^2.9.11",
"element-plus-secondary": "^1.0.0",
"lodash": "^4.17.21",
"snowflake-id": "^1.1.0",
"tinymce": "^5.8.2",
"vue": "^3.5.13",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.0",
"web-storage-cache": "^1.1.1"
},
@@ -28,7 +30,6 @@
"@vue/tsconfig": "^0.7.0",
"axios": "^1.8.4",
"crypto-js": "^4.2.0",
"element-plus-secondary": "^1.0.0",
"less": "^4.3.0",
"pinia": "^3.0.2",
"typescript": "~5.7.2",
+9 -1
View File
@@ -1,8 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ElConfigProvider } from 'element-plus-secondary'
import { getElementLocale } from '@/i18n'
const elLanguage = computed(() => {
return getElementLocale()
})
</script>
<template>
<router-view />
<el-config-provider :locale="elLanguage">
<router-view />
</el-config-provider>
</template>
<style scoped>
+31 -1
View File
@@ -43,7 +43,8 @@
<div v-else class="top-bar-title">
<span class="split"/>
<span>System manage</span>
<!-- <span>System manage</span> -->
<span>{{ t('common.system_manage') }}</span>
</div>
@@ -76,6 +77,21 @@
<el-dropdown-menu>
<el-dropdown-item @click="switchLayout">Switch Layout</el-dropdown-item>
<el-dropdown-item @click="logout">Logout</el-dropdown-item>
<el-dropdown @command="changeLanguage">
<div class="lang-switch">
Language
<el-icon>
<i class="el-icon-arrow-down"/>
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="en">English</el-dropdown-item>
<el-dropdown-item command="zh-CN">中文</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -153,7 +169,9 @@ import icon_user from '@/assets/svg/icon_user.svg'
import icon_ai from '@/assets/svg/icon_ai.svg'
import {ArrowLeftBold} from '@element-plus/icons-vue'
import {useCache} from '@/utils/useCache'
import { useI18n } from 'vue-i18n'
const { locale, t } = useI18n()
const {wsCache} = useCache()
const topLayout = ref(false)
const router = useRouter()
@@ -215,6 +233,10 @@ const switchLayout = () => {
topLayout.value = !topLayout.value
wsCache.set('sqlbot-topbar-layout', topLayout.value)
}
const changeLanguage = (lang: string) => {
locale.value = lang
}
onMounted(() => {
topLayout.value = wsCache.get('sqlbot-topbar-layout') || true
})
@@ -536,4 +558,12 @@ onMounted(() => {
}
}
}
.lang-switch {
cursor: pointer;
padding: 0 12px;
&:hover {
color: var(--el-color-primary);
}
}
</style>
+14
View File
@@ -0,0 +1,14 @@
{
"menu": {
"dashboard": "Dashboard",
"chat": "Chat",
"datasource": "Data Source",
"system": "System"
},
"common": {
"back": "Back",
"confirm": "Confirm",
"cancel": "Cancel",
"system_manage": "System Management"
}
}
+34
View File
@@ -0,0 +1,34 @@
import { createI18n } from 'vue-i18n'
import en from './en.json'
import zhCN from './zh-CN.json'
import elementEnLocale from 'element-plus/es/locale/lang/en'
import elementZhLocale from 'element-plus/es/locale/lang/zh-cn'
const getDefaultLocale = () => {
/* const savedLang = localStorage.getItem('lang')
return savedLang || 'zh-CN' */
return 'zh-CN'
}
const messages = {
en: {
...en,
el: elementEnLocale
},
'zh-CN': {
...zhCN,
el: elementZhLocale
}
}
export const i18n = createI18n({
legacy: false,
locale: getDefaultLocale(),
fallbackLocale: 'en',
globalInjection: true,
messages
})
export const getElementLocale = () => {
return i18n.global.locale.value === 'en' ? elementEnLocale : elementZhLocale
}
+14
View File
@@ -0,0 +1,14 @@
{
"menu": {
"dashboard": "仪表盘",
"chat": "聊天",
"datasource": "数据源",
"system": "系统"
},
"common": {
"back": "返回",
"confirm": "确认",
"cancel": "取消",
"system_manage": "系统管理"
}
}
+2
View File
@@ -3,10 +3,12 @@ import { createPinia } from 'pinia'
import './style.less'
import App from './App.vue'
import router from './router'
import { i18n } from './i18n'
// import 'element-plus/dist/index.css'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(i18n)
app.mount('#app')
+5 -4
View File
@@ -7,7 +7,8 @@ import chat from "@/views/chat/index.vue";
import ds from "@/views/ds/index.vue";
import DashboardEditor from "@/views/dashboard/editor/index.vue";
import Dashboard from "@/views/dashboard/index.vue";
// import setting from "@/views/setting/index.vue";
import Model from "@/views/system/model/index.vue";
import User from "@/views/system/user/index.vue";
import { watchRouter } from "./watch";
const router = createRouter({
history: createWebHashHistory(),
@@ -95,17 +96,17 @@ const router = createRouter({
{
path: "user",
name: "user",
component: () => import("@/views/system/user/index.vue"),
component: User,
meta: { title: "User Management", icon: "icon_user" },
},
{
path: "model",
name: "model",
component: () => import("@/views/system/model/index.vue"),
component: Model,
meta: { title: "AI Model Configuration", icon: "icon_ai" },
},
],
},
}
],
});
watchRouter(router);
+92 -60
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type {ChatMessage} from "@/api/chat.ts";
import {computed, ref} from "vue";
import type {TabsPaneContext} from 'element-plus'
import {computed, nextTick, ref} from "vue";
import type {TabsPaneContext} from 'element-plus-secondary'
import {Loading} from "@element-plus/icons-vue";
import Component from "./component/Component.vue"
const props = defineProps<{
message?: ChatMessage
@@ -53,9 +54,22 @@ const chartObject = computed<{
return {}
})
const currentChartType = ref<"table" | "bar" | "line" | "pie" | undefined>(undefined)
const xAxis = computed(() => {
if (chartObject.value?.axis?.x) {
return [chartObject.value.axis.x]
}
return []
})
const yAxis = computed(() => {
if (chartObject.value?.axis?.y) {
return [chartObject.value.axis.y]
}
return []
})
const chartType = computed<"table" | "bar" | "line" | "pie">({
const currentChartType = ref<"table" | "bar" | 'column' | "line" | "pie" | undefined>(undefined)
const chartType = computed<"table" | "bar" | 'column' | "line" | "pie">({
get() {
if (currentChartType.value) {
return currentChartType.value
@@ -67,19 +81,31 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
}
})
const chartRef = ref()
function onTypeChange(type: string) {
console.log(type)
nextTick(() => {
chartRef.value?.destroyChart()
chartRef.value?.renderChart()
})
}
</script>
<template>
<div v-if="message">
<div>
<div v-if="message.isTyping">Thinking ...</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-container v-if="message">
<el-header style="display: flex; align-items: center; flex-direction: row;">
<div style="flex:1" v-if="message.isTyping">Thinking ...</div>
<div style="flex:1" v-if="chartObject.title && !message.isTyping">{{ chartObject.title }}</div>
<el-tabs v-model="settings.type" class="type-tabs" @tab-click="handleClick" tab-position="top">
<el-tab-pane label="Chart" name="chart">
<template #label>
<el-select v-model="chartType" style="width: 80px" :disabled="settings.type!== 'chart'">
<el-select v-model="chartType" style="width: 80px" :disabled="settings.type!== 'chart'"
@change="onTypeChange">
<el-option value="table">table</el-option>
<el-option value="column">column</el-option>
<el-option value="bar">bar</el-option>
<el-option value="line">line</el-option>
<el-option value="pie">pie</el-option>
@@ -88,59 +114,61 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
</el-tab-pane>
<el-tab-pane label="SQL" name="sql"></el-tab-pane>
</el-tabs>
</div>
<template v-if="message.record">
<el-collapse expand-icon-position="left">
<el-collapse-item name="1">
<template #title>
Inference process
<el-icon v-if="props.message?.isTyping">
<Loading/>
</el-icon>
</el-header>
<el-container direction="vertical">
<template v-if="message.record">
<el-collapse expand-icon-position="left">
<el-collapse-item name="1">
<template #title>
Inference process
<el-icon v-if="props.message?.isTyping">
<Loading/>
</el-icon>
</template>
<div>
<template v-if="message.record.sql_answer">
<div>SQL Generation:</div>
<div v-if="message.record.sql_answer" v-html="renderSqlThinking"></div>
</template>
<template v-if="message.record.chart_answer">
<el-divider></el-divider>
<div>Chart Generation:</div>
<div v-html="renderChartThinking"></div>
</template>
</div>
</el-collapse-item>
</el-collapse>
<div class="answer-content">
<template v-if="settings.type === 'sql'">
<div>
<div v-if="message.record.sql">
{{ message.record.sql }}
</div>
</div>
</template>
<div>
<template v-if="message.record.sql_answer">
<div>SQL Generation:</div>
<div v-if="message.record.sql_answer" v-html="renderSqlThinking"></div>
</template>
<template v-if="message.record.chart_answer">
<el-divider></el-divider>
<div>Chart Generation:</div>
<div v-html="renderChartThinking"></div>
</template>
</div>
</el-collapse-item>
</el-collapse>
<div class="answer-content">
<template v-if="settings.type === 'sql'">
<div>
<div v-if="message.record.sql">
{{ message.record.sql }}
<template v-else-if="settings.type === 'chart'">
<div>
<div v-if="message.record.chart">
<Component
ref="chartRef"
v-if="message.record.id"
:id="message.record.id"
:type="chartType"
:columns="chartObject?.columns"
:x="xAxis"
:y="yAxis"
:data="dataObject.data"/>
</div>
</div>
</div>
</template>
<template v-else-if="settings.type === 'chart'">
<div>
<div v-if="message.record.chart">
{{ chartObject }}
<div v-if="message.record.error" style="color: red">
{{ message.record.error }}
</div>
</div>
<div v-if="message.record.error" style="color: red">
{{ message.record.error }}
</div>
</template>
<template v-else>
<div>
<div v-if="dataObject.fields">
{{ dataObject.fields }}
{{ dataObject.data }}
</div>
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</template>
</el-container>
<!--<el-footer></el-footer>-->
</el-container>
</template>
<style scoped lang="less">
@@ -152,4 +180,8 @@ const chartType = computed<"table" | "bar" | "line" | "pie">({
.answer-content {
padding: 12px;
}
.type-tabs {
margin-right: 24px;
}
</style>
@@ -0,0 +1,22 @@
export abstract class BaseChart {
id: string;
_name: string = 'base-chart';
axis: Array<{ name: string, value: string, type?: 'x' | 'y' }> = [];
data: Array<{ [key: string]: any }> = [];
constructor(id: string, name: string) {
this.id = id;
this._name = name;
}
init(axis: Array<{ name: string, value: string, type?: 'x' | 'y' }>, data: Array<{ [key: string]: any }>): void {
this.axis = axis;
this.data = data;
}
abstract render(): void
abstract destroy(): void
}
@@ -0,0 +1,25 @@
import {BaseChart} from "@/views/chat/component/BaseChart.ts";
import {Chart} from '@antv/g2';
export abstract class BaseG2Chart extends BaseChart {
chart: Chart;
constructor(id: string, name: string) {
super(id, name)
this.chart = new Chart({
container: id,
autoFit: true,
})
}
render() {
this.chart?.render()
}
destroy() {
this.chart?.destroy()
}
}
@@ -0,0 +1,82 @@
<script setup lang="ts">
import {computed, onMounted, onUnmounted} from "vue";
import {getChartInstance} from "@/views/chat/component/index.ts";
import type {BaseChart} from "@/views/chat/component/BaseChart.ts";
const params = withDefaults(defineProps<{
id: string | number
type: string
data?: Array<{ [key: string]: any }>
columns?: Array<{ name: string, value: string }>
x?: Array<{ name: string, value: string }>
y?: Array<{ name: string, value: string }>
}>(), {
data: () => [],
columns: () => [],
x: () => [],
y: () => [],
})
const chartId = computed(() => {
return "chart-component-" + params.id
})
const axis = computed(() => {
const _list: Array<{ name: string, value: string, type?: 'x' | 'y' }> = []
params.columns.forEach(column => {
_list.push({name: column.name, value: column.value})
})
params.x.forEach(column => {
_list.push({name: column.name, value: column.value, type: 'x'})
})
params.y.forEach(column => {
_list.push({name: column.name, value: column.value, type: 'y'})
})
return _list
})
let chartInstance: BaseChart | undefined
function renderChart() {
chartInstance = getChartInstance(params.type, chartId.value)
console.log(chartInstance)
if (chartInstance) {
chartInstance.init(axis.value, params.data)
chartInstance.render()
}
}
function destroyChart() {
if (chartInstance) {
chartInstance.destroy()
chartInstance = undefined
}
}
defineExpose({
renderChart,
destroyChart,
})
onMounted(() => {
renderChart()
})
onUnmounted(() => {
destroyChart()
})
</script>
<template>
<div class="chart-container" :id="chartId"></div>
</template>
<style scoped lang="less">
.chart-container {
height: 100%;
width: 100%;
min-height: 200px;
min-width: 200px;
}
</style>
@@ -0,0 +1,33 @@
import {BaseG2Chart} from "@/views/chat/component/BaseG2Chart.ts";
export class Bar extends BaseG2Chart {
constructor(id: string) {
super(id, "bar");
}
init(axis: Array<{ name: string; value: string; type: "x" | "y" }>, data: Array<{ name: string; value: string }>) {
super.init(axis, data);
const x = this.axis.filter(item => item.type === "x");
const y = this.axis.filter(item => item.type === "y");
if (x.length == 0 || y.length == 0) {
return;
}
this.chart?.interval()
.coordinate({transform: [{type: 'transpose'}]})
.data(data)
.encode('x', x[0].value)
.encode('y', y[0].value)
.scale('x', {
nice: true,
})
.scale('y', {
nice: true,
})
.interaction('elementHighlight', { background: true });
}
}
@@ -0,0 +1,33 @@
import {BaseG2Chart} from "@/views/chat/component/BaseG2Chart.ts";
export class Column extends BaseG2Chart {
constructor(id: string) {
super(id, "column");
}
init(axis: Array<{ name: string; value: string; type: "x" | "y" }>, data: Array<{ name: string; value: string }>) {
super.init(axis, data);
const x = this.axis.filter(item => item.type === "x");
const y = this.axis.filter(item => item.type === "y");
if (x.length == 0 || y.length == 0) {
return;
}
this.chart?.interval()
.data(data)
.encode('x', x[0].value)
.encode('y', y[0].value)
.scale('x', {
nice: true,
})
.scale('y', {
nice: true,
})
.interaction('elementHighlight', { background: true });
}
}
@@ -0,0 +1,43 @@
import {BaseG2Chart} from "@/views/chat/component/BaseG2Chart.ts";
export class Line extends BaseG2Chart {
constructor(id: string) {
super(id, "line");
}
init(axis: Array<{ name: string; value: string; type: "x" | "y" }>, data: Array<{ name: string; value: string }>) {
super.init(axis, data);
const x = this.axis.filter(item => item.type === "x");
const y = this.axis.filter(item => item.type === "y");
if (x.length == 0 || y.length == 0) {
return;
}
this.chart?.data(data)
.encode('x', x[0].value)
.encode('y', y[0].value)
.scale('x', {
nice: true,
})
.scale('y', {
nice: true,
});
this.chart?.line().label({
text: 'value',
style: {
dx: -10,
dy: -12,
},
});
this.chart?.point()
.style('fill', 'white')
.tooltip(false);
}
}
@@ -0,0 +1,29 @@
import {BaseChart} from "@/views/chat/component/BaseChart.ts";
import {Bar} from "@/views/chat/component/charts/Bar.ts";
import {Column} from "@/views/chat/component/charts/Column.ts";
import {Line} from "@/views/chat/component/charts/Line.ts";
const CHART_TYPE_MAP: { [key: string]: any } = {
"column": Column,
"bar": Bar,
"line": Line,
}
const isParent = (type: any, parentType: any) => {
let _type = type
while (_type) {
if (_type === parentType) {
return true
}
_type = _type.__proto__
}
return false
}
export function getChartInstance(type: string, id: string): BaseChart | undefined {
if (isParent(CHART_TYPE_MAP[type], BaseChart)) {
return new CHART_TYPE_MAP[type](id) as BaseChart;
}
return undefined
}
+3
View File
@@ -294,6 +294,9 @@ const sendMessage = async () => {
const data = JSON.parse(str);
switch (data.type) {
case "id":
currentChat.value.records[currentChat.value.records.length - 1].id = data.id;
break;
case "info":
console.log(data.msg);
break;
@@ -80,6 +80,7 @@
v-model:page-size="state.pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
:pager-count="5"
layout="total, sizes, prev, pager, next, jumper"
:total="state.pageInfo.total"
@size-change="handleSizeChange"