mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
feat: init chart
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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}
|
||||
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import type {ChatMessage} from "@/api/chat.ts";
|
||||
import {computed, ref} from "vue";
|
||||
import {computed, nextTick, ref} from "vue";
|
||||
import type {TabsPaneContext} from 'element-plus'
|
||||
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,30 @@ 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="cloumn">cloumn</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 +113,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 +179,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, ref} 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
|
||||
})
|
||||
|
||||
const chartInstance = ref<BaseChart>()
|
||||
|
||||
function renderChart() {
|
||||
chartInstance.value = getChartInstance(params.type, chartId.value)
|
||||
console.log(chartInstance.value)
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.init(axis.value, params.data)
|
||||
chartInstance.value.render()
|
||||
}
|
||||
}
|
||||
|
||||
function destroyChart() {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.destroy()
|
||||
chartInstance.value = 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: 80px;
|
||||
min-width: 100px;
|
||||
}
|
||||
</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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user