mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-02 01:27:14 +00:00
Feat: optimize dashboard UI and fix sql highlight (#1329)
Co-authored-by: hzh97 <2976151305@qq.com> Co-authored-by: aries_ckt <916701291@qq.com>
This commit is contained in:
@@ -45,7 +45,7 @@ function Chart({ chartsData }: Props) {
|
||||
{chartRows?.map((chartRow, index) => (
|
||||
<div key={`chart_row_${index}`} className={`${chartRow?.type !== 'IndicatorValue' ? 'flex gap-3' : ''}`}>
|
||||
{chartRow.charts.map((chart) => {
|
||||
if (chart.chart_type === 'IndicatorValue') {
|
||||
if (chart.chart_type === 'IndicatorValue' || chart.type === 'IndicatorValue') {
|
||||
return (
|
||||
<div key={chart.chart_uid} className="flex flex-row gap-3">
|
||||
{chart.values.map((item) => (
|
||||
@@ -62,11 +62,11 @@ function Chart({ chartsData }: Props) {
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else if (chart.chart_type === 'LineChart') {
|
||||
} else if (chart.chart_type === 'LineChart' || chart.type === 'LineChart') {
|
||||
return <LineChart key={chart.chart_uid} chart={chart} />;
|
||||
} else if (chart.chart_type === 'BarChart') {
|
||||
} else if (chart.chart_type === 'BarChart' || chart.type === 'BarChart') {
|
||||
return <BarChart key={chart.chart_uid} chart={chart} />;
|
||||
} else if (chart.chart_type === 'Table') {
|
||||
} else if (chart.chart_type === 'Table' || chart.type === 'Table') {
|
||||
return <TableChart key={chart.chart_uid} chart={chart} />;
|
||||
}
|
||||
})}
|
||||
|
@@ -9,9 +9,9 @@ import Header from './header';
|
||||
import Chart from '../chart';
|
||||
import classNames from 'classnames';
|
||||
import MuiLoading from '../common/loading';
|
||||
import { Empty } from 'antd';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { getInitMessage } from '@/utils';
|
||||
import MyEmpty from '../common/MyEmpty';
|
||||
|
||||
const ChatContainer = () => {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -33,7 +33,7 @@ const ChatContainer = () => {
|
||||
const contextTemp = list[list.length - 1]?.context;
|
||||
if (contextTemp) {
|
||||
try {
|
||||
const contextObj = JSON.parse(contextTemp);
|
||||
const contextObj = typeof contextTemp === 'string' ? JSON.parse(contextTemp) : contextTemp;
|
||||
setChartsData(contextObj?.template_name === 'report' ? contextObj?.charts : undefined);
|
||||
} catch (e) {
|
||||
setChartsData(undefined);
|
||||
@@ -113,21 +113,15 @@ const ChatContainer = () => {
|
||||
/>
|
||||
<div className="px-4 flex flex-1 flex-wrap overflow-hidden relative">
|
||||
{!!chartsData?.length && (
|
||||
<div className="w-full pb-4 xl:w-3/4 h-3/5 xl:pr-4 xl:h-full overflow-y-auto">
|
||||
<div className="w-full pb-4 xl:w-3/4 h-1/2 xl:pr-4 xl:h-full overflow-y-auto">
|
||||
<Chart chartsData={chartsData} />
|
||||
</div>
|
||||
)}
|
||||
{!chartsData?.length && scene === 'chat_dashboard' && (
|
||||
<Empty
|
||||
image="/empty.png"
|
||||
imageStyle={{ width: 320, height: 320, margin: '0 auto', maxWidth: '100%', maxHeight: '100%' }}
|
||||
className="w-full xl:w-3/4 h-3/5 xl:h-full pt-0 md:pt-10"
|
||||
/>
|
||||
)}
|
||||
{!chartsData?.length && scene === 'chat_dashboard' && <MyEmpty className="w-full xl:w-3/4 h-1/2 xl:h-full" />}
|
||||
{/** chat panel */}
|
||||
<div
|
||||
className={classNames('flex flex-1 flex-col overflow-hidden', {
|
||||
'px-0 xl:pl-4 h-2/5 w-full xl:w-auto xl:h-full border-t xl:border-t-0 xl:border-l dark:border-gray-800': scene === 'chat_dashboard',
|
||||
'px-0 xl:pl-4 h-1/2 w-full xl:w-auto xl:h-full border-t xl:border-t-0 xl:border-l dark:border-gray-800': scene === 'chat_dashboard',
|
||||
'h-full lg:px-8': scene !== 'chat_dashboard',
|
||||
})}
|
||||
>
|
||||
|
@@ -6,7 +6,7 @@ import ChatFeedback from './chat-feedback';
|
||||
import { ChatContext } from '@/app/chat-context';
|
||||
import { FeedBack, IChatDialogueMessageSchema } from '@/types/chat';
|
||||
import classNames from 'classnames';
|
||||
import { Empty, Modal, message, Tooltip } from 'antd';
|
||||
import { Modal, message, Tooltip } from 'antd';
|
||||
import { renderModelIcon } from './header/model-selector';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import copy from 'copy-to-clipboard';
|
||||
@@ -20,6 +20,7 @@ import { getInitMessage } from '@/utils';
|
||||
import { apiInterceptors, getChatFeedBackSelect } from '@/client/api';
|
||||
import useSummary from '@/hooks/use-summary';
|
||||
import AgentContent from './agent-content';
|
||||
import MyEmpty from '../common/MyEmpty';
|
||||
|
||||
type Props = {
|
||||
messages: IChatDialogueMessageSchema[];
|
||||
@@ -200,12 +201,7 @@ const Completion = ({ messages, onSubmit }: Props) => {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Empty
|
||||
image="/empty.png"
|
||||
imageStyle={{ width: 320, height: 320, margin: '0 auto', maxWidth: '100%', maxHeight: '100%' }}
|
||||
className="flex items-center justify-center flex-col h-full w-full"
|
||||
description="Start a conversation"
|
||||
/>
|
||||
<MyEmpty description="Start a conversation" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { ChangeEvent, Key, useEffect, useMemo, useState } from 'react';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Select, Option, Table, Box, Typography, Tooltip } from '@mui/joy';
|
||||
import { Button } from 'antd';
|
||||
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
|
||||
import { Input, Tree, Empty, Tabs } from 'antd';
|
||||
import { Button, Select, Table, Tooltip } from 'antd';
|
||||
import { Input, Tree } from 'antd';
|
||||
import Icon from '@ant-design/icons';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import MonacoEditor from './monaco-editor';
|
||||
import { sendGetRequest, sendSpacePostRequest } from '@/utils/request';
|
||||
@@ -11,9 +10,23 @@ import { useSearchParams } from 'next/navigation';
|
||||
import { OnChange } from '@monaco-editor/react';
|
||||
import Header from './header';
|
||||
import Chart from '../chart';
|
||||
import { CaretRightOutlined, LeftOutlined, RightOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import Database from '../icons/database';
|
||||
import TableIcon from '../icons/table';
|
||||
import Field from '../icons/field';
|
||||
import classNames from 'classnames';
|
||||
import MyEmpty from '../common/MyEmpty';
|
||||
import SplitScreenWeight from '@/components/icons/split-screen-width';
|
||||
import SplitScreenHeight from '@/components/icons/split-screen-height';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
type ITableData = {
|
||||
columns: string[];
|
||||
values: (string | number)[][];
|
||||
};
|
||||
|
||||
interface EditorValueProps {
|
||||
sql?: string;
|
||||
thoughts?: string;
|
||||
@@ -30,7 +43,8 @@ interface RoundProps {
|
||||
interface IProps {
|
||||
editorValue?: EditorValueProps;
|
||||
chartData?: any;
|
||||
tableData?: any;
|
||||
tableData?: ITableData;
|
||||
layout?: 'TB' | 'LR';
|
||||
handleChange: OnChange;
|
||||
}
|
||||
|
||||
@@ -44,64 +58,73 @@ interface ITableTreeItem {
|
||||
children: Array<ITableTreeItem>;
|
||||
}
|
||||
|
||||
function DbEditorContent({ editorValue, chartData, tableData, handleChange }: IProps) {
|
||||
const chartWrapper = React.useMemo(() => {
|
||||
if (!chartData) return <div></div>;
|
||||
function DbEditorContent({ layout = 'LR', editorValue, chartData, tableData, handleChange }: IProps) {
|
||||
const chartWrapper = useMemo(() => {
|
||||
if (!chartData) return null;
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-auto p-3" style={{ flexShrink: 0, overflow: 'hidden' }}>
|
||||
<div className="flex-1 overflow-auto p-2" style={{ flexShrink: 0, overflow: 'hidden' }}>
|
||||
<Chart chartsData={[chartData]} />
|
||||
</div>
|
||||
);
|
||||
}, [chartData]);
|
||||
|
||||
const { columns, dataSource } = useMemo<{ columns: ColumnType<any>[]; dataSource: Record<string, string | number>[] }>(() => {
|
||||
const { columns: cols = [], values: vals = [] } = tableData ?? {};
|
||||
const tbCols = cols.map<ColumnType<any>>((item) => ({
|
||||
key: item,
|
||||
dataIndex: item,
|
||||
title: item,
|
||||
}));
|
||||
const tbDatas = vals.map((row) => {
|
||||
return row.reduce<Record<string, string | number>>((acc, item, index) => {
|
||||
acc[cols[index]] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
|
||||
return {
|
||||
columns: tbCols,
|
||||
dataSource: tbDatas,
|
||||
};
|
||||
}, [tableData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
<div className="flex-1" style={{ flexShrink: 0, overflow: 'auto' }}>
|
||||
<MonacoEditor value={editorValue?.sql || ''} language="mysql" onChange={handleChange} thoughts={editorValue?.thoughts || ''} />
|
||||
</div>
|
||||
{chartWrapper}
|
||||
<div
|
||||
className={classNames('flex w-full flex-1 h-full gap-2 overflow-hidden', {
|
||||
'flex-col': layout === 'TB',
|
||||
'flex-row': layout === 'LR',
|
||||
})}
|
||||
>
|
||||
<div className="flex-1 flex overflow-hidden rounded">
|
||||
<MonacoEditor value={editorValue?.sql || ''} language="mysql" onChange={handleChange} thoughts={editorValue?.thoughts || ''} />
|
||||
</div>
|
||||
<div className="h-96 border-[var(--joy-palette-divider)] border-t border-solid overflow-auto">
|
||||
{tableData?.values?.length > 0 ? (
|
||||
<Table aria-label="basic table" stickyHeader>
|
||||
<thead>
|
||||
<tr>
|
||||
{tableData?.columns?.map((column: any, i: number) => (
|
||||
<th key={column + i}>{column}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tableData?.values?.map((value: any, i: number) => (
|
||||
<tr key={i}>
|
||||
{Object.keys(value)?.map((v) => (
|
||||
<td key={v}>{value[v]}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<div className="flex-1 h-full overflow-auto bg-white dark:bg-theme-dark-container rounded p-4">
|
||||
{!!tableData?.values.length ? (
|
||||
<Table bordered scroll={{ x: 'auto' }} rowKey={columns[0].key} columns={columns} dataSource={dataSource} />
|
||||
) : (
|
||||
<div className="h-full flex justify-center items-center">
|
||||
<Empty />
|
||||
<MyEmpty />
|
||||
</div>
|
||||
)}
|
||||
{chartWrapper}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DbEditor() {
|
||||
const [expandedKeys, setExpandedKeys] = React.useState<React.Key[]>([]);
|
||||
const [searchValue, setSearchValue] = React.useState('');
|
||||
const [currentRound, setCurrentRound] = React.useState<null | string | number>();
|
||||
const [autoExpandParent, setAutoExpandParent] = React.useState(true);
|
||||
const [chartData, setChartData] = React.useState();
|
||||
const [editorValue, setEditorValue] = React.useState<EditorValueProps | EditorValueProps[]>();
|
||||
const [newEditorValue, setNewEditorValue] = React.useState<EditorValueProps>();
|
||||
const [tableData, setTableData] = React.useState<{ columns: string[]; values: any }>();
|
||||
const [currentTabIndex, setCurrentTabIndex] = React.useState<string>();
|
||||
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [currentRound, setCurrentRound] = useState<null | string | number>();
|
||||
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||||
const [chartData, setChartData] = useState();
|
||||
const [editorValue, setEditorValue] = useState<EditorValueProps | EditorValueProps[]>();
|
||||
const [newEditorValue, setNewEditorValue] = useState<EditorValueProps>();
|
||||
const [tableData, setTableData] = useState<{ columns: string[]; values: (string | number)[] }>();
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState<number>();
|
||||
const [isMenuExpand, setIsMenuExpand] = useState<boolean>(false);
|
||||
const [layout, setLayout] = useState<'TB' | 'LR'>('TB');
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const id = searchParams?.get('id');
|
||||
@@ -256,7 +279,7 @@ function DbEditor() {
|
||||
try {
|
||||
if (Array.isArray(res?.data)) {
|
||||
sql = res?.data;
|
||||
setCurrentTabIndex('0');
|
||||
setCurrentTabIndex(0);
|
||||
} else if (typeof res?.data === 'string') {
|
||||
const d = JSON.parse(res?.data);
|
||||
sql = d;
|
||||
@@ -277,39 +300,44 @@ function DbEditor() {
|
||||
},
|
||||
);
|
||||
|
||||
const treeData = React.useMemo(() => {
|
||||
const treeData = useMemo(() => {
|
||||
const loop = (data: Array<ITableTreeItem>, parentKey?: string | number): DataNode[] =>
|
||||
data.map((item: ITableTreeItem) => {
|
||||
const strTitle = item.title;
|
||||
const index = strTitle.indexOf(searchValue);
|
||||
const beforeStr = strTitle.substring(0, index);
|
||||
const afterStr = strTitle.slice(index + searchValue.length);
|
||||
const renderIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'db':
|
||||
return <Database />;
|
||||
case 'table':
|
||||
return <TableIcon />;
|
||||
default:
|
||||
return <Field />;
|
||||
}
|
||||
};
|
||||
const showTitle =
|
||||
index > -1 ? (
|
||||
<Tooltip title={(item?.comment || item?.title) + (item?.can_null === 'YES' ? '(can null)' : `(can't null)`)}>
|
||||
<span>
|
||||
<div className="flex items-center">
|
||||
{renderIcon(item.type)}
|
||||
{beforeStr}
|
||||
<span className="text-[#1677ff]">{searchValue}</span>
|
||||
{afterStr}
|
||||
{item?.type && (
|
||||
<Typography gutterBottom level="body3" className="pl-0.5" style={{ display: 'inline' }}>
|
||||
{`[${item?.type}]`}
|
||||
</Typography>
|
||||
)}
|
||||
</span>
|
||||
{afterStr}
|
||||
{item?.type && <div className="text-gray-400">{item?.type}</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={(item?.comment || item?.title) + (item?.can_null === 'YES' ? '(can null)' : `(can't null)`)}>
|
||||
<span>
|
||||
{strTitle}
|
||||
{item?.type && (
|
||||
<Typography gutterBottom level="body3" className="pl-0.5" style={{ display: 'inline' }}>
|
||||
{`[${item?.type}]`}
|
||||
</Typography>
|
||||
)}
|
||||
</span>
|
||||
<div className="flex items-center">
|
||||
{renderIcon(item.type)}
|
||||
{strTitle}
|
||||
{item?.type && <div className="text-gray-400">{item?.type}</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (item.children) {
|
||||
const itemKey = parentKey ? String(parentKey) + '_' + item.key : item.key;
|
||||
return { title: strTitle, showTitle, key: itemKey, children: loop(item.children, itemKey) };
|
||||
@@ -329,13 +357,14 @@ function DbEditor() {
|
||||
return [];
|
||||
}, [searchValue, tables]);
|
||||
|
||||
const dataList = React.useMemo(() => {
|
||||
const dataList = useMemo(() => {
|
||||
let res: { key: string | number; title: string; parentKey?: string | number }[] = [];
|
||||
const generateList = (data: DataNode[], parentKey?: string | number) => {
|
||||
if (!data || data?.length <= 0) return;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const node = data[i];
|
||||
const { key, title } = node;
|
||||
|
||||
res.push({ key, title: title as string, parentKey });
|
||||
if (node.children) {
|
||||
generateList(node.children, key);
|
||||
@@ -348,8 +377,8 @@ function DbEditor() {
|
||||
return res;
|
||||
}, [treeData]);
|
||||
|
||||
const getParentKey = (key: React.Key, tree: DataNode[]): React.Key => {
|
||||
let parentKey: React.Key;
|
||||
const getParentKey = (key: Key, tree: DataNode[]): Key => {
|
||||
let parentKey: Key;
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
if (node.children) {
|
||||
@@ -363,7 +392,7 @@ function DbEditor() {
|
||||
return parentKey!;
|
||||
};
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
if (tables?.data) {
|
||||
if (!value) {
|
||||
@@ -377,26 +406,26 @@ function DbEditor() {
|
||||
return null;
|
||||
})
|
||||
.filter((item, i, self) => item && self.indexOf(item) === i);
|
||||
setExpandedKeys(newExpandedKeys as React.Key[]);
|
||||
setExpandedKeys(newExpandedKeys as Key[]);
|
||||
}
|
||||
setSearchValue(value);
|
||||
setAutoExpandParent(true);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (currentRound) {
|
||||
handleGetEditorSql(currentRound);
|
||||
}
|
||||
}, [handleGetEditorSql, currentRound]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (editorValue && scene === 'chat_dashboard' && currentTabIndex) {
|
||||
runCharts();
|
||||
}
|
||||
}, [currentTabIndex, scene, editorValue, runCharts]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (editorValue && scene !== 'chat_dashboard') {
|
||||
runSql();
|
||||
}
|
||||
@@ -417,122 +446,173 @@ function DbEditor() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full">
|
||||
<div className="flex flex-col w-full h-full overflow-hidden">
|
||||
<Header />
|
||||
<div className="relative flex flex-1 overflow-auto">
|
||||
<div
|
||||
className="text h-full border-[var(--joy-palette-divider)] border-r border-solid p-3 max-h-full overflow-auto"
|
||||
style={{ width: '300px' }}
|
||||
>
|
||||
<div className="absolute right-4 top-2 z-10">
|
||||
<Button
|
||||
className="mr-2"
|
||||
type="primary"
|
||||
loading={runLoading || runChartsLoading}
|
||||
onClick={async () => {
|
||||
if (scene === 'chat_dashboard') {
|
||||
runCharts();
|
||||
} else {
|
||||
runSql();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
loading={submitLoading || submitChartLoading}
|
||||
onClick={async () => {
|
||||
if (scene === 'chat_dashboard') {
|
||||
await submitChart();
|
||||
} else {
|
||||
await submitSql();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<div className="relative flex flex-1 p-4 pt-0 overflow-hidden">
|
||||
{/* Database Tree Node */}
|
||||
<div className="group/side relative mr-4">
|
||||
<div
|
||||
className={classNames('h-full relative transition-[width] overflow-hidden', {
|
||||
'w-0': isMenuExpand,
|
||||
'w-64': !isMenuExpand,
|
||||
})}
|
||||
>
|
||||
<div className="relative w-64 h-full overflow-hidden flex flex-col rounded bg-white dark:bg-theme-dark-container p-4">
|
||||
<Select
|
||||
size="middle"
|
||||
className="w-full mb-2"
|
||||
value={currentRound}
|
||||
options={rounds?.data?.map((item: RoundProps) => {
|
||||
return {
|
||||
label: item.round_name,
|
||||
value: item.round,
|
||||
};
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setCurrentRound(e);
|
||||
}}
|
||||
/>
|
||||
<Search className="mb-2" placeholder="Search" onChange={onChange} />
|
||||
{treeData && treeData.length > 0 && (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Tree
|
||||
onExpand={(newExpandedKeys: Key[]) => {
|
||||
setExpandedKeys(newExpandedKeys);
|
||||
setAutoExpandParent(false);
|
||||
}}
|
||||
expandedKeys={expandedKeys}
|
||||
autoExpandParent={autoExpandParent}
|
||||
treeData={treeData}
|
||||
fieldNames={{
|
||||
title: 'showTitle',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center py-3">
|
||||
<Select
|
||||
className="h-4 min-w-[240px]"
|
||||
size="sm"
|
||||
value={currentRound as string | null | undefined}
|
||||
onChange={(e: React.SyntheticEvent | null, newValue: string | null) => {
|
||||
setCurrentRound(newValue);
|
||||
<div className="absolute right-0 top-0 translate-x-full h-full flex items-center justify-center opacity-0 hover:opacity-100 group-hover/side:opacity-100 transition-opacity">
|
||||
<div
|
||||
className="bg-white w-4 h-10 flex items-center justify-center dark:bg-theme-dark-container rounded-tr rounded-br z-10 text-xs cursor-pointer shadow-[4px_0_10px_rgba(0,0,0,0.06)] text-opacity-80"
|
||||
onClick={() => {
|
||||
setIsMenuExpand(!isMenuExpand);
|
||||
}}
|
||||
>
|
||||
{rounds?.data?.map((item: RoundProps) => (
|
||||
<Option key={item?.round} value={item?.round}>
|
||||
{item?.round_name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<AutoAwesomeMotionIcon className="ml-2" />
|
||||
{!isMenuExpand ? <LeftOutlined /> : <RightOutlined />}
|
||||
</div>
|
||||
</div>
|
||||
<Search style={{ marginBottom: 8 }} placeholder="Search" onChange={onChange} />
|
||||
{treeData && treeData.length > 0 && (
|
||||
<Tree
|
||||
onExpand={(newExpandedKeys: React.Key[]) => {
|
||||
setExpandedKeys(newExpandedKeys);
|
||||
setAutoExpandParent(false);
|
||||
}}
|
||||
expandedKeys={expandedKeys}
|
||||
autoExpandParent={autoExpandParent}
|
||||
treeData={treeData}
|
||||
fieldNames={{
|
||||
title: 'showTitle',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 max-w-full overflow-hidden">
|
||||
{Array.isArray(editorValue) ? (
|
||||
<>
|
||||
<Box
|
||||
className="h-full"
|
||||
sx={{
|
||||
'.ant-tabs-content, .ant-tabs-tabpane-active': {
|
||||
height: '100%',
|
||||
},
|
||||
'& .ant-tabs-card.ant-tabs-top >.ant-tabs-nav .ant-tabs-tab, & .ant-tabs-card.ant-tabs-top >div>.ant-tabs-nav .ant-tabs-tab': {
|
||||
borderRadius: '0',
|
||||
},
|
||||
{/* Actions */}
|
||||
<div className="mb-2 bg-white dark:bg-theme-dark-container p-2 flex justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="text-xs rounded-none"
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CaretRightOutlined />}
|
||||
loading={runLoading || runChartsLoading}
|
||||
onClick={async () => {
|
||||
if (scene === 'chat_dashboard') {
|
||||
runCharts();
|
||||
} else {
|
||||
runSql();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
className="h-full dark:text-white px-2"
|
||||
activeKey={currentTabIndex}
|
||||
onChange={(activeKey) => {
|
||||
setCurrentTabIndex(activeKey);
|
||||
setNewEditorValue(editorValue?.[Number(activeKey)]);
|
||||
}}
|
||||
items={editorValue?.map((item, i) => ({
|
||||
key: i + '',
|
||||
label: item?.title,
|
||||
children: (
|
||||
<div className="flex flex-col h-full">
|
||||
<DbEditorContent
|
||||
editorValue={item}
|
||||
handleChange={(value) => {
|
||||
const { sql, thoughts } = resolveSqlAndThoughts(value);
|
||||
setNewEditorValue((old) => {
|
||||
return Object.assign({}, old, {
|
||||
sql,
|
||||
thoughts,
|
||||
});
|
||||
});
|
||||
}}
|
||||
tableData={tableData}
|
||||
chartData={chartData}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
className="text-xs rounded-none"
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={submitLoading || submitChartLoading}
|
||||
icon={<SaveFilled />}
|
||||
onClick={async () => {
|
||||
if (scene === 'chat_dashboard') {
|
||||
await submitChart();
|
||||
} else {
|
||||
await submitSql();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Icon
|
||||
className={classNames('flex items-center justify-center w-6 h-6 text-lg rounded', {
|
||||
'bg-theme-primary bg-opacity-10': layout === 'TB',
|
||||
})}
|
||||
component={SplitScreenWeight}
|
||||
onClick={() => {
|
||||
setLayout('TB');
|
||||
}}
|
||||
/>
|
||||
<Icon
|
||||
className={classNames('flex items-center justify-center w-6 h-6 text-lg rounded', {
|
||||
'bg-theme-primary bg-opacity-10': layout === 'LR',
|
||||
})}
|
||||
component={SplitScreenHeight}
|
||||
onClick={() => {
|
||||
setLayout('LR');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Panel */}
|
||||
{Array.isArray(editorValue) ? (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="w-full whitespace-nowrap overflow-x-auto bg-white dark:bg-theme-dark-container mb-2 text-[0px]">
|
||||
{editorValue.map((item, index) => (
|
||||
<Tooltip className="inline-block" key={item.title} title={item.title}>
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[240px] px-3 h-10 text-ellipsis overflow-hidden whitespace-nowrap text-sm leading-10 cursor-pointer font-semibold hover:text-theme-primary transition-colors mr-2 last-of-type:mr-0',
|
||||
{
|
||||
'border-b-2 border-solid border-theme-primary text-theme-primary': currentTabIndex === index,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
setCurrentTabIndex(index);
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{editorValue.map((item, index) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className={classNames('w-full overflow-hidden', {
|
||||
hidden: index !== currentTabIndex,
|
||||
'block flex-1': index === currentTabIndex,
|
||||
})}
|
||||
>
|
||||
<DbEditorContent
|
||||
layout={layout}
|
||||
editorValue={item}
|
||||
handleChange={(value) => {
|
||||
const { sql, thoughts } = resolveSqlAndThoughts(value);
|
||||
setNewEditorValue((old) => {
|
||||
return Object.assign({}, old, {
|
||||
sql,
|
||||
thoughts,
|
||||
});
|
||||
});
|
||||
}}
|
||||
tableData={tableData}
|
||||
chartData={chartData}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DbEditorContent
|
||||
layout={layout}
|
||||
editorValue={editorValue}
|
||||
handleChange={(value) => {
|
||||
const { sql, thoughts } = resolveSqlAndThoughts(value);
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import Editor, { OnChange, loader } from '@monaco-editor/react';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { format } from 'sql-formatter';
|
||||
import { ChatContext } from '@/app/chat-context';
|
||||
import { formatSql } from '@/utils';
|
||||
|
||||
loader.config({ monaco });
|
||||
@@ -16,6 +18,8 @@ interface MonacoEditorProps {
|
||||
|
||||
export default function MonacoEditor({ className, value, language = 'mysql', onChange, thoughts }: MonacoEditorProps) {
|
||||
// merge value and thoughts
|
||||
const { mode } = useContext(ChatContext);
|
||||
|
||||
const editorValue = useMemo(() => {
|
||||
if (language !== 'mysql') {
|
||||
return value;
|
||||
@@ -32,11 +36,8 @@ export default function MonacoEditor({ className, value, language = 'mysql', onC
|
||||
value={editorValue}
|
||||
language={language}
|
||||
onChange={onChange}
|
||||
theme="vs-dark"
|
||||
theme={mode === 'dark' ? 'vs-dark' : 'light'}
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
wordWrap: 'on',
|
||||
}}
|
||||
/>
|
||||
|
@@ -1,20 +1,22 @@
|
||||
import { Button, Empty } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
error?: boolean;
|
||||
description?: string;
|
||||
refresh?: () => void;
|
||||
}
|
||||
|
||||
function MyEmpty({ error, description, refresh }: Props) {
|
||||
function MyEmpty({ className, error, description, refresh }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Empty
|
||||
image="/empty.png"
|
||||
imageStyle={{ width: 320, height: 320, margin: '0 auto', maxWidth: '100%', maxHeight: '100%' }}
|
||||
className="flex items-center justify-center flex-col h-full w-full"
|
||||
imageStyle={{ width: 320, height: 196, margin: '0 auto', maxWidth: '100%', maxHeight: '100%' }}
|
||||
className={classNames('flex items-center justify-center flex-col h-full w-full', className)}
|
||||
description={
|
||||
error ? (
|
||||
<Button type="primary" onClick={refresh}>
|
||||
|
19
web/components/icons/database.tsx
Normal file
19
web/components/icons/database.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Database() {
|
||||
return (
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="49817" width="1em" height="1em">
|
||||
<path
|
||||
d="M512 64c-247.424 0-448 62.72-448 140.032v112c0 77.312 200.576 139.968 448 139.968s448-62.72 448-140.032v-112C960 126.72 759.424 64 512 64z m0 728c-247.424 0-448-62.72-448-140.032v168.064C64 897.28 264.576 960 512 960s448-62.72 448-140.032v-167.936c0 77.312-200.576 139.968-448 139.968z"
|
||||
fill="#3699FF"
|
||||
p-id="49818"
|
||||
></path>
|
||||
<path
|
||||
d="M512 540.032c-247.424 0-448-62.72-448-140.032v168c0 77.312 200.576 140.032 448 140.032s448-62.72 448-140.032V400c0 77.312-200.576 140.032-448 140.032z"
|
||||
fill="#3699FF"
|
||||
opacity=".32"
|
||||
p-id="49819"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
21
web/components/icons/field.tsx
Normal file
21
web/components/icons/field.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Field() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="67616"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path
|
||||
d="M39.385 204.83h346.571L252.054 976.74l-23.63 39.383h259.929v-31.506L614.379 204.83H771.91S960.951 220.584 984.581 0.038H236.3S94.52-7.84 39.384 204.83"
|
||||
fill="#1296db"
|
||||
p-id="67617"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
12
web/components/icons/split-screen-height.tsx
Normal file
12
web/components/icons/split-screen-height.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
function SplitScreenHeight() {
|
||||
return (
|
||||
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024" version="1.1">
|
||||
<path
|
||||
d="M161.05472 919.3472h701.9008a58.71616 58.71616 0 0 0 58.65472-58.65472V180.40832a58.71616 58.71616 0 0 0-58.65472-58.65472H161.09568a58.03008 58.03008 0 0 0-41.4208 17.08032A58.1632 58.1632 0 0 0 102.4 180.30592v680.38656a58.64448 58.64448 0 0 0 58.65472 58.65472z m385.15712-589.568V190.08512h306.95424v660.93056H546.21184V329.7792zM170.83392 190.08512h306.95424v660.93056H170.83392V190.08512z"
|
||||
p-id="13913"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SplitScreenHeight;
|
12
web/components/icons/split-screen-width.tsx
Normal file
12
web/components/icons/split-screen-width.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
function SplitScreenWeight() {
|
||||
return (
|
||||
<svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024" version="1.1">
|
||||
<path
|
||||
d="M171.85792 110.9504a58.65472 58.65472 0 0 0-58.65472 58.65472v701.9008a58.7264 58.7264 0 0 0 58.65472 58.65472h680.28416a58.7264 58.7264 0 0 0 58.65472-58.65472V169.64608a57.98912 57.98912 0 0 0-17.08032-41.41056 58.1632 58.1632 0 0 0-41.472-17.27488H171.85792z m670.60736 750.77632H181.53472V554.77248h660.93056v306.95424z m0-375.38816H181.53472V179.38432h660.93056v306.95424z"
|
||||
p-id="14553"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SplitScreenWeight;
|
23
web/components/icons/table.tsx
Normal file
23
web/components/icons/table.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Table() {
|
||||
return (
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="59847" width="1em" height="1em">
|
||||
<path d="M149.2 99.7h726.6c27.7 0 50.1 22.4 50.1 50.1V336H99.1V149.8c0-27.6 22.4-50.1 50.1-50.1z" fill="#1ECD93" p-id="59848"></path>
|
||||
<path
|
||||
d="M99.1 395h236.2v236.3H99.1zM99.1 690.3h236.2v236.2H149.2c-27.7 0-50.1-22.4-50.1-50.1V690.3zM394.4 395h236.2v236.3H394.4z"
|
||||
fill="#1ECD93"
|
||||
fill-opacity=".5"
|
||||
p-id="59849"
|
||||
></path>
|
||||
<path d="M394.4 690.3h236.2v236.3H394.4z" fill="#A1E6C9" p-id="59850" data-spm-anchor-id="a313x.search_index.0.i13.27343a81CqKUWU"></path>
|
||||
<path d="M689.7 395h236.2v236.3H689.7z" fill="#1ECD93" fill-opacity=".5" p-id="59851"></path>
|
||||
<path
|
||||
d="M689.7 690.3h236.2v186.1c0 27.7-22.4 50.1-50.1 50.1H689.7V690.3z"
|
||||
fill="#A1E6C9"
|
||||
p-id="59852"
|
||||
data-spm-anchor-id="a313x.search_index.0.i17.27343a81CqKUWU"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import { message } from 'antd';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import i18n from '@/app/i18n';
|
||||
import { ChatContext } from '@/app/chat-context';
|
||||
|
||||
type Props = {
|
||||
queryAgentURL?: string;
|
||||
@@ -19,6 +20,7 @@ type ChatParams = {
|
||||
|
||||
const useChat = ({ queryAgentURL = '/api/v1/chat/completions' }: Props) => {
|
||||
const ctrl = useMemo(() => new AbortController(), []);
|
||||
const { scene } = useContext(ChatContext);
|
||||
|
||||
const chat = useCallback(
|
||||
async ({ data, chatId, onMessage, onClose, onDone, onError }: ChatParams) => {
|
||||
@@ -61,16 +63,25 @@ const useChat = ({ queryAgentURL = '/api/v1/chat/completions' }: Props) => {
|
||||
onmessage: (event) => {
|
||||
let message = event.data;
|
||||
try {
|
||||
message = JSON.parse(message).vis;
|
||||
if (scene === 'chat_agent') {
|
||||
message = JSON.parse(message).vis;
|
||||
} else {
|
||||
message = JSON.parse(message);
|
||||
}
|
||||
} catch (e) {
|
||||
message.replaceAll('\\n', '\n');
|
||||
}
|
||||
if (message === '[DONE]') {
|
||||
onDone?.();
|
||||
} else if (message?.startsWith('[ERROR]')) {
|
||||
onError?.(message?.replace('[ERROR]', ''));
|
||||
if (typeof message === 'string') {
|
||||
if (message === '[DONE]') {
|
||||
onDone?.();
|
||||
} else if (message?.startsWith('[ERROR]')) {
|
||||
onError?.(message?.replace('[ERROR]', ''));
|
||||
} else {
|
||||
onMessage?.(message);
|
||||
}
|
||||
} else {
|
||||
onMessage?.(message);
|
||||
onDone?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
@@ -15,6 +15,7 @@ export type ChartData = {
|
||||
chart_uid: string;
|
||||
column_name: Array<string>;
|
||||
values: Array<ChartValue>;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type SceneResponse = {
|
||||
|
Reference in New Issue
Block a user