Commit 97f2750d authored by jaden's avatar jaden 🏄

Initial commit

parents
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# next.js
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
{
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/rules-of-hooks": "off", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "off",// Checks effect dependencies
}
}
name: Build and Push Docker Image
on:
push:
branches:
- fusionTeach
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: jadenxiong/chat-query:fusionTeach
build-args: |
NEXT_PUBLIC_BACKEND_URL=${{ secrets.NEXT_PUBLIC_BACKEND_URL }}
NEXT_PUBLIC_OPEN_AI_API_KEY=${{ secrets.NEXT_PUBLIC_OPEN_AI_API_KEY }}
OPENAI_PROXY_URL=${{ secrets.OPENAI_PROXY_URL }}
# 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
public/service-worker.js
xflow-docs
draw-app
packages
react-live-runner-swc
**/*.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 技术,通过自然语言实现数据查询。
## 演示
> [在线预览](https://query.fusiontech.cn/)
> [demo gif](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 调用。
- BI分析、Function 调用、react代码沙箱、支持导入npm包。
## 应用场景🎬
+ 从低代码到零代码开发。
+ 非业务人员快速进行数据分析。
+ 更多应用场景待探索...
## 部署
+ docker compose
```shell
NEXT_PUBLIC_OPEN_AI_API_KEY="sk-..." docker-compose up
```
+ chat-query和chat-query-backend都是支持docker构建和提供docker镜像.
## 开发环境设置
> 👏 欢迎参与 Chat-Query 的建设。
+ 后端:
```js
pnpm install
pnpm start:dev
```
- 在 .env 文件中添加
```shell
#openai
OPEN_AI_API_KEY='sk-...'
MODEL_NAME="gpt-3.5-turbo-16k-0613"
BASE_URL='https://open-ai-xyy.deno.dev/v1'
# database
DB_HOST="139.198.179.193"
DB_PORT=32094
```
+ 前端
```js
pnpm install
pnpm dev
```
- 在 .env 文件中添加
```shell
NEXT_PUBLIC_OPEN_AI_API_KEY='sk-...'
OPENAI_PROXY_URL="https://open-ai-xyy.deno.dev/"
NEXT_PUBLIC_BACKEND_URL="http://localhost:3001/"
```
## 系统架构
![架构](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)
# CHAT-QUERY
[zh](./README-zh.md)
> Chat-Query is a data query implementation based on metadata models and AI technology through natural language.
## Demo
> [Online Preview](https://chat-query.vercel.app/)
> [demo gif](https://cdn.glitch.me/fd139a45-4a65-41b6-9634-41617ab20cdc/%E6%BC%94%E7%A4%BA.gif?v=1686907695067)
+ **Features🐂:**
- Supports DDL, DBML and database reverse parsing import, AI automatically generates business models.
- Provides basic CRUD functions of business models, AI intelligent analysis, supports model export to DDL, DBML and synchronization with the database.
- Combines models and AI to implement natural language data queries, can be added to the query list and called via API.
- BI analysis, Function call, react code sandbox, supports importing npm packages.
## Application Scenarios🎬
+ From low-code to no-code development.
+ Non-business personnel can quickly perform data analysis.
+ More application scenarios to be explored...
## Deployment
+ docker compose
```shell
NEXT_PUBLIC_OPEN_AI_API_KEY="sk-..." docker-compose up
```
+ Both chat-query and chat-query-backend support docker build and provide docker images.
## Development Environment Setup
> 👏 Welcome to participate in the construction of Chat-Query.
+ Backend:
```js
pnpm install
pnpm start:dev
```
- Add in .env file
```shell
#openai
OPEN_AI_API_KEY='sk-...'
MODEL_NAME="gpt-3.5-turbo-16k-0613"
BASE_URL='https://open-ai-xyy.deno.dev/v1'
# database
DB_HOST="139.198.179.193"
DB_PORT=32094
```
+ Frontend
```js
pnpm install
pnpm dev
```
- Add in .env file
```shell
NEXT_PUBLIC_OPEN_AI_API_KEY='sk-...'
OPENAI_PROXY_URL="https://open-ai-xyy.deno.dev/"
NEXT_PUBLIC_BACKEND_URL="http://localhost:3001/"
```
## 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 '.';
export type DBInfo = {
host: string;
port: number;
user: string;
password: string;
database: string;
client: 'mysql2';
name?: string;
};
export interface Query {
schemaId: string;
DbID: string;
name: string;
content: {
executions: {
content: string;
type: string;
};
params: Record<string, any>;
info: { queryDescription: string; queryName: string };
functions?: string;
filed: 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: Query) {
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 });
}
static updateQuery(queryId: string, functions: string) {
return backendApi.put(`/query/${queryId}`, {
functions,
});
}
}
import getConfig from 'next/config';
import APi, { backendApi } from '.';
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 },
} = getConfig();
export interface schemaParams {
type: 'schema' | 'table';
name?: string;
}
export default class getSchema {
static getTableList(config: schemaParams) {
return APi.post(apiPath + 'getSchema', config);
}
}
import { ChatMessage } from './../../components/AITool/index';
import APi, { backendApi } from '.';
import { get, isArray, max, min, pick } from 'lodash';
import { Options } from 'redaxios';
import OpenAI from './openAI';
export interface View {
type: 'schema' | 'table';
name?: string;
}
const privateConfig: Options & {
timeout: number;
} = {
timeout: 1000 * 60,
};
function extractCodeBlocks(markdownText: string, lang = null) {
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
const codeBlocks = [];
let match;
while ((match = codeBlockRegex.exec(markdownText)) !== null) {
if (lang === null || match[1] === lang) {
codeBlocks.push(match[2]);
}
}
return codeBlocks;
}
export default class getView {
static getViewComponent(params: { props: Record<string, any>; need: string }) {
return new Promise((res, rej) => {
OpenAI.request(
[
{
role: 'user',
content: `我正在使用react-live作为一个实时编辑和编译React组件的工具。请根据我的需求,输出一段能在react-live运行的代码,只需要导出默认组件,不用挂载dom。可用的作用域如下:
{
data,
import: {
'styled-components':{
'描述':'A css in js Framework,Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress'
},
'echarts':{
'描述':'Apache ECharts is a powerful, interactive charting and data visualization library for browser.',
'用法': "import echarts from 'echarts';"
},
'echarts-for-react':{
'描述':'Apache ECharts is a powerful, interactive charting and data visualization library for browser.',
'用法': "import React from 'react';
import ReactECharts from 'echarts-for-react'; // or var ReactECharts = require('echarts-for-react');
<ReactECharts
option={this.getOption()}
notMerge={true}
lazyUpdate={true}
theme={"theme_name"}
onChartReady={this.onChartReadyCallback}
onEvents={EventsDict}
opts={}
/>"
},
},
}
其中import可供导入的库,除此之外不能使用其它第三方库。 key为包名,value 是包的描述。其它每个属性将作为单独全局变量,变量名为属性名。请确保默认导出的组件接受null作为参数,并将传入props取自模块全局作用域变量data(const props = data),全局变量变量data会自动注入,请不要进行声明。例如:
export default App() {
const props = data;
...
}
`,
},
{
role: 'assistant',
content: `好的我明白啦,请问你的业务是什么?`,
},
{
role: 'user',
content: `这是我传入的props:${JSON.stringify(
params.props.slice(0, min([params.props.length, 4]))
)}${params.need}`,
},
],
(responseText: string, cancel?: boolean) => {
let code;
try {
code = JSON.parse(get(JSON.parse(responseText), 'arguments')).reactCode;
} catch (e) {}
res({
data: {
code: code,
},
});
},
undefined,
undefined,
[
{
name: 'runReactCode',
description: '作为一个实时编译React组件代码的函数。',
parameters: {
type: 'object',
properties: {
reactCode: {
type: 'string',
description: '输出一段能在react-live运行的代码',
},
},
required: ['sql', 'queryName', 'queryDescription', 'simulation'],
},
},
],
undefined,
{ name: 'runReactCode' },
false
);
});
// return backendApi.post(
// '/openAi/api/reactLive',
// isArray(params.props)
// ? {
// ...params,
// props: params.props.slice(0, min([params.props.length, 4])),
// }
// : params,
// privateConfig
// );
}
static getViewFunction(params: { data: Record<string, any>; need: string }) {
return backendApi.post(
'/openAi/api/code',
isArray(params.data)
? {
...params,
data: params.data.slice(0, min([params.data.length, 4])),
}
: params,
privateConfig
);
}
static checkChatResult(messageList: ChatMessage[]) {
return backendApi.post('/openAi/api/checkQuery', { messageList }, privateConfig);
}
}
'use client';
import { set } from 'lodash';
import axios from 'redaxios';
const APi = axios.create({});
export const backendApi = axios.create({
baseURL:globalThis?.sessionStorage?.getItem("baseURL") || '/backend',
});
(() => {
if (globalThis.sessionStorage && !sessionStorage.getItem("baseURL")) {
const url = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:3001'
axios.get((url) + "/schema/all").then(
() => {
set(backendApi.defaults, "baseURL", url)
sessionStorage.setItem("baseURL", url)
},
err => {
console.log(err);
sessionStorage.setItem("baseURL",'/backend')
}
);
}
})();
export default APi;
import getConfig from 'next/config';
import { backendApi } from '.';
import { getModel } from '@/utils/gpt';
import { fetchEventSource, EventStreamContentType } from '@microsoft/fetch-event-source';
import { MutableRefObject, SetStateAction } from 'react';
import { functions, get } from 'lodash';
export interface View {
type: 'schema' | 'table';
name?: string;
}
const model = getModel(process.env.NEXT_PUBLIC_MODEL_NAME || 'gpt-4o-mini');
export function prettyObject(msg: any) {
const obj = msg;
if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, ' ');
}
if (msg === '{}') {
return obj.toString();
}
if (msg.startsWith('```json')) {
return msg;
}
return ['```json', msg, '```'].join('\n');
}
type message = {
role: string;
content: string;
};
export default class OpenAI {
static async request(
messages: message[],
onFinish: (responseText: string, cancel?: boolean) => any,
onUpdate?: (responseText: string, delta: string) => any,
onError?: (e: Error) => any,
functions?: any[],
closeFn?: MutableRefObject<any>,
function_call?: any,
stream: boolean = true
) {
const requestPayload = {
messages: messages,
model: model.name,
temperature: model.temperature,
frequency_penalty: model.frequency_penalty,
presence_penalty: model.presence_penalty,
stream,
functions,
function_call,
};
const controller = new AbortController();
const chatPayload = {
method: 'POST',
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: {
'cache-control': 'no-cache',
'Content-Type': 'application/json',
'x-requested-with': 'XMLHttpRequest',
Accept: 'text/event-stream',
},
};
const requestTimeoutId = setTimeout(() => controller.abort(), 1000 * 120);
const chatPath = '/openai/v1/chat/completions';
if (closeFn) {
controller.signal.onabort = () => {
onFinish('', true);
};
closeFn.current = () => {
clearTimeout(requestTimeoutId);
controller.abort();
};
}
if (stream) {
let responseText = '';
let finished = false;
const finish = (cancel?: boolean) => {
if (!finished) {
onFinish(responseText, cancel);
finished = true;
}
};
controller.signal.onabort = () => {
finish(true);
};
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get('content-type');
// console.log('[OpenAI] request response content type: ', contentType);
if (contentType?.startsWith('text/plain')) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers.get('content-type')?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join('\n\n');
return finish();
}
},
onmessage(msg) {
if (msg.data === '[DONE]' || finished) {
return finish();
}
const text = msg.data;
try {
// console.log(text);
const json = JSON.parse(text);
const delta = json.choices[0].delta.content;
if (json.choices[0].finish_reason === 'function_call') {
// 🌟 output the Tree structure data
console.log(json);
}
if (delta) {
responseText += delta;
onUpdate?.(responseText, delta);
}
} catch (e) {
console.error('[Request] parse error', text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message =
resJson.choices?.at(0)?.message?.content ??
(JSON.stringify(get(resJson, 'choices[0].message.function_call')) || '');
onFinish(message);
}
}
}
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';
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;
}
import { Upload, Message, UploadProps } from '@arco-design/web-react';
import * as XLSX from 'xlsx';
import { useState } from 'react';
import pinyin from 'pinyin';
import { flatten, get, isDate, isString, map } from 'lodash';
const e = /^\s+$/g;
function getDataType(value: string) {
if (!value || (isString(value) && e.test(value))) {
console.log(value, 'value');
return 'VARCHAR(255)';
} else if (isDate(value) && !isNaN(Date.parse(value))) {
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return 'DATE';
} else {
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d{1,3})?$/.test(value)) {
return 'DATETIME';
}
return 'VARCHAR(255)';
}
} else if (isFinite(Number(value))) {
const number = Number(value);
if (Number.isInteger(number)) {
if (number >= -2147483648 && number <= 2147483647) {
return 'INT';
} else {
return 'BIGINT';
}
} else {
if (number >= -9999999 && number <= 9999999) {
return 'DECIMAL(10, 2)';
} else {
return 'DECIMAL(20, 2)';
}
}
} else if (value === 'true' || value === 'false') {
return 'BOOLEAN';
} else {
if (value.length <= 255) {
return 'VARCHAR(255)';
} else {
return 'TEXT';
}
}
}
function isChinese(text: string) {
var re = /[\u4E00-\u9FA5\uF900-\uFA2D]/;
return re.test(text);
}
export function handleUpload(files: File[]) {
return new Promise(res => {
const ddlArr: string[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const reader = new FileReader();
reader.onload = (e: any) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {
type: 'array',
cellDates: true,
cellNF: true,
});
const sheetName = workbook.SheetNames[0];
const jsonData = XLSX.utils.sheet_to_json<any>(workbook.Sheets[sheetName], {
defval: '',
header: 1,
});
let tableName = file.name.split('.')[0];
if (isChinese(tableName)) {
tableName = flatten(
pinyin(tableName, {
style: 'normal',
})
).join('_');
}
const columns: any[] = jsonData[0];
const columnTypes = map(columns, (_, column) => {
const firstNonNullValue = get(
jsonData.slice(1).find(row => row[column] != null),
[column],
'VARCHAR(255)'
);
return getDataType(firstNonNullValue);
});
const ddl = `CREATE TABLE \`${tableName}\` (${map(
columns,
(column, i) => `\`${column}\` ${columnTypes[i]}`
).join(', ')});`;
ddlArr.push(ddl);
if (ddlArr.length === files.length) {
res(ddlArr);
}
};
reader.readAsArrayBuffer((file as File & { originFile: File })?.originFile);
}
});
}
const ImportExcel = ({ setList }: { setList: typeof useState<UploadProps['fileList']> }) => {
return (
<Upload
drag
multiple
accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
onDrop={e => {
let uploadFile = e.dataTransfer.files;
}}
autoUpload={false}
onChange={lists => setList(lists)}
/>
);
};
export default ImportExcel;
This diff is collapsed.
import { Graph, Path } from '@antv/x6';
export default function ConnectorInit() {
Graph.registerConnector(
'algo-connector',
(s, e) => {
const offset = 4;
const deltaY = Math.abs(e.y - s.y);
const control = Math.floor((deltaY / 3) * 2);
const v1 = { x: s.x, y: s.y + offset + control };
const v2 = { x: e.x, y: e.y - offset - control };
return Path.normalize(
`M ${s.x} ${s.y}
L ${s.x} ${s.y + offset}
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
L ${e.x} ${e.y}
`
);
},
true
);
}
import { Cell } from '@antv/x6';
import { ReactShapeConfig } from '@antv/x6-react-shape';
import { type } from 'os';
import React from 'react';
interface DagContext {
activeCell?: Cell;
}
export enum DagActions {
'CHANGE_ACTIVE_CELL',
}
let count = 1;
type dispatchType<T> = (type: T) => any;
type ExtractProps<T> = T extends React.ComponentType<infer P> ? P : never;
type NodeProps = ReactShapeConfig['component'];
export type CustomProps = ExtractProps<NodeProps>;
export type DagContextType = React.Context<
DagContext & {
dispatch: (actions: {
type: DagActions;
payload: (typeof actions)['type'] extends DagActions.CHANGE_ACTIVE_CELL ? Cell : never;
}) => any;
}
>;
export default function getDagContext() {
const DagContext = React.createContext<DagContext>({});
DagContext.displayName = 'DagContext' + count;
const { Provider, Consumer, displayName } = DagContext;
console.log(count, 'countcount');
count++;
return { Provider, Consumer, displayName, DagContext };
}
import { Graph } from '@antv/x6';
export default function EdgesInit(){
Graph.registerEdge(
'dag-edge',
{
inherit: 'edge',
attrs: {
line: {
stroke: '#C2C8D5',
strokeWidth: 1,
targetMarker: null,
},
},
},
true
);
}
\ No newline at end of file
'use client';
import React, {
useEffect,
useRef,
useCallback,
useReducer,
type Provider as ReactProvider,
useMemo,
useState,
} from 'react';
import { Graph, Path, Cell } from '@antv/x6';
import { Selection } from '@antv/x6-plugin-selection';
// import insertCss from 'insert-css';
import { History } from '@antv/x6-plugin-history';
import { DagreLayout } from '@antv/layout';
import styled from 'styled-components';
import { Button, Collapse, Layout, List } from '@arco-design/web-react';
import { IconLayout } from '@arco-design/web-react/icon';
import { Dnd } from '@antv/x6-plugin-dnd';
import { MiniMap } from '@antv/x6-plugin-minimap';
import nodeStatusList from './nodeStatus';
import { NodeStatus, NodeType } from './nodes/AlgoNode';
import ConnectorInit from './connectors';
import EdgesInit from './edges';
import nodesInit from './nodes';
import { Div } from './styleWidgets';
import { SlideForm } from './nodeConfigs';
import getDagContext, { DagActions } from './context';
import { produce } from 'immer';
import { set, uniqueId } from 'lodash';
import { Portal } from '@antv/x6-react-shape';
const CollapseItem = Collapse.Item;
//注册
ConnectorInit();
EdgesInit();
nodesInit();
const Sider = Layout.Sider;
const Header = Layout.Header;
const Content = Layout.Content;
const X6ReactPortalProvider = Portal.getProvider();
function A() {
console.log('A');
return <div>A</div>;
}
function useFirst(fn: typeof getDagContext) {
const flag = useRef<ReturnType<typeof fn>>();
if (!flag.current) {
flag.current = fn();
}
return flag.current;
}
export default function Demo() {
const el = useRef<HTMLDivElement>(null);
const graphEl = useRef<Graph | null>();
const dndEl = useRef<Dnd | null>();
const context = useFirst(getDagContext);
const graph = graphEl.current;
const dnd = dndEl.current;
const { Provider, DagContext } = context;
console.log(DagContext, 'DagContext');
const [state, dispatch] = useReducer(
produce(
(
state: typeof Provider extends ReactProvider<infer P> ? P : never,
action: {
type: DagActions;
payload: (typeof action)['type'] extends DagActions.CHANGE_ACTIVE_CELL ? Cell : never;
}
) => {
switch (action.type) {
case DagActions.CHANGE_ACTIVE_CELL: {
console.info(action);
state.activeCell = action.payload;
}
}
}
),
{}
);
const store = useMemo(() => {
return {
...state,
dispatch: dispatch,
};
}, [state]);
console.log(store);
useEffect(() => {
if (el) {
const w = (el?.current?.clientWidth || 400) - 400;
const h = el?.current?.clientHeight;
const graph: Graph & {
DagContext?: typeof DagContext;
} = (graphEl.current = new Graph({
container: el.current!,
width: w,
height: 418,
panning: {
enabled: true,
eventTypes: ['leftMouseDown', 'mouseWheel'],
},
mousewheel: {
enabled: true,
modifiers: 'ctrl',
factor: 1.1,
maxScale: 1.5,
minScale: 0.5,
},
highlighting: {
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#fff',
stroke: '#31d0c6',
strokeWidth: 4,
},
},
},
},
connecting: {
snap: true,
allowBlank: false,
allowLoop: false,
highlight: true,
connector: 'algo-connector',
connectionPoint: 'anchor',
anchor: 'center',
validateMagnet({ magnet }) {
return magnet.getAttribute('port-group') !== 'top';
},
createEdge() {
return graph.createEdge({
shape: 'dag-edge',
attrs: {
line: {
strokeDasharray: '5 5',
},
},
zIndex: -1,
});
},
},
} as any));
graph.DagContext = DagContext;
const dnd = (dndEl.current = new Dnd({
target: graph,
}));
// graph.use(
// new MiniMap({
// container: document.getElementById('minimap'),
// }),
// )
graph.use(
new Selection({
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
})
);
graph.use(
new History({
enabled: true,
})
);
graph.on('edge:connected', ({ edge }) => {
edge.attr({
line: {
strokeDasharray: '',
},
});
});
graph.on('node:change:data', ({ node }) => {
const edges = graph.getIncomingEdges(node);
const { status } = node.getData() as NodeStatus;
edges?.forEach(edge => {
if (status === 'running') {
edge.attr('line/strokeDasharray', 5);
edge.attr('line/style/animation', 'running-line 30s infinite linear');
} else {
edge.attr('line/strokeDasharray', '');
edge.attr('line/style/animation', '');
}
});
});
graph.on('node:contextmenu', ({ node, view }) => {
dispatch({
type: DagActions.CHANGE_ACTIVE_CELL,
payload: node,
});
});
// 初始化节点/边
const init = (data: Cell.Metadata[]) => {
const cells: Cell[] = [];
data.forEach(item => {
cells.push(graph.createNode(item));
});
graph.resetCells(cells);
};
init([
{
id: 'flow' + '_' + Date.now(),
shape: 'dag-node',
x: 290,
y: 110,
data: {
label: '流程开始',
status: 'success',
nodeType: NodeType.FLOW_START,
},
ports: [],
},
]);
graph.centerContent();
return () => {
graph.dispose();
};
}
}, []);
const startDrag = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
// 该 node 为拖拽的节点,默认也是放置到画布上的节点,可以自定义任何属性
if (graph && dnd) {
const node = graph.createNode({
shape: 'rect',
width: 100,
height: 40,
});
dnd.start(node, e.nativeEvent);
}
};
return (
<Div className="react-portal-app">
<Layout className="h-[500px]">
<Header className="h-[40px] flex pl-[200px] bg-slate-50 border-b border-b-gray-200 relative z-[1] bg-clip-content">
<div className="px-[10px]">
<Button
iconOnly
icon={<IconLayout />}
onClick={() => {
// const dagreLayout = new DagreLayout({
// type: 'dagre',
// rankdir: 'LR',
// align: 'UR',
// ranksep: 35,
// nodesep: 15,
// });
// const model = dagreLayout.layout({
// nodes: graphEl.current?.getNodes().map((v,i) => {
// return {
// id: `${v.id}`,
// shape: 'circle',
// width: 32,
// height: 32,
// label: i,
// attrs: {
// body: {
// fill: '#5F95FF',
// stroke: 'transparent',
// },
// label: {
// fill: '#ffffff',
// },
// },
// };
// }),
// edges: graphEl.current?.getEdges(),
// });
// console.log(model);
// graphEl.current?.fromJSON(model);
}}
/>
</div>
</Header>
<Layout>
<Sider className="bg-slate-100 mt-[-40px] border-r border-r-gray-200 pt-[40px]">
<Collapse bordered={false} defaultActiveKey={['1']}>
<CollapseItem header="Query" name="1">
<List
style={{ width: '100%' }}
size="small"
header={null}
dataSource={['查询1', '查询2']}
render={(item, index) => (
<div className="m-[10px] text-center bg-slate-300 p-[5px] rounded cursor-pointer text-purple-400 font-bold shadow">
<span className="w-full" key={index}>
{item}
</span>
</div>
)}
/>
</CollapseItem>
</Collapse>
</Sider>
<Content>
<Provider value={store}>
<X6ReactPortalProvider />
</Provider>
<div ref={el}></div>
</Content>
<Sider
className="bg-slate-100 mt-[-40px] border-l border-l-gray-200 pt-[40px]"
collapsible
collapsedWidth={0}
>
<SlideForm />
</Sider>
</Layout>
</Layout>
</Div>
);
}
import { Button, SchemaForm } from 'formily-arco';
import { useCallback, useRef } from 'react';
const schema = {
type: 'object',
properties: {
select: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-reactions': '{{useAsyncDataSource(loadData)}}',
},
},
}
const scope = {
async loadData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
label: 'CCC',
value: 'ccc',
},
{
label: 'DDD',
value: 'ddd',
},
])
}, 1000)
})
},
}
export function SlideForm (){
const formRef = useRef<any>()
const handleClick = useCallback(() => {
console.log(formRef?.current?.getForm()?.values)
}, [])
return (
<div className="p-[10px] bg-slate-50 max-h-full h-full">
<SchemaForm ref={formRef} schema={schema} scope={scope} />
<Button onClick={handleClick}>点击测试</Button>
</div>
)
}
\ No newline at end of file
const nodeStatusList = [
[
{
id: '1',
status: 'running',
},
{
id: '2',
status: 'default',
},
{
id: '3',
status: 'default',
},
{
id: '4',
status: 'default',
},
],
[
{
id: '1',
status: 'success',
},
{
id: '2',
status: 'running',
},
{
id: '3',
status: 'default',
},
{
id: '4',
status: 'default',
},
],
[
{
id: '1',
status: 'success',
},
{
id: '2',
status: 'success',
},
{
id: '3',
status: 'running',
},
{
id: '4',
status: 'running',
},
],
[
{
id: '1',
status: 'success',
},
{
id: '2',
status: 'success',
},
{
id: '3',
status: 'success',
},
{
id: '4',
status: 'failed',
},
],
];
export default nodeStatusList;
\ No newline at end of file
import { get, map, uniqueId } from 'lodash';
import { useContext, useEffect, useMemo, useState } from 'react';
import { CustomProps, DagContextType } from '../context';
import {
Button,
Dropdown,
Form,
Input,
Menu,
Modal,
Popover,
Select,
Trigger,
} from '@arco-design/web-react';
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
import { Graph, Node } from '@antv/x6';
import { DagreLayout } from '@antv/layout';
export interface NodeStatus {
id: string;
status: 'default' | 'success' | 'failed' | 'running';
label?: string;
nodeType?: NodeType;
}
export enum NodeType {
'FLOW_START',
}
const image = {
logo: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*evDjT5vjkX0AAAAAAAAAAAAAARQnAQ',
success: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ',
failed: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ',
running: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*t8fURKfgSOgAAAAAAAAAAAAAARQnAQ',
};
function removeNodeAndConnected(node: Node, graph: Graph) {
// 获取与节点关联的所有边
const connectedEdges = graph.getConnectedEdges(node, { deep: true });
connectedEdges.forEach(edge => {
// 找出这个节点作为source的目标节点
if (edge.getSourceCell() === node) {
const target = edge.getTargetCell() as Node;
if (target) {
// 递归删除目标节点及其关联的节点和边
removeNodeAndConnected(target, graph);
}
}
// 删除边
graph.removeCell(edge);
});
// 删除节点
graph.removeCell(node);
}
const FormItem = Form.Item;
const Option = Select.Option;
const data = {
函数: ['Chengdu', 'Mianyang', 'Aba'],
'SQL 查询': ['Beijing', 'Shanghai', 'Tianjin'],
算子: ["Xi'an", "Xi'an", "Xi'an"],
};
function MenuList({ node, graph, nodeType }: { node: Node; graph: Graph; nodeType: NodeType }) {
const types = Object.keys(data) as Array<keyof typeof data>;
const defaultType = types[0];
const [type, setType] = useState<keyof typeof data>(defaultType);
const [cities, setCities] = useState<string[]>([]);
const [city, setCity] = useState('');
useEffect(() => {
const cities = data[type] || [];
setCities(cities);
setCity(cities[0]);
}, [type]);
return (
<div className="w-[150px] min-h-[50px]">
<Button
type="default"
className="cursor-pointer w-full text-left my-[5px]"
icon={<IconPlus />}
onClick={() => {
Modal.confirm({
title: '配置节点',
icon: null,
footer: null,
content: (
<div>
<Form autoComplete="off">
<FormItem label="函数类型">
<Select
placeholder="Select Province"
onChange={value => setType(value)}
defaultValue={type}
>
{map(data, (_, k) => {
console.log(k, 'kkk');
return (
<Option key={k} value={k}>
{k}
</Option>
);
})}
</Select>
</FormItem>
<FormItem label="函数名称">
<Select
placeholder="Select city"
onChange={value => setCity(value)}
value={city}
></Select>
</FormItem>
<FormItem wrapperCol={{ offset: 5 }}>
<Button type="primary">确定</Button>
</FormItem>
</Form>
</div>
),
});
const NeedPorts = [
{
id: node.id + 'port' + uniqueId(),
group: 'bottom',
},
{
id: node.id + 'port' + uniqueId(),
group: 'top',
},
];
const childNode = {
id: node.id + '_' + 'node' + uniqueId(),
shape: 'dag-node',
x: 0,
y: 0,
data: {
label: '读数据',
status: 'success',
},
ports: [NeedPorts[1]],
};
node.addPort(NeedPorts[0]);
setTimeout(() => {
graph.addNode(childNode);
graph.addEdge({
id: node.id + '_' + 'edge' + uniqueId(),
shape: 'dag-edge',
source: {
cell: node.id,
port: NeedPorts[0].id,
},
target: {
cell: childNode.id,
// port: NeedPorts[1].id,
},
zIndex: 0,
});
// 获取所有节点
const nodes = graph
.getNodes()
.map(node => ({ id: node.id, raw: node, ...node.size() }));
// 获取所有边
const edges = graph.getEdges().map(edge => ({
source: edge.getSourceCell()!.id,
target: edge.getTargetCell()!.id,
raw: edge,
}));
const dagreLayout = new DagreLayout({
type: 'dagre',
rankdir: 'TB',
ranksep: 30,
nodesep: 70,
});
const model = dagreLayout.layout({
nodes,
edges,
}) as {
nodes: ((typeof nodes)[0] & { x: number; y: number })[];
edges: typeof edges;
};
(model.nodes || []).forEach(({ raw, x, y }) => {
raw.position(x, y);
});
graph.centerContent();
}, 50);
}}
>
添加子节点
</Button>
<Button
hidden={nodeType === NodeType.FLOW_START}
type="default"
status="danger"
className="cursor-pointer w-full text-left my-[5px]"
icon={<IconDelete />}
onClick={() => {
removeNodeAndConnected(node, graph);
}}
>
删除
</Button>
</div>
);
}
const AlgoNode = (props: CustomProps) => {
const { node, graph } = props;
const data = node?.getData() as NodeStatus;
const { label, status = 'default', nodeType } = data;
const DagContext: DagContextType = get(graph, 'DagContext')!;
const { activeCell } = useContext(DagContext);
graph.removeConnectedEdges;
function render() {
if (nodeType === NodeType.FLOW_START) {
return (
<Button type="outline" className="w-full" shape="round" status="success">
流程开始
</Button>
);
} else {
return (
<div className={`node ${status}`}>
<img src={image.logo} alt="logo" />
<span className="label">{label}</span>
<span className="status">
{status === 'success' && <img src={image.success} alt="success" />}
{status === 'failed' && <img src={image.failed} alt="failed" />}
{status === 'running' && <img src={image.running} alt="running" />}
</span>
</div>
);
}
}
return (
<>
{render()}
<Popover
trigger="hover"
content={<MenuList node={node} graph={graph} nodeType={nodeType} />}
position="right"
title="操作"
>
<Button
hidden={activeCell?.id !== node?.id}
icon={<IconPlus />}
shape="circle"
iconOnly
type="primary"
className="inset-y-0 right-[-30px] m-auto absolute"
size="mini"
></Button>
</Popover>
</>
);
};
export default {
shape: 'dag-node',
width: 180,
height: 36,
component: AlgoNode,
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
},
},
};
import { Graph, Node } from '@antv/x6';
import { Button } from '@arco-design/web-react';
import { CustomProps } from '../context';
function StartNode(props: CustomProps) {
return <Button type="primary">流程开始</Button>;
}
export default {
shape: 'start-node',
width: 180,
height: 36,
component: StartNode,
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#C2C8D5',
strokeWidth: 1,
fill: '#fff',
},
},
},
},
},
};
import { register } from '@antv/x6-react-shape';
import AlgoNode from './AlgoNode';
export default function nodesInit() {
register(AlgoNode);
}
import styled from "styled-components";
export const Div = styled.div`
border: #c2c8d59d 1px solid;
margin: 5px;
.x6-graph {
height: 100% !important;
}
.node {
display: flex;
align-items: center;
width: 100%;
height: 100%;
background-color: #fff;
border: 1px solid #c2c8d5;
border-left: 4px solid #5f95ff;
border-radius: 4px;
box-shadow: 0 2px 5px 1px rgba(0, 0, 0, 0.06);
}
.node img {
width: 20px;
height: 20px;
flex-shrink: 0;
margin-left: 8px;
}
.node .label {
display: inline-block;
flex-shrink: 0;
width: 104px;
margin-left: 8px;
color: #666;
font-size: 12px;
}
.node .status {
flex-shrink: 0;
}
.node.success {
border-left: 4px solid #52c41a;
}
.node.failed {
border-left: 4px solid #ff4d4f;
}
.node.running .status img {
animation: spin 1s linear infinite;
}
.x6-node-selected .node {
border-color: #1890ff;
border-radius: 2px;
box-shadow: 0 0 0 4px #d4e8fe;
}
.x6-node-selected .node.success {
border-color: #52c41a;
border-radius: 2px;
box-shadow: 0 0 0 4px #ccecc0;
}
.x6-node-selected .node.failed {
border-color: #ff4d4f;
border-radius: 2px;
box-shadow: 0 0 0 4px #fedcdc;
}
.x6-edge:hover path:nth-child(2) {
stroke: #1890ff;
stroke-width: 1px;
}
.x6-edge-selected path:nth-child(2) {
stroke: #1890ff;
stroke-width: 1.5px !important;
}
.arco-collapse-item-content-box{
padding: 5px;
}
@keyframes running-line {
to {
stroke-dashoffset: -1000;
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`;
\ No newline at end of file
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}`,
});
},
undefined,
{
new: true,
importType: 'dbml',
}
);
}}
// 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.
This diff is collapsed.
version: '3'
services:
mysql:
image: mysql:8.4
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=123789
- MYSQL_DATABASE=chat_query
- MYSQL_USER=xyy
- MYSQL_PASSWORD=123789
volumes:
- ./out:/var/lib/mysql
nest:
image: jadenxiong/chat-query-backend:fusionTeach
restart: on-failure
environment:
- OPEN_AI_API_KEY=${OPEN_AI_API_KEY}
- MODEL_NAME=gpt-4o-mini
- BASE_URL=https://open-ai-xyy.deno.dev/v1
- DB_HOST=mysql
- DB_PORT=3306
- DB_PASSWORD=123789
- DB_USER=root
ports:
- "3001:3001"
depends_on:
- mysql
next:
image: jadenxiong/chat-query:fusionTeach
environment:
- NEXT_PUBLIC_BACKEND_URL=http://nest:3001/
- NEXT_PUBLIC_OPEN_AI_API_KEY=${OPEN_AI_API_KEY}
- OPENAI_PROXY_URL=https://open-ai-xyy.deno.dev/
- MODEL_NAME=gpt-4o-mini
ports:
- "3000:3000"
depends_on:
- nest
This diff is collapsed.
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;
This diff is collapsed.
This diff is collapsed.
// chat to query
export const CLEAR_ALL_TABLES_DATA = '重置表中所有数据,包括重置主键';
export const QUERY_ALL_DATA = '查询所有表中的数据';
export const LIST_ALL_TABLES = '查出所有的表';
export const CHINESE_DATA_POPULATOR =
'分析模型表之间的关联关系,不提供参数的情况下,在数据库每个表中插入数量不等的生产环境标准的中文模拟数据';
// chat to view
export const chatToView = ['表格', '图表', '卡片', '表单', '列表'];
export const themes = [
['#CB2E34', '#eb0aa4', '#CC961F', '#00b42a'],
['#CFB325', '#84B723', '#D26913', '#ff7d00'],
['#2971CF', '#B01BB6', '#0fc6c2', '#ffb400'],
];
export const tableWidth = 240;
export const tableMarginLeft = 48;
export const tableMarginTop = 48;
export const tableRowNumbers = 5;
export const titleHeight = 40;
export const commentHeight = 24;
export const fieldHeight = 32;
export const dbAdaptor = 'indexed';
import { Application, proxy } from 'https://deno.land/x/oak/mod.ts';
const app = new Application();
app.use(proxy('https://api.openai.com'));
app.listen({ port: 8000 });
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.
module.exports = {
plugins: {
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
};
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