feat: init chart

This commit is contained in:
ulleo
2025-06-09 15:44:43 +08:00
parent e5ae9b5ceb
commit 09a0bc8c63
14 changed files with 371 additions and 62 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}
+1
View File
@@ -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",
+90 -59
View File
@@ -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
}
+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;