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
+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;