Commit 7ec6916c authored by 熊洋洋's avatar 熊洋洋

feat: init project 🚀🔥🔥

parents
{
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/rules-of-hooks": "off", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "off",// Checks effect dependencies
}
}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
backend
TOKEN.ts
.env
.vscode
\ No newline at end of file
**/*.svg
package.json
out
.DS_Store
*.png
.editorconfig
Dockerfile*
.gitignore
.prettierignore
LICENSE
.next
# .prettierrc
tabWidth: 4
semi: true
singleQuote: true
trailingComma: 'es5'
bracketSpacing: true
jsxBracketSameLine: false
arrowParens: 'avoid'
printWidth: 100
# CHAT-QUERY
[en](./README.md)
> Chat-Query 是一个基于元数据模型和 AI 技术,通过自然语言实现数据查询。
## 演示
> [view](https://chat-quey.netlify.app/)
> [demo](https://cdn.glitch.me/fd139a45-4a65-41b6-9634-41617ab20cdc/%E6%BC%94%E7%A4%BA.gif?v=1686907695067)
+ **功能特点🐂:**
- 支持导入 DDL、DBML 和数据库逆向解析,AI 自动生成业务模型。
- 提供业务模型的基本 CRUD 功能、AI 智能分析,支持模型导出为 DDL、DBML 以及与数据库同步。
- 结合模型和 AI 实现自然语言数据查询,可添加至查询列表并通过 API 调用。
## 应用场景🎬
+ 从低代码到零代码开发。
+ 非业务人员快速进行数据分析。
+ 更多应用场景待探索...
## 开发环境设置
> 👏 欢迎参与 Chat-Query 的建设。
+ 后端:
```js
pnpm start:dev
```
- 在 .env 文件中添加 OPEN_AI_API_KEY='sk-...'等环境变量
+ 前端
```js
pnpm install
gitpnpm dev
```
-在 .env 文件中添加 NEXT_PUBLIC_OPEN_AI_API_KEY='sk-...'
## 系统架构
![架构](https://cdn.glitch.global/fd139a45-4a65-41b6-9634-41617ab20cdc/%E6%97%A0%E6%A0%87%E9%A2%98-2023-05-31-1202%20(1).png?v=1686908252244)
\ No newline at end of file
# CHAT-QUERY
[中文](./README-zh.md)
> Chat-Query is a data query tool based on metadata models and AI technology, enabling natural language queries.
## Demo
> [view](https://chat-quey.netlify.app/)
> [demo](https://cdn.glitch.me/fd139a45-4a65-41b6-9634-41617ab20cdc/%E6%BC%94%E7%A4%BA.gif?v=1686907695067)
+ **Features🐂:**
- Supports importing DDL, DBML, and reverse database parsing, with AI automatically generating business models.
- Provides basic CRUD functionality for business models, AI intelligent analysis, and supports exporting models as DDL, DBML, and synchronizing with databases.
- Combines models and AI to enable natural language data queries, which can be added to the query list and called via API.
## Application Scenarios🎬
+ From low-code to no-code development.
+ Non-business users can quickly perform data analysis.
+ More application scenarios to be explored...
## Development Environment Setup
> 👏 Welcome to contribute to the development of Chat-Query.
+ Backend:
```js
pnpm start:dev
```
- Add environement variables such as OPEN_AI_API_KEY = 'sk-...' to the .env file
+ Frontend
```js
pnpm install
pnpm dev
```
- Add OPEN_AI_API_KEY='sk-...' in components/AITool/TOKEN.ts
## System Architecture
![Architecture](https://cdn.glitch.global/fd139a45-4a65-41b6-9634-41617ab20cdc/%E6%97%A0%E6%A0%87%E9%A2%98-2023-05-31-1202%20(1).png?v=1686908252244)
\ No newline at end of file
import getConfig from 'next/config';
import APi, { backendApi } from '.';
const {
publicRuntimeConfig: { apiPath },
} = getConfig();
export type DBInfo = {
host: string;
port: number;
user: string;
password: string;
database: string;
client: 'mysql2';
name?: string
};
export default class ConnectDb {
static create(config: DBInfo) {
return backendApi.post('query/testConnectDb', config);
}
static getDbDBML(config: DBInfo) {
return backendApi.post('query/getDbDBML', config);
}
static addDbForSchema(params: {
'config': DBInfo,
'schemaId':string,
'name':string
}) {
return backendApi.post('query/createDbConnect',params);
}
static getAllForSchema(schemaId:string) {
return backendApi.get(`query/${schemaId}/DbConnect`);
}
static removeDbForSchema(DbID:string) {
return backendApi.delete(`query/DbConnect/${DbID}`);
}
static addQuery(query: {
schemaId:string,
DbID:string,
name:string,
content: {
executions:{
content: string;
type: string
},
params:Record<string,any>
info: { queryDescription:string,queryName:string}
}
}) {
return backendApi.post("query/add",query)
}
static deleteQuery(queryId:string) {
return backendApi.delete(`/query/${queryId}`)
}
static getQueries(schemaId: string) {
return backendApi.get(`query/${schemaId}/queries`)
}
static runQuery(queryId: string,params:Record<string, any>) {
return backendApi.post(`query/run/${queryId}`,{params})
}
}
import getConfig from 'next/config';
import APi, { backendApi } from '.';
const {
publicRuntimeConfig: { apiPath },
} = getConfig();
export type ExecuteSqlPrams = Record<string, any>;
export default class ExecuteQuery {
static executeSql(config: ExecuteSqlPrams, execution: {
content: string;
type: string
}[], dbID: string,) {
return backendApi.post( '/query/querySql', {
config,
execution,
dbID,
});
}
}
import getConfig from 'next/config';
import type { NextApiRequest, NextApiResponse } from 'next';
import { table } from 'console';
import APi from '.';
const {
publicRuntimeConfig: { apiPath },
publicRuntimeConfig,
} = getConfig();
export interface schemaParams {
type: 'schema' | 'table';
name?: string;
}
export default class getSchema {
static getTableList(config: schemaParams) {
return APi.post(apiPath + 'getSchema', config);
}
}
import getConfig from 'next/config';
import APi, { backendApi } from '.';
const {
publicRuntimeConfig: { apiPath },
publicRuntimeConfig,
} = getConfig();
export interface View {
type: 'schema' | 'table';
name?: string;
}
export default class getView {
static getViewComponent(params: {
props: Record<string, any>,
need: string
}) {
return backendApi.post('/openAi/api/reactLive', params);
}
}
import axios from 'redaxios';
const APi = axios.create({
});
export const backendApi = axios.create({
"baseURL":process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:3001"
});
export default APi;
\ No newline at end of file
import React from 'react';
import MarkdownIt from 'markdown-it';
import mdHighlight from 'markdown-it-highlightjs';
// import 'katex/dist/katex.min.css';
import doMarkdownit from '@digitalocean/do-markdownit';
import Prism from '@digitalocean/do-markdownit/vendor/prismjs';
import prismTools from '@digitalocean/do-markdownit/vendor/prismjs/plugins/toolbar/prism-toolbar';
import prismCopyToClipboard from '@digitalocean/do-markdownit/vendor/prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard';
import { debounce } from 'lodash';
// import style manually
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface Props {
role: ChatMessage['role'];
message: string;
}
// Finish!
const addPlugins = debounce(() => {
prismTools(Prism);
prismCopyToClipboard(Prism);
Prism.highlightAll();
}, 50);
export const htmlString = (message: string | (() => string)) => {
const md = MarkdownIt()
.use(mdHighlight)
.use(doMarkdownit, {
fence_environment: {
allowedEnvironments: '*',
},
fence_classes: {
allowedClasses: false,
},
callout: {
allowedClasses: ['note', 'warning', 'info', 'draft'],
},
});
addPlugins();
if (typeof message === 'function') {
return md.render(message());
} else if (typeof message === 'string') {
return md.render(message);
}
return '';
};
export function codeWrapper(type: string, code: string) {
return '``` ' + type + '\n' + code + '\n' + '```';
}
export default function MessageItem({ message, role }: Props) {
// role === 'user';
return (
<div
className={`flex gap-3 p-4 box-border mx-[5px] shadow rounded transition-colors mt-[20px] font-hm ${
role === 'user'
? 'bg-[rgb(var(--primary-6))] text-white shadow-[var(--pc)]'
: 'bg-[var(--white-bg)] text-[#333]'
}`}
>
<div
className="message prose text-slate break-words overflow-hidden"
dangerouslySetInnerHTML={{
__html: htmlString(message),
}}
/>
</div>
);
}
This diff is collapsed.
import { useCallback } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
} from 'reactflow';
// 👇 you need to import the reactflow styles
import 'reactflow/dist/style.css';
const initialNodes = [];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(params => setEdges(eds => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
className="canvas"
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
}
export default Flow;
import { Dropdown, Menu, Space, Divider } from '@arco-design/web-react';
import graphState from '../hooks/use-graph-state';
import tableModel from '../hooks/table-model';
import { useTranslation } from 'react-i18next';
export default function ContextMenu({ setShowModal, children }) {
const { version } = graphState.useContainer();
const { updateGraph, addTable } = tableModel();
const { t } = useTranslation('ContextMenu');
const menus = [
{
key: 'N',
title: t('Add New Table'),
action: () => addTable(),
},
{
key: 'I',
title: t('Import Table'),
action: () => setShowModal('import'),
},
{
key: 'line',
},
{
key: 'S',
title: t('Save Change'),
action: () => updateGraph(),
},
{
key: 'E',
title: t('Export Database'),
action: () => setShowModal('export'),
},
];
return (
<Dropdown
trigger="contextMenu"
position="bl"
droplist={
version !== 'currentVersion' ? null : (
<Menu className="context-menu">
{menus.map(item =>
item.key === 'line' ? (
<Divider key={item.key} className="context-menu-line" />
) : (
<Menu.Item
key={item.key}
className="context-menu-item"
onClick={item.action}
>
{item.title}
<Space size={4}>
<div className="arco-home-key"></div>
<div className="arco-home-key">{item.key}</div>
</Space>
</Menu.Item>
)
)}
</Menu>
)
}
>
{children}
</Dropdown>
);
}
import { useState, useEffect } from 'react';
import { Modal, Notification, Tabs } from '@arco-design/web-react';
import Editor from '@monaco-editor/react';
import graphState from '../hooks/use-graph-state';
import exportSQL from '../utils/export-sql';
import { useTranslation } from 'react-i18next';
const TabPane = Tabs.TabPane;
/**
* It's a modal that displays the command to be exported
* @returns Modal component
*/
export default function ExportModal({ showModal, onCloseModal }) {
const [exportType, setExportType] = useState('dbml');
const [sqlValue, setSqlValue] = useState('');
const { t } = useTranslation('modal');
const { tableDict, linkDict, theme } = graphState.useContainer();
const copy = async () => {
try {
await window.navigator.clipboard.writeText(sqlValue);
Notification.success({
title: t('Copy Success'),
});
} catch (e) {
console.log(e);
Notification.error({
title: t('Copy Failed'),
});
}
};
useEffect(() => {
if (showModal === 'export') {
const sql = exportSQL(tableDict, linkDict, exportType);
setSqlValue(sql);
}
}, [showModal, exportType]);
const editor = (
<Editor
className={`!mt-0 ${theme === 'dark' ? 'bg-[#1e1e1e]' : ' bg-[#fff]'} mt-[10px]`}
language={exportType === 'dbml' ? 'apex' : 'sql'}
width="680px"
height="60vh"
theme={theme === 'dark' ? 'vs-dark' : 'light'}
value={sqlValue}
options={{
acceptSuggestionOnCommitCharacter: true,
acceptSuggestionOnEnter: 'on',
accessibilitySupport: 'auto',
autoIndent: false,
automaticLayout: true,
codeLens: true,
colorDecorators: true,
contextmenu: true,
cursorBlinking: 'blink',
cursorSmoothCaretAnimation: false,
cursorStyle: 'line',
disableLayerHinting: false,
disableMonospaceOptimizations: false,
dragAndDrop: false,
fixedOverflowWidgets: false,
folding: true,
foldingStrategy: 'auto',
fontLigatures: false,
formatOnPaste: false,
formatOnType: false,
hideCursorInOverviewRuler: false,
highlightActiveIndentGuide: true,
links: true,
mouseWheelZoom: false,
multiCursorMergeOverlapping: true,
multiCursorModifier: 'alt',
overviewRulerBorder: true,
overviewRulerLanes: 2,
quickSuggestions: true,
quickSuggestionsDelay: 100,
readOnly: false,
renderControlCharacters: false,
renderFinalNewline: true,
renderIndentGuides: true,
renderLineHighlight: 'line',
renderWhitespace: 'none',
revealHorizontalRightPadding: 300,
roundedSelection: true,
rulers: [],
scrollBeyondLastColumn: 5,
scrollBeyondLastLine: true,
selectOnLineNumbers: true,
selectionClipboard: true,
selectionHighlight: true,
showFoldingControls: 'mouseover',
smoothScrolling: false,
suggestOnTriggerCharacters: true,
wordBasedSuggestions: true,
wordSeparators: '~!@#$%^&*()-=+[{]}|;:\'",.<>/?',
wordWrap: 'wordWrapColumn',
wordWrapBreakAfterCharacters: '\t})]?|&,;',
wordWrapBreakBeforeCharacters: '{([+',
wordWrapBreakObtrusiveCharacters: '.',
wordWrapColumn: 80,
wordWrapMinified: true,
wrappingIndent: 'none',
// minimap: {
// autohide: true,
// },
}}
onChange={setSqlValue}
/>
);
return (
<Modal
title={null}
simple
visible={showModal === 'export'}
autoFocus={false}
onOk={() => copy()}
okText={t('Copy')}
cancelText={t('Close')}
onCancel={() => onCloseModal()}
style={{ width: 'auto' }}
>
<h5 className="text-[20px] py-[10px] font-bold">{t('Export ERD Data Model')}</h5>
<Tabs
activeTab={exportType}
onChange={val => setExportType(val)}
className="ring-2 ring-[#359c899a] p-0 w-[680px]"
>
<TabPane key="dbml" title="DBML">
{editor}
</TabPane>
<TabPane key="postgres" title="PostgreSQL">
{editor}
</TabPane>
<TabPane key="mysql" title="MySQL">
{editor}
</TabPane>
<TabPane key="mssql" title="MSSQL">
{editor}
</TabPane>
</Tabs>
</Modal>
);
}
import { Checkbox, Form, Input, Space, Tag, Modal, AutoComplete } from '@arco-design/web-react';
import fieldTypes from '../data/filed_typs';
import graphState from '../hooks/use-graph-state';
import tableModel from '../hooks/table-model';
import { useTranslation } from 'react-i18next';
/**
* It renders a form for editing a table
* @param props - The props passed to the component.
* @returns A TableForm component
*/
export default function FieldForm(props) {
const [form] = Form.useForm();
const { formChange, onFormChange } = props;
const { editingField, setEditingField, addingField, setAddingField } =
graphState.useContainer();
const { updateTable, removeField } = tableModel();
const { field, table } = editingField;
const save = values => {
const data = { ...field, ...values };
table.fields = table.fields.map(f => (f.id === data.id ? data : f));
updateTable(table);
};
const { t } = useTranslation('graph');
return table ? (
<Modal
title={
<div style={{ textAlign: 'left' }}>
{t('Edit')}
{table ? (
<Tag color="arcoblue" style={{ margin: '0 4px' }}>
{table.name}
</Tag>
) : (
''
)}
{t('Field')}
</div>
}
visible={!!table}
onCancel={() => {
if (addingField?.index) {
removeField(addingField.table, addingField.index);
}
setEditingField({});
}}
onOk={() => {
setAddingField(null);
form.submit();
}}
escToExit={!formChange}
maskClosable={!formChange}
afterClose={() => {
onFormChange(false);
}}
afterOpen={() => {
form.resetFields();
}}
style={{ width: 580 }}
okText="Commit"
cancelText={t('Cancel')}
>
{field && (
<Form
onSubmit={save}
form={form}
labelAlign="left"
requiredSymbol={false}
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
onValuesChange={(changedValues, allValues) => {
if (!formChange) onFormChange(true);
}}
>
<Space direction="vertical" style={{ width: '100%' }}>
<Space className="table-form-item">
<Form.Item
label={t('Name')}
field="name"
initialValue={field.name}
rules={[
{
required: true,
message: t('Please enter field name'),
},
{
validator: (value, cb) => {
return table.fields
.filter(item => item.id !== field.id)
.find(item => item.name === value)
? cb(t('have same name field'))
: cb();
},
},
]}
>
<Input allowClear placeholder={t('Name')} />
</Form.Item>
<Form.Item
label={t('Type')}
field="type"
initialValue={field.type}
rules={[
{
required: true,
message: t('Please choose field type'),
},
]}
>
<AutoComplete
data={fieldTypes}
placeholder={t('Type')}
></AutoComplete>
</Form.Item>
</Space>
<Space className="table-form-item">
<Form.Item
label={t('Comment')}
field="note"
initialValue={field.note || ''}
>
<Input allowClear placeholder={t('Comment')} />
</Form.Item>
<Form.Item
label={t('Default')}
field="dbdefault"
initialValue={field.dbdefault || ''}
>
<Input allowClear placeholder={t('Default')} />
</Form.Item>
</Space>
<Space className="table-form-item">
<Form.Item noStyle field="pk" initialValue={field.pk}>
<Checkbox defaultChecked={field.pk}>Primary</Checkbox>
</Form.Item>
<Form.Item noStyle field="unique" initialValue={field.unique}>
<Checkbox defaultChecked={field.unique}>Unique</Checkbox>
</Form.Item>
<Form.Item noStyle field="not_null" initialValue={field.not_null}>
<Checkbox defaultChecked={field.not_null}>Not Null</Checkbox>
</Form.Item>
<Form.Item noStyle field="increment" initialValue={field.increment}>
<Checkbox defaultChecked={field.increment}>Increment</Checkbox>
</Form.Item>
</Space>
</Space>
</Form>
)}
</Modal>
) : null;
}
This diff is collapsed.
import { Modal, Button, Space, Popconfirm } from '@arco-design/web-react';
import graphState from '../hooks/use-graph-state';
import { useTranslation } from 'react-i18next';
/**
* It renders a modal that allows the user to change the relation of a link or delete the link
* @param props - { editingLink, setEditingLink, setLinkDict }
* @returns Modal component
*/
export default function LinkModal(props) {
const { editingLink, setEditingLink } = props;
const { setLinkDict } = graphState.useContainer();
const { t } = useTranslation('linkModal');
const changeRelation = relation => {
const { linkId, fieldId } = editingLink;
setLinkDict(state => {
return {
...state,
[linkId]: {
...state[linkId],
endpoints: state[linkId].endpoints.map(endpoint => {
if (endpoint.fieldId === fieldId) {
return {
...endpoint,
relation,
};
}
if (relation === '*' && endpoint.fieldId !== fieldId) {
return {
...endpoint,
relation: '1',
};
}
return endpoint;
}),
},
};
});
setEditingLink(null);
};
const removeLink = () => {
const { linkId, fieldId } = editingLink;
setLinkDict(state => {
delete state[linkId];
return { ...state };
});
setEditingLink(null);
};
return (
<Modal
title="Link"
visible={!!editingLink}
onCancel={() => setEditingLink(null)}
footer={null}
autoFocus={false}
focusLock={false}
>
<Space
style={{
width: '100%',
justifyContent: 'space-between',
}}
>
<Space>
<label>{t('Change relation')}:</label>
<Button
type="primary"
onClick={() => {
changeRelation('1');
}}
>
{t('startPoint')}
</Button>
<Button
type="primary"
onClick={() => {
changeRelation('*');
}}
>
{t('endpoint')}
</Button>
</Space>
<Popconfirm
title="Are you sure to delete this path?"
onOk={() => {
removeLink();
}}
>
<Button>{t('Delete Path')}</Button>
</Popconfirm>
</Space>
</Modal>
);
}
import graphState from '../hooks/use-graph-state';
import { tableWidth, fieldHeight, commentHeight, titleHeight } from '../data/settings';
const control = 20;
const padding = 5;
const gripWidth = 10;
const gripRadius = gripWidth / 2;
const margin = 0.5;
/**
* It takes a link object and returns a path element that connects the two tables
* @param props - The props object that is passed to the component.
* {
* link,
* setEditingLink,
* isActive
* }
* @returns A svg path is being returned.
*/
export default function LinkPath(props) {
const { link, setEditingLink, isActive } = props;
const { tableDict, version } = graphState.useContainer();
const editable = version === 'currentVersion';
if (!tableDict) return null;
const { endpoints } = link;
const [sourceTable, targetTable] = [tableDict[endpoints[0].id], tableDict[endpoints[1].id]];
const [sourceFieldIndex, targetFieldIndex] = [
sourceTable.fields.findIndex(field => field.id === endpoints[0].fieldId),
targetTable.fields.findIndex(field => field.id === endpoints[1].fieldId),
];
const calcHeight = titleHeight + commentHeight + fieldHeight / 2;
const sourceFieldPosition = {
x: sourceTable.x,
y: sourceTable.y + sourceFieldIndex * fieldHeight + calcHeight,
...endpoints[0],
};
const targetFieldPosition = {
x: targetTable.x,
y: targetTable.y + targetFieldIndex * fieldHeight + calcHeight,
...endpoints[1],
};
// const [source, target] = [sourceFieldPosition, targetFieldPosition];
const [source, target] = [sourceFieldPosition, targetFieldPosition].sort((a, b) => {
return a.x - b.x || a.y - b.y;
});
const sourceLeft = source.x + padding + gripRadius + margin;
const sourceRight = source.x + tableWidth - padding - gripRadius - margin;
let x = sourceLeft;
const y = source.y + gripRadius + margin;
const targetLeft = target.x + padding + gripRadius + margin;
const targetRight = target.x + tableWidth - padding - gripRadius - margin;
let minDistance = Math.abs(sourceLeft - targetLeft);
let x1 = targetLeft;
[
[sourceLeft, targetRight],
[sourceRight, targetLeft],
[sourceRight, targetRight],
].forEach(items => {
if (Math.abs(items[0] - items[1]) < minDistance) {
minDistance = Math.min(items[0] - items[1]);
x = items[0];
x1 = items[1];
}
});
const y1 = target.y + gripRadius + margin;
const midX = x1 - (x1 - x) / 2;
const midY = y1 - (y1 - y) / 2;
const handlerContextMenu = e => {
e.preventDefault();
e.stopPropagation();
};
let d = `M ${x} ${y}
C ${x + control} ${y} ${midX} ${midY} ${midX} ${midY}
C ${midX} ${midY} ${x1 - control} ${y1} ${x1} ${y1}`;
let foreignObjectPositions = [
{
x: (x + control + midX) / 2 - 10,
y: (y + midY) / 2 - 10,
},
{ x: (x1 - control + midX) / 2 - 10, y: (y1 + midY) / 2 - 10 },
];
if (endpoints[0].id == endpoints[1].id) {
const factor = (y1 - y) / 50 < 2 ? 2 : (y1 - y) / 50;
d = `M ${sourceRight} ${y}
L ${sourceRight + control} ${y}
C ${sourceRight + control * factor} ${y} ${sourceRight + control * factor} ${y1} ${
sourceRight + control
} ${y1}
L ${sourceRight} ${y1}`;
foreignObjectPositions = [
{
x: 0,
y: 0,
},
{ x: targetRight + control - 10, y: y1 - 10 },
];
}
return (
<>
<path
d={d}
stroke="black"
// strokeWidth="2"
fill="none"
className={`path-line hover:stroke-[var(--pc)] hover:stroke-[5px] ${
isActive ? 'stroke-[var(--pc)] stroke-[5px]' : ''
}`}
/>
<foreignObject
x={foreignObjectPositions[0].x}
y={foreignObjectPositions[0].y}
width={20}
height={20}
onMouseDown={() => {
if (!editable) return;
setEditingLink({
linkId: link.id,
fieldId: source.fieldId,
});
}}
onContextMenu={handlerContextMenu}
>
<div
style={{
cursor: editable ? 'pointer' : 'default',
userSelect: 'none',
}}
className="path-label"
>
{source.relation}
</div>
</foreignObject>
<foreignObject
x={foreignObjectPositions[1].x}
y={foreignObjectPositions[1].y}
width={20}
height={20}
onMouseDown={() => {
if (!editable) return;
setEditingLink({
linkId: link.id,
fieldId: target.fieldId,
});
}}
onContextMenu={handlerContextMenu}
>
<div
style={{
cursor: editable ? 'pointer' : 'default',
userSelect: 'none',
}}
className="path-label"
>
{target.relation}
</div>
</foreignObject>
</>
);
}
import { Space, Button, Switch, Notification } from '@arco-design/web-react';
import { IconSunFill, IconMoonFill, IconThunderbolt, IconRobot } from '@arco-design/web-react/icon';
import Link from 'next/link';
import graphState from '../hooks/use-graph-state';
import { Dropdown } from '@arco-design/web-react';
import { Menu } from '@arco-design/web-react';
import { useTranslation } from 'react-i18next';
import SwitchLang from './switchI18n';
import AI from './AITool';
import { useMemo, useState } from 'react';
import { map, uniqueId } from 'lodash';
import { useRouter } from 'next/router';
import { ADD_SCHEMA } from '@/data/prompt';
/**
* It renders a nav bar with a link to the home page, a button to add a new graph, and a dropdown menu
* with a list of import options
* @param props - the props passed to the component
* @returns List Nav component
*/
export default function ListNav({
importGraph,
addGraph,
customNode,
addExample,
importDBML,
handlerImportGraph,
}) {
const { theme, setTheme } = graphState.useContainer();
const { t } = useTranslation('ListNav');
const router = useRouter();
const custom = customNode
? customNode
: [
<Dropdown
key={0}
trigger={'hover'}
droplist={
<Menu>
<Menu.Item key="3" onClick={() => importGraph(3)}>
{t('Import Database')}
</Menu.Item>
<Menu.Item key="0" onClick={() => importGraph(1)}>
{t('Import DBML')}
</Menu.Item>
<Menu.Item key="2" onClick={() => importGraph(2)}>
{t('Import DDL')}
</Menu.Item>
<Menu.Item key="1" onClick={() => setSimpleModeVisible(true)}>
{/* <span className=" text-lime-600 mr-[10px]">
<IconRobot />
</span> */}
{t('Created by AI')}
</Menu.Item>
<Menu.Item key="1" onClick={() => addGraph()}>
{t('Create from Blank')}
</Menu.Item>
</Menu>
}
>
<Button type="primary" shape="round" className="shadow">
+ {t('New Model')}
</Button>
</Dropdown>,
<Button
key={1}
size="small"
shape="round"
className="shadow"
onClick={() => addExample()}
>
{t('Import Example')}
</Button>,
];
const [messageList, setMessageList] = useState([
{
role: 'system',
content:
'IMPRTANT: You are a virtual assistant powered by the gpt-3.5-turbo model, now time is 2023/5/30 16:57:14}',
},
{
role: 'user',
content: ADD_SCHEMA(),
},
{
role: 'assistant',
content: '好的,我明白了。请问你的业务是什么?',
},
]);
const [simpleModeVisible, setSimpleModeVisible] = useState(false);
return (
<div className="nav">
<AI
startView={3}
messageList={messageList}
setMessageList={() => false}
simpleMode
simpleModeVisible={simpleModeVisible}
setSimpleModeVisible={setSimpleModeVisible}
doneFx={message => {
const elNode = document.createElement('div');
elNode.innerHTML = message;
let sqlNodes = elNode.querySelectorAll('dbml');
let modelName = elNode.querySelector('modelName');
if (!sqlNodes.length) {
let str = message.replace(/```dbml/g, `<dbml>`);
str = str.replace(/```/g, `</sql>`);
elNode.innerHTML = str;
sqlNodes = elNode.querySelectorAll('dbml');
}
if (!sqlNodes[0]?.textContent) {
Notification.error({
title: t(
'AI was unable to understand your input, please further clarify your input.'
),
});
}
importDBML.current(sqlNodes[0]?.textContent, ({ tableDict, linkDict }) => {
handlerImportGraph({
tableDict,
linkDict,
name: `${modelName.outerText}`,
});
});
}}
// renderMessageItem={props => <MessageItem {...props} />}
/>
<div>
<Link href="/graphs" passHref>
<strong className="text-[22px] app-text mr-[10px]">CHAT QUERY</strong>
<span className="text-[var(--pc)]">
{t('Data Query Based on Data Model and AI')}
</span>
</Link>
</div>
<Space>
{custom}
<SwitchLang />
<Switch
className="shadow"
checkedIcon={<IconMoonFill />}
uncheckedIcon={<IconSunFill className="text-orange-500 " />}
checked={theme === 'dark'}
onChange={e => setTheme(e ? 'dark' : 'light')}
/>
</Space>
</div>
);
}
import { useState, useEffect } from 'react';
import { Drawer, Notification, Popconfirm, Space } from '@arco-design/web-react';
import { delLogs, getLogs } from '../data/db';
import graphState from '../hooks/use-graph-state';
import tableModel from '../hooks/table-model';
import { useTranslation } from 'react-i18next';
export default function LogsDrawer({ showDrawer, onCloseDrawer }) {
const { id, version } = graphState.useContainer();
const { applyVersion } = tableModel();
const [logs, setLogs] = useState(undefined);
const { t } = useTranslation();
const viewLogs = async () => {
const records = await getLogs(id);
setLogs(records);
};
useEffect(() => {
if (showDrawer === 'logs') {
viewLogs();
}
}, [showDrawer]);
const deleteLogs = (e, id) => {
e.preventDefault();
e.stopPropagation();
delLogs(id);
setLogs(state => state.filter(item => item.id !== id));
Notification.success({
title: 'Delete logs record success',
});
};
return (
<Drawer
width={320}
title="Logs Record"
visible={showDrawer === 'logs'}
autoFocus={false}
footer={null}
mask={false}
onCancel={() => onCloseDrawer()}
style={{ boxShadow: '0 0 8px rgba(0, 0, 0, 0.1)' }}
>
<Space
align="start"
className={`custom-radio-card ${
version === 'currentVersion' ? 'custom-radio-card-checked' : ''
}`}
onClick={() => applyVersion('currentVersion')}
>
<div className="custom-radio-card-dot"></div>
<div>
<div className="custom-radio-card-title">Current Version</div>
</div>
</Space>
{logs
? logs.map(item => (
<Space
key={item.updatedAt}
align="start"
className={`custom-radio-card ${
version === item.updatedAt ? 'custom-radio-card-checked' : ''
}`}
onClick={() => applyVersion(item)}
>
<div className="custom-radio-card-dot"></div>
<div>
<div className="custom-radio-card-title overflow-hidden text-ellipsis whitespace-nowrap w-[200px]">
Version {item.id}
</div>
<div className="custom-radio-card-secondary">
Auto save at {new Date(item.updatedAt).toLocaleString()}
</div>
</div>
<Popconfirm
position="br"
title={t('Are you sure you want to delete this logs record?')}
okText={t('Yes')}
cancelText={t('No')}
onOk={e => deleteLogs(e, item.id)}
onCancel={e => {
e.preventDefault();
e.stopPropagation();
}}
>
<a
className="delete-btn text-red-500 hover:text-red-800"
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}
>
[{t('Delete')}]
</a>
</Popconfirm>
</Space>
))
: null}
</Drawer>
);
}
This diff is collapsed.
import { useState } from 'react';
import { Select } from '@arco-design/web-react';
const Option = Select.Option;
/**
* It renders a hidden input with the value of the selected option, and a Select component from antd
* that allows the user to select an option from a list of options, or create a new option
* @returns A SelectInput component that takes in a name, options, defaultValue, and width.
*/
export default function SelectInput({ name, options, defaultValue, width }) {
const [value, setValue] = useState(defaultValue);
const handleChange = value => {
setValue(value);
};
return (
<>
<input type="hidden" name={name} value={value} />
<Select value={value} onChange={handleChange} style={{ width }} allowCreate>
{options.map(item => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
</>
);
}
import { Dropdown, Menu, Button, Space, Trigger } from '@arco-design/web-react';
import { IconLanguage } from '@arco-design/web-react/icon';
import { useTranslation } from 'react-i18next';
const DropList = () => {
const { i18n } = useTranslation();
return (
<Menu
onClickMenuItem={key => i18n.changeLanguage(key)}
style={{ marginBottom: -4 }}
mode="popButton"
tooltipProps={{ position: 'left' }}
hasCollapseButton
>
<Menu.Item key="zh" className="text-[12px]">
中文
</Menu.Item>
<Menu.Item key="en" className="text-[12px]">
en
</Menu.Item>
</Menu>
);
};
export default function SwitchLang() {
return (
<Trigger
popup={() => <DropList />}
trigger={['click', 'hover']}
clickToClose
position="top"
>
<div className={`button-trigger`}>
<Button type="primary" shape="round" size="small" className="shadow">
<IconLanguage />
</Button>
</div>
</Trigger>
);
}
This diff is collapsed.
This diff is collapsed.
import { useState, useEffect } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Button, Input, Menu, Tooltip } from '@arco-design/web-react';
import {
IconToLeft,
IconToRight,
IconSortAscending,
IconSortDescending,
IconFilter,
} from '@arco-design/web-react/icon';
import graphState from '../hooks/use-graph-state';
import { useTranslation } from 'react-i18next';
export default function TableNav({ onTableSelected, tableSelectedId, setTableSelectId }) {
const [collapsed, setCollapsed] = useState(false);
const [tableList, setTableList] = useState([]);
const [order, setOrder] = useState(1);
const [filterValue, setFilterValue] = useState('');
const [showFilter, setShowFilter] = useState(false);
const { tableList: tables } = graphState.useContainer();
const { t } = useTranslation('tablesNav');
useEffect(() => {
setTableList(tables);
return () => setTableList([]);
}, [tables]);
const handlerSort = () => {
if (!Array.isArray(tables) && !tables.length) return;
const newList = tables.sort((a, b) => {
const nameA = a.name.toUpperCase(); // ignore upper and lowercase
const nameB = b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return order;
}
if (nameA > nameB) {
return -order;
}
// names must be equal
return 0;
});
setTableList(newList);
setOrder(val => -val);
};
const handlerFilter = e => {
const { value } = e.target;
if (!value) {
setTableList(tables);
return;
}
const newList = tableList.filter(
item => item.name.toUpperCase().indexOf(value.toUpperCase()) > -1
);
setTableList(newList);
setFilterValue(value);
};
useHotkeys('ctrl+., meta+.', () => setCollapsed(val => !val), { preventDefault: true }, [
tables,
]);
return (
<div className="left-table-nav">
<div className="table-nav-title">
<div>
{t(' Tables')}
<span className="table-nav-count">({tableList.length})</span>
</div>
<div>
<Button
size="mini"
icon={!collapsed ? <IconToLeft /> : <IconToRight />}
onClick={() => setCollapsed(val => !val)}
/>
<Button
size="mini"
icon={order === 1 ? <IconSortAscending /> : <IconSortDescending />}
onClick={() => handlerSort()}
/>
<Button
size="mini"
className={filterValue ? 'table-filter' : ''}
icon={<IconFilter />}
onClick={() => setShowFilter(val => !val)}
/>
</div>
</div>
{!collapsed && tableList.length ? (
<div className="table-nav-content">
{showFilter && (
<div className="table-nav-search">
<Input
style={{ width: 180 }}
allowClear
defaultValue={filterValue}
placeholder={t('input table name')}
onPressEnter={e => handlerFilter(e)}
onClear={() => {
setTableList(tables);
setFilterValue('');
}}
// onChange={val => setFilterValue(val)}
/>
</div>
)}
<Menu
className="table-nav-menu"
autoScrollIntoView
selectedKeys={[tableSelectedId]}
>
{tableList.map(table => (
<Menu.Item
key={table.id}
onClick={() => onTableSelected(table)}
onMouseOver={() => setTableSelectId(table.id)}
>
<Tooltip position="right" content={table.note || ''}>
<div>
{table.name}
<span className="table-nav-count">
({table.fields.length})
</span>
</div>
</Tooltip>
</Menu.Item>
))}
</Menu>
</div>
) : null}
</div>
);
}
/* Creating a database called graphDB and creating a table called graphs. */
import { Notification } from '@arco-design/web-react';
import { diffJson } from 'diff';
import i18n from 'i18next';
import { backendApi } from '@/client/api';
import { get, omit } from 'lodash';
function getViewGraphs(v) {
return {
...get(v, 'graph', {}),
...omit(v, 'graph'),
};
}
export const getAllGraphs = async () => {
const { data } = await backendApi.get('/schema/all');
const graphs = data.map(v => {
return getViewGraphs(v);
});
return graphs;
};
export const getGraph = async id => {
const { data } = await backendApi.get(`/schema/${id}`);
return getViewGraphs(data);
};
export const saveGraph = async ({ id, name, tableDict, linkDict, box }) => {
const { t } = i18n;
try {
const data = await getGraph(id);
const logJson = {
tableDict: data.tableDict,
linkDict: data.linkDict,
name: data.name,
};
if (diffJson({ tableDict, linkDict, name }, logJson).length > 1) {
await backendApi.put(`/schema/${id}`, {
name: name,
graph: {
tableDict,
linkDict,
box,
name,
},
});
}
Notification.success({
title: t('Save success'),
});
} catch (e) {
Notification.error({
title: t('Save failed'),
});
}
};
export const delGraph = async id => {
return await backendApi.delete(`/schema/${id}`);
};
export const addGraph = async (graph = {}, id = null) => {
const { data } = await backendApi.post('/schema/create', {
name: graph.name,
graph: {
...graph,
box: {
x: 0,
y: 0,
w: global.innerWidth,
h: global.innerHeight,
clientW: global.innerWidth,
clientH: global.innerHeight,
},
},
});
return getViewGraphs(data).id;
};
export const getLogs = async id => {
const { data } = await backendApi.get(`/schema/getLogs/${id}`);
return data.map(v => getViewGraphs(v));
};
export const delLogs = id => backendApi.delete(`/schema/getLogs/${id}`);
import { dbAdaptor } from './settings';
const dbc = {
indexed: require('./adaptor/indexed'),
}[dbAdaptor];
export const getAllGraphs = async () => await dbc.getAllGraphs();
export const getGraph = async id => await dbc.getGraph(id);
export const saveGraph = async args => await dbc.saveGraph(args);
export const delGraph = async id => await dbc.delGraph(id);
export const addGraph = async (graph = {}, id = null) => await dbc.addGraph(graph, id);
export const getLogs = async id => await dbc.getLogs(id);
export const delLogs = async id => await dbc.delLogs(id);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
const fieldTypes = [
'INTEGER',
'SMALLINT',
'BIGINT',
'NUMERIC',
'FLOAT',
'DOUBLE',
'BOOLEAN',
'CHARACTER',
'VARCHAR',
'TEXT',
'DATE',
'TIME',
'TIMESTAMP',
'JSON',
'BLOB',
];
export default fieldTypes;
type dbml = string
export const GET_QUERY = (sql:dbml) =>`作为一个数据库模型业务分析专家,您需要根据当前数据库模型(DBML格式)生成对应的 MySQL 数据库可执行的 SQL 文本,并列出执行该 SQL 所需的变量及其解释,最后为此条查询命名。当前数据库模型为:
\`\`\`dbml
${sql}
\`\`\`
请您使用以下模版格式输出结果(注意模版中的标签和变量名称是必须存在的):
=======模版开始=======
根据您提供的数据库模型,已为您生成查询:
<sql> &=&sql&=& </sql>
执行所需变量:
判断解决当前问题的查询执行时是否需要变量。若条件成立,请使用:
<var>执行 SQL 所需变量名称</var>
<varDescription>变量解释</varDescription>
若不需要变量,则无需提供任何信息。
查询命名和描述:
<queryName> &=& 查询名称 &=& </queryName>
<queryDescription> &=& 查询描述 &=& </queryDescription>
=======模版结束=======
请您仅替换模版中的 &=& 和 &=& 之间的文字,变量命名请放在 $ 和 $ 之间,并且保持模版排版一致。例如:
根据您提供的数据库模型,已为您生成查询:
<sql>
SELECT $field$ FROM users where id=$id$;
INSERT INTO users (email, name) VALUES ($email$, $name$);
</sql>
执行所需变量:
<var> $field$ </var>: <varDescription> users 表中的某个字段。</varDescription>
查询命名和描述:
<queryName> 查用户和插入用户 </queryName>
<queryDescription> 先根据用户id查出用户,然后插入一个用户。 </queryDescription>
`
export const GET_SCHEMA_INFO = (sql:dbml) =>`作为一个数据模型业务分析专家,您需要根据当前数据模型(DBML格式)进行精确的分析,根据模型的提供的信息,分析该模型并输出简要描述。当前数据库模型为:
<dbml>
${sql}
</dbml>
`
export const ADD_TABLE = (sql:dbml)=>`作为一位资深的业务分析的专家,你需要结合当前数据库模型(DBML格式)并精确地分析我需要在当前数据库模型新增加的业务需求,向当前数据库模型添加(DBML格式)新表或者修改表字段之间的关联关系。当前数据库模型为:
<dbml>
${sql}
</dbml>
请您使用以下模版格式输出结果 (输出格式:XML,注意:XMl标签需要保留。dbml文本为新的模型)
=======模版开始=======
根据您提供的业务需求和数据库模型,已为您生成新的DBML数据库模型:
<dbml>
{{dbml文本}}
</dbml>
已经为你生成:<tableName>{{表名}}</tableName> <tableName>{{表名}}</tableName>
<field>{{表名}}:{{字段}}</field>:<description>{{表的字段描述}}</description>
=======模版结束=======
请您仅替换模版中的{{和}}之间的文字,并且保持模版排版一致。例如下面这个输出案例:
根据您提供的业务需求和数据库模型,已为您生成新的DBML数据库模型:
<dbml>Table ecommerce.merchants {
id int
country_code int
merchant_name varchar
"created at" varchar
admin_id int [ref: > U.id]
}
// If schema name is omitted, it will default to "public" schema.
Table users as U {
id int [pk, increment] // auto-increment
full_name varchar
}</dbml>
已经为你生成:<tableName>users</tableName>
<field>product_tags.id</field>:<description>主键</description>
<field>full_name</field>:<description>姓名</description>`
export const ADD_SCHEMA=()=>`你作为一个擅长业务分析的数据库模型建表专家,您需要分析我的业务需求并且输出 DBML格式 的数据库模型,添加一个或多个表和新增的表字段之间的关联关系。
请您使用以下模版格式输出结果 (输出格式:XML,注意:XMl标签需要保留)
=======模版开始=======
根据您提供的业务需求和数据库模型,已为您生成新的DBML数据库模型:
<modelName>该模型名称</modelName>
<dbml>{{dbml文本}}</dbml>
已经为你生成:<tableName>{{表名}}</tableName> <tableName>{{表名}}</tableName>
=======模版结束=======
请您仅替换模版中的{{和}}之间的文字,并且保持模版排版一致。例如输出:
根据您提供的业务需求和数据库模型,已为您生成新的DBML数据库模型:
<modelName>电子商务</modelName>
<dbml>Table ecommerce.merchants {
id int
country_code int
merchant_name varchar
"created at" varchar
admin_id int [ref: > U.id]
}
// If schema name is omitted, it will default to "public" schema.
Table users as U {
id int [pk, increment] // auto-increment
full_name varchar
created_at timestamp
country_code int
}</dbml>
已经为你生成:<tableName>merchants</tableName> <tableName>users</tableName>
`
\ No newline at end of file
export const CLEAR_ALL_TABLES_DATA = "重置表中所有数据,包括重置主键"
export const QUERY_ALL_DATA = "查询所有表中的数据"
export const LIST_ALL_TABLES = "查出所有的表"
export const CHINESE_DATA_POPULATOR = "分析模型表之间的关联关系,在数据库每个表中插入数量不等的生产环境标准的中文模拟数据"
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment