├── .DS_Store
├── LICENSE
├── README.md
├── README_EN.md
└── chat-sql
├── .gitignore
├── eslint.config.mjs
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── assets
│ ├── chat.m4v
│ ├── dify.png
│ ├── edit.gif
│ ├── initialization.png
│ ├── landscape.jpg
│ ├── logo.svg
│ └── rendering.png
├── chatSQL.yml
├── file.svg
├── fonts
│ ├── MapleMono-Bold.ttf
│ ├── MapleMono-Italic.ttf
│ └── MapleMono-Regular.ttf
├── git_info.json
├── globe.svg
├── next.svg
├── vercel.svg
└── window.svg
├── scripts
└── generate-git-info.js
├── src
├── app
│ ├── App.css
│ ├── api
│ │ ├── git-history
│ │ │ └── route.ts
│ │ └── project-stats
│ │ │ └── route.ts
│ ├── changelog
│ │ ├── changelog.module.css
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── loading.tsx
│ ├── page.module.css
│ ├── page.tsx
│ └── share
│ │ └── page.tsx
├── components
│ ├── History
│ │ ├── HistoryItem.module.css
│ │ ├── HistoryItem.tsx
│ │ ├── HistoryPanel.module.css
│ │ ├── HistoryPanel.tsx
│ │ ├── SearchBar.module.css
│ │ ├── SearchBar.tsx
│ │ └── SearchRecords.module.css
│ ├── LLMInteractive
│ │ ├── LLMWindow
│ │ │ ├── LLMWindow.module.css
│ │ │ └── LLMWindow.tsx
│ │ └── renderedArea
│ │ │ ├── Container.tsx
│ │ │ ├── DatabaseFlow.css
│ │ │ ├── DatabaseFlow.tsx
│ │ │ ├── ProblemViewer.module.css
│ │ │ ├── ProblemViewer.tsx
│ │ │ ├── TableDisplay.tsx
│ │ │ ├── TableNavigator.module.css
│ │ │ ├── TableNavigator.tsx
│ │ │ ├── TupleViewer.module.css
│ │ │ └── TupleViewer.tsx
│ ├── NavBar
│ │ ├── NavBar.module.css
│ │ ├── NavBar.tsx
│ │ └── ShareButton.tsx
│ ├── SideBar
│ │ ├── GuidingModal.module.css
│ │ ├── GuidingModal.tsx
│ │ ├── SideBar.module.css
│ │ ├── SideBar.tsx
│ │ ├── ThemeToggle.module.css
│ │ ├── ThemeToggle.tsx
│ │ └── index.ts
│ ├── Tutorial
│ │ ├── InitTutorialButton.module.css
│ │ ├── InitTutorialButton.tsx
│ │ └── tutorialData.ts
│ ├── codeEditing
│ │ ├── EmptyQueryState.module.css
│ │ ├── EmptyQueryState.tsx
│ │ ├── MonacoEditorStyles.css
│ │ ├── QueryResultTable.module.css
│ │ ├── QueryResultTable.tsx
│ │ ├── SQLEditor.css
│ │ └── SQLEditor.tsx
│ ├── common
│ │ ├── CustomTooltip.css
│ │ └── CustomTooltip.tsx
│ └── utils
│ │ ├── ShinyText.css
│ │ └── ShinyText.js
├── contexts
│ ├── CompletionContext.tsx
│ ├── EditorContext.tsx
│ ├── LLMContext.tsx
│ ├── QueryContext.tsx
│ └── ThemeContext.tsx
├── hooks
│ ├── useHistoryRecords.tsx
│ ├── useRecords.tsx
│ └── useTagsManager.tsx
├── lib
│ ├── LLMResultView.module.css
│ ├── LLMResultView.tsx
│ ├── aggregateFunctions.ts
│ ├── conditionEvaluator.ts
│ ├── constraintValidator.ts
│ ├── index.ts
│ ├── parseMySQL.tsx
│ ├── queryHelpers.ts
│ ├── resultComparator.ts
│ ├── sqlCompletionProvider.ts
│ ├── sqlHoverProvider.ts
│ └── transactionManager.ts
├── services
│ ├── recordsIndexDB.tsx
│ └── sqlExecutor.ts
└── types
│ ├── database.ts
│ ├── dify.ts
│ ├── git.ts
│ └── sqlExecutor.ts
└── tsconfig.json
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/.DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 junhao Zhuo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 𝐜𝐡𝐚𝐭𝐒𝐐𝐋
2 |
3 | [English](./README_EN.md) | 简体中文
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Website
14 |
15 |
16 |
17 | ChatSQL 是一个交互式 SQL 学习平台,通过人工智能技术生成个性化的 SQL 练习题,帮助用户从入门到精通 SQL 查询语言。平台结合了直观的数据库可视化工具、智能代码编辑器和即时反馈系统,为用户提供沉浸式学习体验。无论您是 SQL 初学者还是希望提升查询技能的开发者,ChatSQL 都能根据您的水平定制适合的学习内容,让 SQL 学习变得更加高效和有趣。
18 |
19 | ## ✨ 特性
20 |
21 | - 🤖 AI 生成练习:提供两种方式的习题来源
22 | - 通过预设的教程, 循序渐进地练习`select`, `join`, 聚合操作与嵌套子查询等知识点.
23 | - 与dify工作流交互, 输入难度,标签与描述自动生成 SQL 练习题.
24 |
25 | - 📊 数据库结构可视化:直观展示表关系和字段信息, 外检约束等信息一目了然;
26 | - ⌨️ Monaco编辑器与schema的补全整合:
27 | - 支持sql语法高亮和悬浮的语法提示
28 | - 针对当前schema信息提供`tab`的自动补全
29 |
30 | - 📝 即时结果验证:实时验证查询结果
31 | - 由构建于前端的sql引擎0延迟地处理sql查询结果.
32 | - 支持将查询结果与期望结果进行比较, 评价查询结果是否正确.
33 |
34 |
35 |
36 |
37 | ## 🖥 界面预览
38 |
39 | ### 初始化界面
40 |
41 | 
42 |
43 | - 点击侧边栏中的“初始化教程”, 可以同预设的数据库表结构进行交互;
44 | - 点击侧边栏中的“帮助”, 可以查看基本的操作演示.
45 |
46 | ### 数据库结构可视化
47 |
48 | 
49 |
50 | - 默认显示数据库结构的可视化视图;
51 | - 可在左下角切换元组视图.
52 |
53 | ### SQL 编辑器演示
54 |
55 |
56 |
57 | 对应快捷键:
58 |
59 | - `command+enter` : 执行查询
60 | - `command+j`: 检测查询结果是否匹配;
61 | - `command+k`: 搜索历史记录.
62 |
63 | ## 🛠 技术栈
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | - **框架**: [Next.js](https://nextjs.org/) 15.3.0
76 | - **UI 组件**:
77 | - [Ant Design](https://ant.design/) 5.24.6
78 | - [Material-UI](https://mui.com/) 7.0.2
79 | - **编辑器**: [Monaco Editor](https://microsoft.github.io/monaco-editor/)
80 | - **流程图**:
81 | - [XY Flow](https://reactflow.dev/) (@xyflow/react)
82 | - 用于数据库表关系可视化
83 | - 支持自定义节点和边的样式
84 | - 提供图表交互操作
85 | - 基于 D3.js 的缩放和拖拽功能
86 | - **AI 集成**: [Dify.ai](https://dify.ai/)
87 | - **类型检查**: [TypeScript](https://www.typescriptlang.org/)
88 |
89 | ## 🚀 快速开始
90 |
91 | ### 前置要求
92 |
93 | - Node.js 18.0 或更高版本
94 | - npm 包管理器
95 | - Dify.ai 账号和 API 密钥
96 |
97 | ### 安装步骤
98 |
99 | 1. 克隆仓库
100 |
101 | ```bash
102 | git clone https://github.com/ffy6511/chatSQL.git
103 | cd chatSQL/chat-sql
104 | ```
105 |
106 | 2. 安装依赖
107 |
108 | ```bash
109 | npm install
110 | ```
111 |
112 | 3. 配置环境变量
113 |
114 | ```bash
115 | touch .env
116 | ```
117 |
118 | 编辑 `.env` 文件并添加你的 Dify API 密钥:
119 |
120 | ```
121 | NEXT_PUBLIC_DIFY_API_KEY=your_api_key_here
122 | ```
123 |
124 | 4. 启动开发服务器
125 |
126 | ```bash
127 | npm run dev
128 | ```
129 |
130 | 5. 更新git日志: 如果您希望更新自己的"更新日志"界面, 请执行
131 |
132 | ```bash
133 | npm run generate-git
134 | ```
135 |
136 | ### Dify 工作流配置
137 |
138 | 1. 在 [Dify 平台](https://dify.ai) 创建新应用(选择工作流)
139 | 2. 导入工作流配置:
140 | - 从项目中下载 `public/chatSQL.yml` 文件
141 | - 在 Dify 平台中导入该配置文件
142 | -
143 | 3. 获取 API 密钥并在个人设置中配置(工作流默认使用 Gemini,可根据需要修改)
144 |
145 | ## 🤝 贡献
146 |
147 | 欢迎提交 Pull Request 和 Issue!
148 |
149 | ## 📄 许可证
150 |
151 | [MIT License](./LICENSE)
152 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # 𝐜𝐡𝐚𝐭𝐒𝐐𝐋
2 |
3 | English | [简体中文](./README.md)
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Website
14 |
15 | ChatSQL is an interactive SQL learning platform that leverages artificial intelligence to generate personalized SQL exercises, helping users master SQL query language from beginner to advanced levels. The platform combines intuitive database visualization tools, a smart code editor, and real-time feedback system to provide an immersive learning experience. Whether you're a SQL beginner or a developer looking to enhance your query skills, ChatSQL tailors learning content to your proficiency level, making SQL learning more efficient and engaging.
16 |
17 | ## ✨ Features
18 |
19 | - 🤖 AI-Generated Exercises: Two sources of practice problems
20 | - Through preset tutorials, progressively practice `select`, `join`, aggregation operations, and nested subqueries.
21 | - Interact with Dify workflow to automatically generate SQL exercises by inputting difficulty, tags, and descriptions.
22 |
23 | - 📊 Database Structure Visualization: Intuitively displays table relationships and field information, with foreign key constraints clearly visible.
24 | - ⌨️ Monaco Editor with Schema Completion Integration:
25 | - Supports SQL syntax highlighting and hover syntax tips
26 | - Provides `tab` auto-completion based on current schema information
27 |
28 | - 📝 Instant Result Validation: Real-time verification of query results
29 | - SQL engine built into the frontend processes query results with zero delay
30 | - Supports comparing query results with expected results to evaluate correctness
31 |
32 | ## 🖥 Interface Preview
33 |
34 | ### Initialization Interface
35 | 
36 | - Click "Initialization Tutorial" in the sidebar to interact with the preset database structure
37 | - Click "Help" in the sidebar to view basic operation demonstrations
38 |
39 | ### Database Structure Visualization
40 | 
41 | - Database structure visualization view is displayed by default
42 | - You can switch to tuple view in the bottom left corner
43 |
44 | ### SQL Editor Demo
45 |
46 |
47 | Corresponding shortcuts:
48 | - `command+enter` : Execute query
49 | - `command+j`: Check if query results match
50 | - `command+k`: Search history records
51 |
52 | ## 🛠 Tech Stack
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | - **Framework**: [Next.js](https://nextjs.org/) 15.3.0
65 | - **UI Components**:
66 | - [Ant Design](https://ant.design/) 5.24.6
67 | - [Material-UI](https://mui.com/) 7.0.2
68 | - **Editor**: [Monaco Editor](https://microsoft.github.io/monaco-editor/)
69 | - **Flow Diagram**:
70 | - [XY Flow](https://reactflow.dev/) (@xyflow/react)
71 | - For database table relationship visualization
72 | - Supports custom node and edge styles
73 | - Provides interactive chart operations
74 | - D3.js-based zooming and dragging functionality
75 | - **AI Integration**: [Dify.ai](https://dify.ai/)
76 | - **Type Checking**: [TypeScript](https://www.typescriptlang.org/)
77 |
78 | ## 🚀 Quick Start
79 |
80 | ### Prerequisites
81 |
82 | - Node.js 18.0 or higher
83 | - npm package manager
84 | - Dify.ai account and API key
85 |
86 | ### Installation Steps
87 |
88 | 1. Clone the repository
89 |
90 | ```bash
91 | git clone https://github.com/ffy6511/chatSQL.git
92 | cd chatSQL/chat-sql
93 | ```
94 |
95 | 2. Install dependencies
96 |
97 | ```bash
98 | npm install
99 | ```
100 |
101 | 3. Configure environment variables
102 |
103 | ```bash
104 | touch .env
105 | ```
106 |
107 | Edit the `.env` file and add your Dify API key:
108 |
109 | ```
110 | NEXT_PUBLIC_DIFY_API_KEY=your_api_key_here
111 | ```
112 |
113 | 4. Start the development server
114 |
115 | ```bash
116 | npm run dev
117 | ```
118 |
119 | 5. Update the git log: If you wish to update your own "changelog" interface, please execute
120 |
121 | ```bash
122 | npm run generate-git
123 | ```
124 |
125 | ### Dify Workflow Configuration
126 |
127 | 1. Create a new application (select workflow) on [Dify platform](https://dify.ai)
128 | 2. Import workflow configuration:
129 | - Download the `public/chatSQL.yml` file from the project
130 | - Import this configuration file in the Dify platform
131 | -
132 | 3. Get API key and configure in personal settings (workflow uses Gemini by default, can be modified as needed)
133 |
134 | ## 🤝 Contributing
135 |
136 | Pull requests and issues are welcome!
137 |
138 | ## 📄 License
139 |
140 | [MIT License](./LICENSE)
141 |
--------------------------------------------------------------------------------
/chat-sql/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | .work_log.md
17 |
18 | # next.js
19 | /.next/
20 | /out/
21 |
22 | # production
23 | /build
24 |
25 | # misc
26 | .DS_Store
27 | *.pem
28 |
29 | # debug
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 | .pnpm-debug.log*
34 |
35 | # env files (can opt-in for committing if needed)
36 | .env*
37 |
38 | # vercel
39 | .vercel
40 |
41 | # typescript
42 | *.tsbuildinfo
43 | next-env.d.ts
44 |
--------------------------------------------------------------------------------
/chat-sql/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | {
15 | rules: {
16 | // 禁用所有报错的规则
17 | "@typescript-eslint/no-explicit-any": "off",
18 | "@typescript-eslint/no-unused-vars": "off",
19 | "@typescript-eslint/no-require-imports": "off",
20 | "prefer-const": "off",
21 | "react/no-unescaped-entities": "off",
22 | "react-hooks/exhaustive-deps": "off",
23 |
24 | // 如果将来需要启用某些规则,可以设置为 "warn" 或 "error"
25 | // "@typescript-eslint/no-explicit-any": "warn",
26 | }
27 | }
28 | ];
29 |
30 | export default eslintConfig;
31 |
--------------------------------------------------------------------------------
/chat-sql/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | /* config options here */
4 | transpilePackages: ['antd']
5 | };
6 |
7 | module.exports = nextConfig;
8 |
--------------------------------------------------------------------------------
/chat-sql/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-sql",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "generate-git": "node scripts/generate-git-info.js",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.14.0",
14 | "@emotion/styled": "^11.14.0",
15 | "@monaco-editor/react": "^4.7.0",
16 | "@mui/icons-material": "^7.0.2",
17 | "@mui/lab": "^7.0.0-beta.11",
18 | "@mui/material": "^7.0.2",
19 | "@mui/x-data-grid": "^7.28.3",
20 | "@tsparticles/slim": "^3.8.1",
21 | "@xyflow/react": "^12.5.5",
22 | "antd": "^5.24.6",
23 | "monaco-editor": "^0.52.2",
24 | "next": "15.3.0",
25 | "next-themes": "^0.4.6",
26 | "node-sql-parser": "^5.3.8",
27 | "react": "^18.2.0",
28 | "react-dom": "^18.2.0",
29 | "simple-git": "^3.27.0",
30 | "tsparticles-engine": "^2.12.0"
31 | },
32 | "resolutions": {
33 | "antd": {
34 | "react": "$react",
35 | "react-dom": "$react-dom"
36 | }
37 | },
38 | "devDependencies": {
39 | "@eslint/eslintrc": "^3",
40 | "@tailwindcss/postcss": "^4",
41 | "@types/node": "^20",
42 | "@types/react": "^19",
43 | "@types/react-dom": "^19",
44 | "eslint": "^9",
45 | "eslint-config-next": "15.3.0",
46 | "monaco-editor-webpack-plugin": "^7.1.0",
47 | "tailwindcss": "^4",
48 | "typescript": "^5"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/chat-sql/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/chat-sql/public/assets/chat.m4v:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/assets/chat.m4v
--------------------------------------------------------------------------------
/chat-sql/public/assets/dify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/assets/dify.png
--------------------------------------------------------------------------------
/chat-sql/public/assets/edit.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/assets/edit.gif
--------------------------------------------------------------------------------
/chat-sql/public/assets/initialization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/assets/initialization.png
--------------------------------------------------------------------------------
/chat-sql/public/assets/landscape.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/assets/landscape.jpg
--------------------------------------------------------------------------------
/chat-sql/public/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/chat-sql/public/assets/rendering.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/assets/rendering.png
--------------------------------------------------------------------------------
/chat-sql/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/chat-sql/public/fonts/MapleMono-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/fonts/MapleMono-Bold.ttf
--------------------------------------------------------------------------------
/chat-sql/public/fonts/MapleMono-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/fonts/MapleMono-Italic.ttf
--------------------------------------------------------------------------------
/chat-sql/public/fonts/MapleMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/public/fonts/MapleMono-Regular.ttf
--------------------------------------------------------------------------------
/chat-sql/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/chat-sql/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/chat-sql/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/chat-sql/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/chat-sql/scripts/generate-git-info.js:
--------------------------------------------------------------------------------
1 | const simpleGit = require('simple-git');
2 | const fs = require('fs').promises;
3 | const path = require('path');
4 |
5 | // 设置描述的最大字符数
6 | const MAX_DESCRIPTION_LENGTH = 400;
7 |
8 | async function generateGitInfo() {
9 | const git = simpleGit();
10 |
11 | // 获取git信息
12 | const [logs, branchSummary] = await Promise.all([
13 | git.log({ maxCount: 50 }),
14 | git.branch()
15 | ]);
16 |
17 | // 获取每个提交的详细描述
18 | const commitsWithDescription = await Promise.all(
19 | logs.all.map(async (commit) => {
20 | try {
21 | // 使用 git show 命令获取完整的提交信息
22 | const result = await git.raw(['show', commit.hash, '--no-patch', '--format=%B']);
23 |
24 | // 分离提交消息的标题和详细描述
25 | const lines = result.trim().split('\n');
26 | const title = lines[0]; // 第一行是标题
27 |
28 | // 跳过第一行和空行,获取详细描述
29 | let description = '';
30 | if (lines.length > 1) {
31 | const descLines = lines.slice(1).filter(line => line.trim() !== '');
32 | if (descLines.length > 0) {
33 | description = descLines.join('\n');
34 |
35 | // 限制描述的长度
36 | if (description.length > MAX_DESCRIPTION_LENGTH) {
37 | description = description.substring(0, MAX_DESCRIPTION_LENGTH) + '...';
38 | }
39 | }
40 | }
41 |
42 | console.log(`Commit ${commit.hash.substring(0, 7)}:`);
43 | console.log(`Title: ${title}`);
44 | console.log(`Description: ${description ? description.substring(0, 50) + '...' : '(no description)'}`);
45 | console.log('---');
46 |
47 | return {
48 | hash: commit.hash,
49 | date: commit.date,
50 | message: title, // 使用标题作为消息
51 | description: description, // 添加详细描述
52 | author: commit.author_name
53 | };
54 | } catch (error) {
55 | console.error(`Error processing commit ${commit.hash}:`, error);
56 | return {
57 | hash: commit.hash,
58 | date: commit.date,
59 | message: commit.message,
60 | description: '',
61 | author: commit.author_name
62 | };
63 | }
64 | })
65 | );
66 |
67 | // 按日期分组提交
68 | const commitsByDate = {};
69 | commitsWithDescription.forEach(commit => {
70 | const date = new Date(commit.date).toLocaleDateString('zh-CN');
71 | if (!commitsByDate[date]) {
72 | commitsByDate[date] = [];
73 | }
74 | commitsByDate[date].push(commit);
75 | });
76 |
77 | const gitInfo = {
78 | history: commitsWithDescription,
79 | commitsByDate: commitsByDate,
80 | stats: {
81 | totalCommits: logs.total,
82 | contributors: [...new Set(logs.all.map(commit => commit.author_name))],
83 | lastUpdate: logs.latest?.date || '',
84 | activeBranches: branchSummary.all.length
85 | }
86 | };
87 |
88 | // 写入JSON文件
89 | await fs.writeFile(
90 | path.join(__dirname, '../public/git_info.json'),
91 | JSON.stringify(gitInfo, null, 2)
92 | );
93 |
94 | console.log('Git info generated successfully!');
95 | }
96 |
97 | generateGitInfo().catch(console.error);
98 |
--------------------------------------------------------------------------------
/chat-sql/src/app/App.css:
--------------------------------------------------------------------------------
1 | /* App.css */
2 | .app-container {
3 | display: flex;
4 | height: calc(100vh - var(--navbar-height));
5 | width: 100vw;
6 | overflow: hidden;
7 | margin-top: var(--navbar-height);
8 | }
9 |
10 | .sidebar-container {
11 | width: 50px;
12 | height: 100%;
13 | flex-shrink: 0;
14 | z-index: 10;
15 | }
16 |
17 | .main-container {
18 | flex: 1;
19 | display: flex;
20 | flex-direction: column;
21 | height: 100%;
22 | overflow: hidden;
23 | padding-top: var(--navbar-height);
24 | }
25 |
26 | .content-container {
27 | display: flex;
28 | flex: 1;
29 | overflow: hidden;
30 | }
31 |
32 | .history-panel-container {
33 | width: 250px;
34 | height: 100%;
35 | overflow-y: auto;
36 | transition: width 0.3s ease;
37 | border-right: 1px solid var(--sidebar-border);
38 | }
39 |
40 | .history-panel-collapsed {
41 | width: 0;
42 | overflow: hidden;
43 | }
44 |
45 | .editor-container {
46 | flex: 1;
47 | display: flex;
48 | flex-direction: column;
49 | height: 100%;
50 | overflow: hidden;
51 | }
52 |
53 | .editor-area {
54 | flex: 1;
55 | display: flex;
56 | overflow: hidden;
57 | }
58 |
59 | .sql-editor {
60 | flex: 1;
61 | height: 100%;
62 | overflow: hidden;
63 | border-right: 1px solid var(--sidebar-border);
64 | }
65 |
66 | .result-area {
67 | flex: 1;
68 | height: 100%;
69 | overflow: auto;
70 | padding: 10px;
71 | background-color: var(--background);
72 | }
73 |
74 | /* 响应式布局 */
75 | @media (max-width: 768px) {
76 | .editor-area {
77 | flex-direction: column;
78 | }
79 |
80 | .sql-editor, .result-area {
81 | flex: none;
82 | height: 50%;
83 | width: 100%;
84 | }
85 |
86 | .sql-editor {
87 | border-right: none;
88 | border-bottom: 1px solid var(--sidebar-border);
89 | }
90 | }
91 |
92 | /* 确保 LLM 窗口垂直居中 */
93 | .full-height-llm-container {
94 | display: flex;
95 | align-items: center;
96 | justify-content: center;
97 | height: 100%;
98 | width: 100%;
99 | background-color: var(--background);
100 | }
101 |
102 | /* 确保内容区域有正确的高度 */
103 | .upper-content, .lower-content {
104 | height: 100%;
105 | width: 100%;
106 | overflow: hidden;
107 | }
108 |
109 | /* 确保 DatabaseFlow 容器有正确的高度 */
110 | .database-flow-container {
111 | width: 100% !important;
112 | height: 100% !important;
113 | position: relative;
114 | }
115 |
116 |
117 | .lower-left-panel{
118 | height: 100%;
119 | width: 100%;
120 | overflow: hidden;
121 | }
122 |
123 | .lower-left-content{
124 | height: 100%;
125 | width: 100%;
126 | overflow: hidden;
127 | }
--------------------------------------------------------------------------------
/chat-sql/src/app/api/git-history/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | export async function GET() {
6 | try {
7 | // 读取生成的 git_info.json 文件
8 | const filePath = path.join(process.cwd(), 'public', 'git_info.json');
9 | const fileContent = fs.readFileSync(filePath, 'utf8');
10 | const gitInfo = JSON.parse(fileContent);
11 |
12 | // 返回提交历史
13 | return NextResponse.json(gitInfo.history);
14 | } catch (error) {
15 | console.error('Error reading git history:', error);
16 | return NextResponse.json(
17 | { error: 'Failed to fetch git history' },
18 | { status: 500 }
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chat-sql/src/app/api/project-stats/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | export async function GET() {
6 | try {
7 | // 读取生成的 git_info.json 文件
8 | const filePath = path.join(process.cwd(), 'public', 'git_info.json');
9 | const fileContent = fs.readFileSync(filePath, 'utf8');
10 | const gitInfo = JSON.parse(fileContent);
11 |
12 | // 返回项目统计信息
13 | return NextResponse.json(gitInfo.stats);
14 | } catch (error) {
15 | console.error('Error reading project stats:', error);
16 | return NextResponse.json(
17 | { error: 'Failed to fetch project stats' },
18 | { status: 500 }
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/chat-sql/src/app/changelog/changelog.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | margin-top: var(--navbar-height);
4 | position: relative;
5 | overflow: hidden;
6 | background: transparent !important;
7 | }
8 |
9 | .parallaxBackground {
10 | position: fixed;
11 | top: 0;
12 | left: 0;
13 | width: 100%;
14 | height: 100%;
15 | background-image: url('/assets/landscape.jpg');
16 | background-size: cover;
17 | background-position: center;
18 | background-attachment: fixed;
19 | opacity: 0.4;
20 | z-index: -1;
21 | }
22 |
23 | .content {
24 | position: relative;
25 | padding: 24px;
26 | max-width: 1000px;
27 | margin: 0 auto;
28 | z-index: 1;
29 | background: transparent !important;
30 | }
31 |
32 | .loadingContainer {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | height: 100vh;
37 | background: transparent !important;
38 | }
39 |
--------------------------------------------------------------------------------
/chat-sql/src/app/changelog/page.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 20px;
4 | position: relative;
5 | background-color: var(--background);
6 | }
7 |
8 | .parallaxBackground {
9 | position: absolute;
10 | top: 0;
11 | left: 0;
12 | right: 0;
13 | bottom: 0;
14 | background-image: url('/images/grid-pattern.svg');
15 | background-size: 30px 30px;
16 | opacity: 0.05;
17 | z-index: 0;
18 | pointer-events: none;
19 | }
20 |
21 | .content {
22 | position: relative;
23 | z-index: 1;
24 | max-width: 800px;
25 | margin: 0 auto;
26 | padding-top: 40px;
27 | }
28 |
29 | .yearTitle {
30 | font-size: 2rem;
31 | font-weight: 700;
32 | margin: 2rem 0 1rem;
33 | color: var(--primary-text);
34 | position: relative;
35 | display: inline-block;
36 | }
37 |
38 | .yearTitle::after {
39 | content: '';
40 | position: absolute;
41 | bottom: -5px;
42 | left: 0;
43 | width: 100%;
44 | height: 3px;
45 | background-color: var(--link-color);
46 | border-radius: 3px;
47 | }
48 |
49 | .monthTitle {
50 | font-size: 1.5rem;
51 | font-weight: 600;
52 | margin: 1.5rem 0 1rem;
53 | color: var(--primary-text);
54 | }
55 |
56 | .commitCard {
57 | background-color: var(--card-bg) !important;
58 | border: 1px solid var(--card-border) !important;
59 | transition: all 0.3s ease !important;
60 | }
61 |
62 | .commitCard:hover {
63 | transform: translateY(-2px);
64 | box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1) !important;
65 | }
66 |
67 | [data-theme="dark"] .commitCard:hover {
68 | box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3) !important;
69 | }
70 |
71 | .commitDetails {
72 | color: var(--secondary-text) !important;
73 | }
74 |
75 | .loadingContainer {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | height: 100vh;
80 | }
81 |
82 | .timelineCard {
83 | background-color: var(--card-bg) !important;
84 | border: 1px solid var(--card-border) !important;
85 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
86 | }
87 |
88 | [data-theme="dark"] .timelineCard {
89 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
90 | }
--------------------------------------------------------------------------------
/chat-sql/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ffy6511/chatSQL/2953989274c234e462fa29d731f08666bd3c8c57/chat-sql/src/app/favicon.ico
--------------------------------------------------------------------------------
/chat-sql/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | :root {
4 | /* 基础颜色 */
5 | --background: #ffffff;
6 | --foreground: #171717;
7 | --font-mono: 'Maple Mono', monospace;
8 | --navbar-height: 40px; /* 导航栏高度变量 */
9 |
10 | /* 侧边栏 */
11 | --sidebar-bg: #fff;
12 | --sidebar-border: #f0f0f0;
13 | --button-hover: #f5f5f5;
14 |
15 | /* 卡片 */
16 | --card-bg: #fff;
17 | --card-border: #eaeaea;
18 |
19 | /* 文本颜色 */
20 | --primary-text: #171717;
21 | --secondary-text: #666666;
22 | --tertiary-text: #999999;
23 |
24 | --inverted-text: #b0b0b0;
25 |
26 | /* 图标颜色 */
27 | --icon-color: #666666;
28 | --icon-color-hover: #333333;
29 |
30 | /* 按钮颜色 */
31 | --button-text: #666666;
32 | --button-bg: transparent;
33 | --button-border: #eaeaea;
34 |
35 | /* 输入框 */
36 | --input-bg: #ffffff;
37 | --input-border: #d9d9d9;
38 | --input-text: #171717;
39 |
40 | /* 链接颜色 */
41 | --link-color: #1677ff;
42 | --link-hover: #4096ff;
43 |
44 | /* 代码块 */
45 | --code-bg: #f5f5f5;
46 | --code-text: #333333;
47 |
48 | /* 分割线 */
49 | --divider-color: #f0f0f0;
50 |
51 | /* 消息提示颜色 */
52 | --success-color: #52c41a;
53 | --info-color: #1677ff;
54 | --error-color: #ff4d4f;
55 | }
56 |
57 | [data-theme="dark"] {
58 | /* 基础颜色 */
59 | --background: #121212;
60 | --foreground: #f0f0f0;
61 |
62 | /* 侧边栏 */
63 | --sidebar-bg: #1e1e1e;
64 | --sidebar-border: #333333;
65 | --button-hover: #333333;
66 |
67 | /* 卡片 */
68 | --card-bg: #1e1e1e;
69 | --card-border: #333333;
70 |
71 | /* 文本颜色 */
72 | --primary-text: #f0f0f0;
73 | --secondary-text: #b0b0b0;
74 | --tertiary-text: #808080;
75 | --inverted-text: #666666;
76 |
77 | /* 图标颜色 */
78 | --icon-color-dark: #b0b0b0;
79 | --icon-color-hover: #f0f0f0;
80 |
81 | /* 按钮颜色 */
82 | --button-text: #b0b0b0;
83 | --button-bg: transparent;
84 | --button-border: #333333;
85 |
86 | /* 输入框 */
87 | --input-bg: #2c2c2c;
88 | --input-border: #444444;
89 | --input-text: #f0f0f0;
90 |
91 | /* 链接颜色 */
92 | --link-color: #4096ff;
93 | --link-hover: #69b1ff;
94 |
95 | /* 代码块 */
96 | --code-bg: #2c2c2c;
97 | --code-text: #e0e0e0;
98 |
99 | /* 分割线 */
100 | --divider-color: #333333;
101 |
102 | /* 消息提示颜色 */
103 | --success-color: #52c41a;
104 | --info-color: #1677ff;
105 | --error-color: #ff4d4f;
106 | }
107 |
108 | @theme inline {
109 | --color-background: var(--background);
110 | --color-foreground: var(--foreground);
111 | --font-sans: var(--font-geist-sans);
112 | --font-mono: var(--font-geist-mono);
113 | }
114 |
115 | body {
116 | background: var(--background);
117 | color: var(--primary-text);
118 | font-family: Arial, Helvetica, sans-serif;
119 | }
120 |
121 | /* 全局文本样式 */
122 | h1, h2, h3, h4, h5, h6 {
123 | color: var(--primary-text);
124 | }
125 |
126 | p, span, div {
127 | color: var(--primary-text);
128 | }
129 |
130 | a {
131 | color: var(--link-color);
132 | }
133 |
134 | a:hover {
135 | color: var(--link-hover);
136 | }
137 |
138 | /* 输入框全局样式 */
139 | input, textarea, select {
140 | background-color: var(--input-bg);
141 | border-color: var(--input-border);
142 | color: var(--input-text);
143 | }
144 |
145 | /* 代码块全局样式 */
146 | code, pre {
147 | background-color: var(--code-bg);
148 | color: var(--code-text);
149 | }
150 |
151 | /* 分割线全局样式 */
152 | hr {
153 | border-color: var(--divider-color);
154 | }
155 |
156 |
157 | /* 全局快捷键提示样式 */
158 | .shortcut-tooltip {
159 | display: flex;
160 | align-items: center;
161 | padding: 4px 0;
162 | color: var(--tertiary-text);
163 | }
164 |
165 |
166 | .shortcut-icon {
167 | font-size: 1em !important;
168 | margin-left: 4px;
169 | margin-right: 2px;
170 | color: var(--tertiary-text);
171 | }
172 |
173 | .shortcut-plus {
174 | margin: 0 2px;
175 | }
176 |
177 | .shortcut-tooltip .shortcut-plus {
178 | margin: 0 2px;
179 | opacity: 0.7;
180 | }
181 |
182 | /* 定义字体 */
183 | @font-face {
184 | font-family: 'Maple Mono';
185 | font-style: normal;
186 | font-weight: normal;
187 | src: url('/fonts/MapleMono-Regular.ttf') format('truetype');
188 | font-display: swap;
189 | }
190 |
191 | @font-face {
192 | font-family: 'Maple Mono';
193 | font-style: italic;
194 | font-weight: normal;
195 | src: url('/fonts/MapleMono-Italic.ttf') format('truetype');
196 | font-display: swap;
197 | }
198 |
199 | @font-face {
200 | font-family: 'Maple Mono';
201 | font-style: normal;
202 | font-weight: bold;
203 | src: url('/fonts/MapleMono-Bold.ttf') format('truetype');
204 | font-display: swap;
205 | }
206 |
207 | /* 在:root中定义变量 */
208 | :root {
209 | --font-mono: 'Maple Mono', monospace !important;
210 | }
211 |
212 | /* 强制应用到Monaco编辑器 */
213 | .monaco-editor {
214 | font-family: var(--font-mono) !important;
215 | }
216 |
217 | .monaco-editor .view-line * {
218 | font-family: var(--font-mono) !important;
219 | }
220 |
221 | /* 添加页面过渡动画 */
222 | @keyframes fadeIn {
223 | from {
224 | opacity: 0;
225 | transform: translateY(10px);
226 | }
227 | to {
228 | opacity: 1;
229 | transform: translateY(0);
230 | }
231 | }
232 |
233 | .content-container {
234 | animation: fadeIn 0.5s ease-out;
235 | }
236 |
237 | /* 骨架屏动画 */
238 | @keyframes skeletonPulse {
239 | 0% {
240 | opacity: 0.4;
241 | }
242 | 50% {
243 | opacity: 0.8;
244 | }
245 | 100% {
246 | opacity: 0.4;
247 | }
248 | }
249 |
250 | .skeleton-pulse {
251 | animation: skeletonPulse 1.5s infinite ease-in-out;
252 | }
253 |
254 | /* 消息提示样式 */
255 | .ant-message-notice-content {
256 | background-color: var(--card-bg) !important;
257 | color: var(--primary-text) !important;
258 | border: 1px solid var(--card-border) !important;
259 | padding: 10px 16px !important;
260 | border-radius: 8px !important;
261 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15) !important;
262 | }
263 |
264 | [data-theme="dark"] .ant-message-notice-content {
265 | background-color: #1e1e1e !important;
266 | color: #f0f0f0 !important;
267 | border: 1px solid #333333 !important;
268 | }
269 |
270 | /* 添加全局下拉菜单样式 */
271 | [data-theme="dark"] .ant-dropdown-menu {
272 | background-color: #1f1f1f !important;
273 | border-color: #333 !important;
274 | box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.48), 0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 9px 28px 8px rgba(0, 0, 0, 0.2) !important;
275 | }
276 |
277 | [data-theme="dark"] .ant-dropdown-menu-item {
278 | color: #e0e0e0 !important;
279 | }
280 |
281 | [data-theme="dark"] .ant-dropdown-menu-item:hover {
282 | background-color: #333 !important;
283 | }
284 |
285 | [data-theme="dark"] .ant-dropdown-menu-item-icon {
286 | color: #b0b0b0 !important;
287 | }
288 |
--------------------------------------------------------------------------------
/chat-sql/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState } from 'react';
4 | import { usePathname } from 'next/navigation';
5 | import { Geist, Geist_Mono } from "next/font/google";
6 | import "./globals.css";
7 | import { LLMProvider } from "@/contexts/LLMContext";
8 | import { QueryProvider } from "@/contexts/QueryContext";
9 | import { CompletionProvider } from "@/contexts/CompletionContext";
10 | import { EditorProvider } from "@/contexts/EditorContext";
11 | import { ThemeProvider } from '@/contexts/ThemeContext';
12 | import NavBar from "@/components/NavBar/NavBar";
13 | import Loading from './loading';
14 |
15 | const geistSans = Geist({
16 | variable: "--font-geist-sans",
17 | subsets: ["latin"],
18 | });
19 |
20 | const geistMono = Geist_Mono({
21 | variable: "--font-geist-mono",
22 | subsets: ["latin"],
23 | });
24 |
25 |
26 | export default function RootLayout({
27 | children,
28 | }: {
29 | children: React.ReactNode
30 | }) {
31 | const [isLoading, setIsLoading] = useState(false);
32 | const pathname = usePathname();
33 |
34 | // 当路径变化时,显示加载状态
35 | useEffect(() => {
36 | setIsLoading(true);
37 |
38 | // 使用短暂的延迟来确保加载状态可见
39 | const timer = setTimeout(() => {
40 | setIsLoading(false);
41 | }, 1000); // 稍微延长加载时间,让用户能看到骨架屏效果
42 |
43 | return () => clearTimeout(timer);
44 | }, [pathname]);
45 |
46 | return (
47 |
48 |
49 | chatSQL
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {isLoading ? : children}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/chat-sql/src/app/loading.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react';
4 | import { Box, Container, Skeleton, Paper } from '@mui/material';
5 | import { styled } from '@mui/material/styles';
6 |
7 | const LoadingContainer = styled(Container)({
8 | width: '55%',
9 | marginTop: 'var(--navbar-height, 64px)',
10 | padding: '24px',
11 | animation: 'fadeIn 0.3s ease-in-out',
12 | '@keyframes fadeIn': {
13 | from: { opacity: 0 },
14 | to: { opacity: 1 },
15 | },
16 | });
17 |
18 | const SkeletonCard = styled(Paper)(({ theme }) => ({
19 | padding: '24px',
20 | marginBottom: '24px',
21 | background: 'rgba(255, 255, 255, 0.8)',
22 | backdropFilter: 'blur(5px)',
23 | borderRadius: '16px',
24 | boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)',
25 | }));
26 |
27 | export default function Loading() {
28 | return (
29 |
30 | {/* 标题骨架 */}
31 |
32 |
33 |
34 |
35 | {/* 年份标题骨架 */}
36 |
37 |
38 | {/* 内容骨架 - 重复多个卡片 */}
39 | {Array.from(new Array(3)).map((_, index) => (
40 |
41 |
42 |
43 |
44 |
45 |
46 | {Array.from(new Array(2)).map((_, cardIndex) => (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | ))}
59 |
60 | ))}
61 |
62 | );
63 | }
--------------------------------------------------------------------------------
/chat-sql/src/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | background-color: var(--background);
9 | }
10 |
11 | .description {
12 | display: inherit;
13 | justify-content: inherit;
14 | align-items: inherit;
15 | font-size: 0.85rem;
16 | max-width: var(--max-width);
17 | width: 100%;
18 | z-index: 2;
19 | font-family: var(--font-mono);
20 | }
21 |
22 | .description a {
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | gap: 0.5rem;
27 | }
28 |
29 | .description p {
30 | position: relative;
31 | margin: 0;
32 | padding: 1rem;
33 | background-color: var(--card-bg);
34 | border: 1px solid var(--card-border);
35 | border-radius: 8px;
36 | color: var(--primary-text);
37 | }
38 |
39 | .code {
40 | font-weight: 700;
41 | font-family: var(--font-mono);
42 | }
43 |
44 | .grid {
45 | display: grid;
46 | grid-template-columns: repeat(4, minmax(25%, auto));
47 | max-width: 100%;
48 | width: var(--max-width);
49 | }
50 |
51 | .card {
52 | padding: 1rem 1.2rem;
53 | border-radius: 8px;
54 | background-color: var(--card-bg);
55 | border: 1px solid var(--card-border);
56 | transition: background 200ms, border 200ms;
57 | color: var(--primary-text);
58 | }
59 |
60 | .card span {
61 | display: inline-block;
62 | transition: transform 200ms;
63 | }
64 |
65 | .card h2 {
66 | font-weight: 600;
67 | margin-bottom: 0.7rem;
68 | color: var(--primary-text);
69 | }
70 |
71 | .card p {
72 | margin: 0;
73 | opacity: 0.6;
74 | font-size: 0.9rem;
75 | line-height: 1.5;
76 | max-width: 30ch;
77 | color: var(--secondary-text);
78 | }
79 |
80 | .center {
81 | display: flex;
82 | justify-content: center;
83 | align-items: center;
84 | position: relative;
85 | padding: 4rem 0;
86 | }
87 |
88 | .center::before {
89 | background: var(--secondary-glow);
90 | border-radius: 50%;
91 | width: 480px;
92 | height: 360px;
93 | margin-left: -400px;
94 | }
95 |
96 | .center::after {
97 | background: var(--primary-glow);
98 | width: 240px;
99 | height: 180px;
100 | z-index: -1;
101 | }
102 |
103 | .center::before,
104 | .center::after {
105 | content: '';
106 | left: 50%;
107 | position: absolute;
108 | filter: blur(45px);
109 | transform: translateZ(0);
110 | }
111 |
112 | .logo {
113 | position: relative;
114 | }
115 |
116 | /* 暗色模式下的特殊处理 */
117 | [data-theme="dark"] .card {
118 | border-color: var(--card-border);
119 | }
120 |
121 | [data-theme="dark"] .logo img {
122 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
123 | }
--------------------------------------------------------------------------------
/chat-sql/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { Splitter } from 'antd';
5 | import './App.css';
6 | import SQLEditor from '@/components/codeEditing/SQLEditor';
7 | import Container from '@/components/LLMInteractive/renderedArea/Container';
8 | import LLMWindow from '@/components/LLMInteractive/LLMWindow/LLMWindow';
9 | import { useLLMContext } from '@/contexts/LLMContext';
10 | import HistoryPanel from '@/components/History/HistoryPanel';
11 | import SideBar from '@/components/SideBar';
12 | import { useQueryContext } from '@/contexts/QueryContext';
13 | import { useEditorContext } from '@/contexts/EditorContext';
14 | import QueryResultTable from '@/components/codeEditing/QueryResultTable';
15 | import EmptyQueryState from '@/components/codeEditing/EmptyQueryState';
16 |
17 | const SQLQueryArea: React.FC = () => {
18 | const { queryResult } = useQueryContext();
19 |
20 | return (
21 |
22 | {queryResult ? (
23 |
24 | ) : (
25 |
26 | )}
27 |
28 | );
29 | };
30 |
31 | const Page: React.FC = () => {
32 | const { showLLMWindow } = useLLMContext();
33 | const [isHistoryCollapsed, setIsHistoryCollapsed] = useState(false);
34 |
35 | const handleToggleHistory = () => {
36 | setIsHistoryCollapsed(!isHistoryCollapsed);
37 | };
38 |
39 | const { sqlEditorValue, setSqlEditorValue } = useEditorContext(); // 使用EditorContext
40 |
41 | // 添加查询结果处理函数
42 | const handleQueryResult = (data: any) => {
43 | console.log('Query result:', data);
44 | // 这里可以添加更多的结果处理逻辑
45 | };
46 |
47 | return (
48 |
49 |
50 |
51 | {/* 左侧侧边栏 */}
52 |
58 |
59 |
60 |
61 | {/* 右侧区域:历史记录 + 大区域 */}
62 |
63 |
64 | {/* 使用条件渲染来控制历史面板的显示/隐藏 */}
65 | {!isHistoryCollapsed && (
66 |
73 |
74 |
75 |
76 |
77 | )}
78 |
79 | {/* 右侧大区域 */}
80 |
81 | {showLLMWindow ? (
82 | // 当显示LLM窗口时,占据整个区域并垂直居中
83 |
89 |
90 |
91 | ) : (
92 | // 正常模式:1上2下
93 |
94 | {/* 上部区域 - 容器 */}
95 |
101 |
102 |
103 |
104 |
105 |
106 | {/* 下部区域:水平分为两部分 */}
107 |
108 |
109 | {/* 下部左侧区域 */}
110 |
115 |
116 |
117 |
118 | {/* 下部右侧区域 */}
119 |
120 |
121 |
124 |
125 |
126 |
127 |
128 |
129 | )}
130 |
131 |
132 |
133 |
134 |
135 |
136 | );
137 | };
138 |
139 | export default Page;
140 |
--------------------------------------------------------------------------------
/chat-sql/src/app/share/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, Suspense } from 'react';
4 | import { useSearchParams } from 'next/navigation';
5 | import { message } from 'antd';
6 |
7 | // 创建一个内部组件来处理数据导入逻辑
8 | const ImportHandler: React.FC = () => {
9 | const searchParams = useSearchParams();
10 | const [messageApi, contextHolder] = message.useMessage();
11 |
12 | // 辅助函数:URL 安全的 Base64 解码为 UTF-8
13 | const b64url_to_utf8 = (str: string) => {
14 | // 还原标准 Base64
15 | str = str.replace(/-/g, '+').replace(/_/g, '/');
16 | // 添加补位
17 | while (str.length % 4) {
18 | str += '=';
19 | }
20 |
21 | try {
22 | return decodeURIComponent(
23 | atob(str).split('').map(function(c) {
24 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
25 | }).join('')
26 | );
27 | } catch (e) {
28 | throw new Error('解码失败:无效的分享数据');
29 | }
30 | };
31 |
32 | useEffect(() => {
33 | const importSharedData = async () => {
34 | try {
35 | const sharedData = searchParams.get('data');
36 | if (!sharedData) {
37 | messageApi.error('无效的分享链接');
38 | return;
39 | }
40 |
41 | // 使用 URL 安全的 Base64 解码
42 | const decodedData = JSON.parse(b64url_to_utf8(sharedData));
43 |
44 | // 处理数据中的日期
45 | const processedData = decodedData.map((item: any) => ({
46 | ...item,
47 | createdAt: item.createdAt ? new Date(item.createdAt) : new Date(),
48 | updatedAt: item.updatedAt ? new Date(item.updatedAt) : new Date()
49 | }));
50 |
51 | // 打开数据库
52 | const request = indexedDB.open('llm_problems_db');
53 |
54 | request.onerror = () => {
55 | messageApi.error('无法访问数据库');
56 | };
57 |
58 | request.onsuccess = (event) => {
59 | const db = (event.target as IDBOpenDBRequest).result;
60 | const transaction = db.transaction(['problems'], 'readwrite');
61 | const store = transaction.objectStore('problems');
62 |
63 | // 清除现有数据
64 | store.clear().onsuccess = () => {
65 | // 导入处理后的数据
66 | processedData.forEach((item: any) => {
67 | store.add(item);
68 | });
69 | };
70 |
71 | transaction.oncomplete = () => {
72 | messageApi.success('数据导入成功');
73 | // 重定向到主页
74 | setTimeout(() => {
75 | window.location.href = '/';
76 | }, 1500);
77 | };
78 | };
79 | } catch (error) {
80 | console.error('导入失败:', error);
81 | messageApi.error(error instanceof Error ? error.message : '导入失败');
82 | }
83 | };
84 |
85 | importSharedData();
86 | }, [searchParams, messageApi]);
87 |
88 | return (
89 |
90 | {contextHolder}
91 |
正在导入分享内容...
92 |
93 | );
94 | };
95 |
96 | // 主页面组件
97 | const SharePage: React.FC = () => {
98 | return (
99 | 加载中... }>
100 |
101 |
102 | );
103 | };
104 |
105 | export default SharePage;
106 |
--------------------------------------------------------------------------------
/chat-sql/src/components/History/HistoryItem.module.css:
--------------------------------------------------------------------------------
1 | .historyItem {
2 | padding: 0px;
3 | cursor: pointer;
4 | transition: all 0.3s ease;
5 | position: relative;
6 | background-color: var(--card-bg);
7 | border-radius: 10px!important;
8 | border-bottom: 1px solid var(--divider-color);
9 | margin-left: 6px;
10 | margin-right: 6px;
11 | }
12 |
13 | .historyItem:hover {
14 | background-color: var(--button-hover);
15 | }
16 |
17 | .active {
18 | background-color: var(--button-hover);
19 | }
20 |
21 | .titleContainer {
22 | display: flex;
23 | flex-direction: column;
24 | gap: 8px;
25 | }
26 |
27 | .title {
28 | font-size: 14px;
29 | color: var(--primary-text);
30 | padding-left: 6px;
31 | width: 100%;
32 | }
33 |
34 | .infoContainer {
35 | display: flex;
36 | align-items: center;
37 | padding-left: 6px;
38 | gap: 8px;
39 | font-size: 12px;
40 | color: var(--tertiary-text);
41 | }
42 |
43 | .dateInfo {
44 | display: flex;
45 | align-items: center;
46 | gap: 4px;
47 | }
48 |
49 | .tagsContainer {
50 | display: flex;
51 | gap: 2px;
52 | }
53 |
54 | .tag {
55 | font-size: 11px;
56 | padding: 0 4px;
57 | font-family: var(--font-mono) !important;
58 | line-height: 16px;
59 | }
60 |
61 | .actions {
62 | position: absolute;
63 | right: 12px;
64 | top: 50%;
65 | transform: translateY(-50%);
66 | display: flex;
67 | gap: 6px;
68 | opacity: 0;
69 | transition: all 0.3s ease;
70 | }
71 |
72 | .historyItem:hover .actions {
73 | opacity: 1;
74 | }
75 |
76 | .actions button {
77 | border-radius: 4px;
78 | display: flex;
79 | align-items: center;
80 | justify-content: center;
81 | transition: all 0.2s ease;
82 | }
83 |
84 | .actions button:hover {
85 | background-color: rgba(0, 0, 0, 0.05);
86 | transform: scale(1.05);
87 | }
88 |
89 | .editContainer {
90 | display: flex;
91 | width: 100%;
92 | gap: 8px;
93 | align-items: center;
94 | }
95 |
96 | .editContainer input {
97 | flex: 1;
98 | border-radius: 4px;
99 | border: 1px solid #d9d9d9;
100 | padding: 4px 8px;
101 | transition: all 0.3s;
102 | }
103 |
104 | .editContainer input:focus {
105 | border-color: #1677ff;
106 | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
107 | outline: none;
108 | }
109 |
110 | /* 删除确认对话框样式 */
111 | .deleteModal {
112 | width: 320px !important;
113 | }
114 |
115 | .tutorial {
116 | background-color: rgba(24, 144, 255, 0.1);
117 | border-left: 3px solid #1890ff;
118 | }
119 |
120 | .tutorial:hover {
121 | background-color: rgba(24, 144, 255, 0.2);
122 | }
123 |
124 | .moreButton{
125 | opacity: 0;
126 | transition: all 0.3s ease;
127 | font-size: 1.3em;
128 | margin-right: 6px;
129 | }
130 |
131 | .historyItem:hover .moreButton{
132 | opacity: 1;
133 | }
134 |
135 | /* dropdown的暗色模式适配样式 */
136 |
137 | /* 添加一个全局样式容器类 */
138 | .globalStylesContainer {
139 | /* 可以为空,仅用于包装全局样式 */
140 | }
141 |
142 | /* 下拉菜单适配暗色模式 */
143 | .globalStylesContainer [data-theme="dark"] :global(.ant-dropdown-menu) {
144 | background-color: #1f1f1f !important;
145 | border-color: #333 !important;
146 | }
147 |
148 | /* 菜单项文字颜色 */
149 | .globalStylesContainer [data-theme="dark"] :global(.ant-dropdown-menu-item) {
150 | color: #e0e0e0 !important;
151 | }
152 |
153 | /* 菜单项悬停样式 */
154 | .globalStylesContainer [data-theme="dark"] :global(.ant-dropdown-menu-item:hover) {
155 | background-color: #333 !important;
156 | }
157 |
158 | /* 危险操作菜单项 */
159 | .globalStylesContainer [data-theme="dark"] :global(.ant-dropdown-menu-item-danger) {
160 | color: #ff7875 !important;
161 | }
162 |
163 | /* 确认删除对话框 */
164 | .globalStylesContainer [data-theme="dark"] :global(.ant-modal-content),
165 | .globalStylesContainer [data-theme="dark"] :global(.ant-modal-header) {
166 | background-color: #1f1f1f !important;
167 | border-color: #333 !important;
168 | }
169 |
170 | .globalStylesContainer [data-theme="dark"] :global(.ant-modal-title),
171 | .globalStylesContainer [data-theme="dark"] :global(.ant-modal-body) {
172 | color: #e0e0e0 !important;
173 | }
174 |
--------------------------------------------------------------------------------
/chat-sql/src/components/History/HistoryItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useState } from 'react';
4 | import {
5 | List,
6 | Typography,
7 | Button,
8 | Input,
9 | Tooltip,
10 | Modal,
11 | Tag,
12 | Dropdown,
13 | Space,
14 | MenuProps
15 | } from 'antd';
16 | import {
17 | DeleteOutlined,
18 | EditOutlined,
19 | StarOutlined,
20 | StarFilled,
21 | MoreOutlined,
22 | ClockCircleOutlined
23 | } from '@ant-design/icons';
24 | import { LLMProblem } from '@/services/recordsIndexDB';
25 | import styles from './HistoryItem.module.css';
26 |
27 | const { Text } = Typography;
28 |
29 | interface HistoryItemProps {
30 | record: LLMProblem;
31 | isActive: boolean;
32 | onSelect: (id: number) => void;
33 | onDelete: (id: number) => void;
34 | onToggleFavorite: (id: number) => void;
35 | onRename: (id: number, newTitle: string) => void;
36 | }
37 |
38 | const HistoryItem: React.FC = ({
39 | record,
40 | isActive,
41 | onSelect,
42 | onDelete,
43 | onToggleFavorite,
44 | onRename
45 | }) => {
46 | const [isEditing, setIsEditing] = useState(false);
47 | const [newTitle, setNewTitle] = useState(record.title || '');
48 | const [isDeleteConfirmVisible, setIsDeleteConfirmVisible] = useState(false);
49 |
50 | // 标题处理函数
51 | const truncateTitle = (title: string, maxLength: number = 17) => {
52 | if (!title) return '';
53 | return title.length > maxLength ? `${title.substring(0, maxLength)}...` : title;
54 | };
55 |
56 | const handleRenameSubmit = () => {
57 | if (newTitle.trim()) {
58 | // 限制保存时的标题长度
59 | const truncatedTitle = truncateTitle(newTitle.trim(), 50); // 存储时允许更长的标题
60 | onRename(record.id!, truncatedTitle);
61 | setIsEditing(false);
62 | }
63 | };
64 |
65 | const handleRenameCancel = () => {
66 | setNewTitle(record.title || '');
67 | setIsEditing(false);
68 | };
69 |
70 | const handleDelete = () => {
71 | setIsDeleteConfirmVisible(true);
72 | };
73 |
74 | const confirmDelete = () => {
75 | onDelete(record.id!);
76 | setIsDeleteConfirmVisible(false);
77 | };
78 |
79 | const items: MenuProps['items'] = [
80 | {
81 | key: 'rename',
82 | icon: ,
83 | label: '重命名',
84 | onClick: () => {
85 | setIsEditing(true);
86 | }
87 | },
88 | {
89 | key: 'favorite',
90 | icon: record.isFavorite ? : ,
91 | label: record.isFavorite ? '取消收藏' : '收藏',
92 | onClick: () => {
93 | onToggleFavorite(record.id!);
94 | }
95 | },
96 | {
97 | key: 'delete',
98 | icon: ,
99 | label: '删除',
100 | danger: true,
101 | onClick: () => {
102 | handleDelete();
103 | }
104 | }
105 | ];
106 |
107 | // 格式化时间的函数
108 | const formatDate = (dateInput: Date | string) => {
109 | try {
110 | const date = dateInput instanceof Date ? dateInput : new Date(dateInput);
111 |
112 | if (isNaN(date.getTime())) {
113 | return '未知时间';
114 | }
115 |
116 | const year = date.getFullYear().toString().slice(2); // 只取年份后两位
117 | const month = (date.getMonth() + 1).toString().padStart(2, '0');
118 | const day = date.getDate().toString().padStart(2, '0');
119 | const hours = date.getHours().toString().padStart(2, '0');
120 | const minutes = date.getMinutes().toString().padStart(2, '0');
121 |
122 | return `${year}/${month}/${day} ${hours}:${minutes}`;
123 | } catch (error) {
124 | console.error('日期格式化错误:', error);
125 | return '未知时间';
126 | }
127 | };
128 |
129 | return (
130 |
131 |
onSelect(record.id!)}
134 | >
135 | {isEditing ? (
136 | e.stopPropagation()}>
137 | setNewTitle(e.target.value)}
140 | onPressEnter={handleRenameSubmit}
141 | maxLength={50} // 添加输入长度限制
142 | showCount // 显示字数统计
143 | autoFocus
144 | />
145 |
146 |
147 | 确定
148 |
149 |
150 | 取消
151 |
152 |
153 |
154 | ) : (
155 | <>
156 |
157 | {/* 移除悬浮的标题显示 */}
158 |
159 | {truncateTitle(record.title!)} {/* 显示截断的标题 */}
160 |
161 |
162 |
163 | {/* */}
164 |
165 | {record.createdAt ? formatDate(record.createdAt) : '未知时间'}
166 |
167 |
168 |
169 | {record.data?.isBuiltIn && (
170 | #{record.data.order}
171 | )}
172 | {record.data?.category && (
173 | {record.data.category}
174 | )}
175 |
176 |
177 |
178 |
179 | e.stopPropagation()}>
180 |
185 | }
188 | className={styles.moreButton}
189 | />
190 |
191 |
192 | >
193 | )}
194 |
195 | setIsDeleteConfirmVisible(false)}
200 | okText="确认"
201 | cancelText="取消"
202 | >
203 | 确定要删除这条记录吗?
204 |
205 |
206 |
207 | );
208 | };
209 |
210 | export default HistoryItem;
211 |
--------------------------------------------------------------------------------
/chat-sql/src/components/History/HistoryPanel.module.css:
--------------------------------------------------------------------------------
1 | .historyPanel {
2 | height: 100%;
3 | min-height: 80vh;
4 | display: flex;
5 | flex-direction: column;
6 | padding-top: 1em;
7 | overflow: hidden;
8 | background-color: var(--sidebar-bg);
9 | border-radius: 8px;
10 | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.03);
11 | }
12 |
13 | /* 添加 Tabs 相关样式 */
14 | .tabsContainer {
15 | flex: 1;
16 | display: flex;
17 | flex-direction: column;
18 | overflow: hidden;
19 | }
20 |
21 | .tabsContainer :global(.ant-tabs) {
22 | height: 100%;
23 | display: flex;
24 | flex-direction: column;
25 | }
26 |
27 | .tabsContainer :global(.ant-tabs-nav) {
28 | margin: 0 !important;
29 | padding: 0 16px;
30 | margin-bottom: 8px!important;
31 | }
32 |
33 | .tabsContainer :global(.ant-tabs-nav-list) {
34 | width: 100%;
35 | display: flex !important;
36 | }
37 |
38 | .tabsContainer :global(.ant-tabs-tab) {
39 | flex: 1;
40 | display: flex;
41 | justify-content: center;
42 | margin: 0 !important;
43 | }
44 |
45 | .tabsContainer :global(.ant-tabs-content-holder) {
46 | flex: 1;
47 | overflow: hidden;
48 | }
49 |
50 | .tabsContainer :global(.ant-tabs-content) {
51 | height: 100%;
52 | }
53 |
54 | .headerContainer {
55 | display: flex;
56 | align-items: center;
57 | justify-content: space-between;
58 | margin-bottom: 20px;
59 | gap: 8px;
60 | }
61 |
62 | .searchContainer {
63 | flex: 1;
64 | }
65 |
66 | .list {
67 | overflow-y: auto;
68 | height: 100%;
69 | padding: 0;
70 | scrollbar-width: thin;
71 | scrollbar-color: #d0d0d0 transparent;
72 | }
73 |
74 | .list::-webkit-scrollbar {
75 | width: 6px;
76 | }
77 |
78 | .list::-webkit-scrollbar-track {
79 | background: transparent;
80 | }
81 |
82 | .list::-webkit-scrollbar-thumb {
83 | background-color: #d0d0d0;
84 | border-radius: 6px;
85 | }
86 |
87 | .spinner {
88 | display: flex;
89 | justify-content: center;
90 | align-items: center;
91 | height: 100px;
92 | color: #1677ff;
93 | }
94 |
95 | .empty {
96 | margin-top: 20vh;
97 | color: var(--tertiary-text) !important;
98 | }
99 |
100 | /* 添加针对 Ant Design Empty 组件的样式覆盖 */
101 | .empty :global(.ant-empty-description) {
102 | color: var(--tertiary-text) !important;
103 | }
104 |
105 |
106 | .ant-tabs-nav{
107 | margin-bottom: 16px !important;
108 | }
109 |
110 | .ant-tabs-tab{
111 | padding: 8px 16px !important;
112 | transition: all 0.3s ease;
113 | }
114 |
115 | .ant-tabs-tab-active {
116 | background-color: rgba(22, 119, 255, 0.1);
117 | border-radius: 4px;
118 | }
119 |
120 | .ant-tabs-ink-bar {
121 | background: var(--link-color) !important;
122 | height: 3px !important;
123 | border-radius: 3px !important;
124 | }
125 |
126 |
127 | .actionButton {
128 | width: 30px !important;
129 | height: 30px !important;
130 | font-size: 1.2em!important;
131 | padding: 0;
132 | display: flex;
133 | align-items: center;
134 | justify-content: center;
135 | border-radius: 8px;
136 | transition: all 0.3s ease;
137 | color: var(--icon-color) !important;
138 | background: transparent;
139 | }
140 |
141 | .actionButton:hover {
142 | background: var(--button-hover) !important;
143 | color: var(--icon-color-hover) !important;
144 | }
145 |
146 | [data-theme="dark"] .actionButton {
147 | color: var(--icon-color-dark) !important;
148 | }
149 |
150 | [data-theme="dark"] .actionButton:hover {
151 | color: var(--icon-color-hover) !important;
152 | }
153 |
154 | .container {
155 | height: 100%;
156 | overflow-y: auto;
157 | padding: 16px;
158 | background-color: var(--background);
159 | }
160 |
161 | .header {
162 | display: flex;
163 | justify-content: space-between;
164 | align-items: center;
165 | margin-bottom: 16px;
166 | padding-bottom: 8px;
167 | border-bottom: 1px solid var(--divider-color);
168 | }
169 |
170 | .title {
171 | font-size: 18px;
172 | font-weight: 600;
173 | color: var(--primary-text);
174 | }
175 |
176 | .recordList {
177 | display: flex;
178 | flex-direction: column;
179 | gap: 8px;
180 | }
181 |
182 | .recordItem {
183 | padding: 12px;
184 | border-radius: 8px;
185 | background-color: var(--card-bg);
186 | border: 1px solid var(--card-border);
187 | cursor: pointer;
188 | transition: all 0.2s ease;
189 | }
190 |
191 | .recordItem:hover {
192 | transform: translateY(-2px);
193 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
194 | }
195 |
196 | [data-theme="dark"] .recordItem:hover {
197 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
198 | }
199 |
200 | .recordTitle {
201 | font-weight: 500;
202 | margin-bottom: 4px;
203 | color: var(--primary-text);
204 | }
205 |
206 | .recordDate {
207 | font-size: 12px;
208 | color: var(--tertiary-text);
209 | }
210 |
211 | .emptyState {
212 | display: flex;
213 | flex-direction: column;
214 | align-items: center;
215 | justify-content: center;
216 | height: 100%;
217 | color: var(--tertiary-text);
218 | text-align: center;
219 | padding: 20px;
220 | }
221 |
222 | .emptyIcon {
223 | font-size: 48px;
224 | margin-bottom: 16px;
225 | opacity: 0.5;
226 | color: var(--tertiary-text) !important;
227 | }
228 |
229 | .emptyText {
230 | font-size: 16px;
231 | max-width: 240px;
232 | color: var(--tertiary-text) !important;
233 | }
234 |
235 | .searchInput {
236 | width: 100%;
237 | margin-bottom: 16px;
238 | background-color: var(--input-bg) !important;
239 | color: var(--input-text) !important;
240 | border-color: var(--input-border) !important;
241 | }
242 |
243 | .searchInput input {
244 | color: var(--input-text) !important;
245 | }
246 |
247 | .searchInput:hover {
248 | border-color: var(--link-color) !important;
249 | }
250 |
251 | .searchInput:focus-within {
252 | border-color: var(--link-color) !important;
253 | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
254 | }
255 |
256 | [data-theme="dark"] .searchInput:focus-within {
257 | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3);
258 | }
259 |
--------------------------------------------------------------------------------
/chat-sql/src/components/History/HistoryPanel.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import {
3 | Button,
4 | Tooltip,
5 | Empty,
6 | List,
7 | Spin,
8 | message,
9 | Tabs,
10 | Badge
11 | } from 'antd';
12 | import {
13 | EditOutlined,
14 | ClockCircleOutlined,
15 | HeartOutlined
16 | } from '@ant-design/icons';
17 | import { useLLMContext } from '@/contexts/LLMContext';
18 | import { useCompletionContext } from '@/contexts/CompletionContext';
19 | import { useEditorContext } from '@/contexts/EditorContext';
20 | import { useHistoryRecords } from '@/hooks/useHistoryRecords';
21 | import HistoryItem from './HistoryItem';
22 | import styles from './HistoryPanel.module.css';
23 | import SearchBar from './SearchBar';
24 |
25 | const HistoryPanel: React.FC = () => {
26 | const [messageApi, contextHolder] = message.useMessage();
27 | const [searchQuery, setSearchQuery] = useState('');
28 | const {
29 | recentRecords,
30 | favoriteRecords,
31 | loading,
32 | handleDelete,
33 | handleToggleFavorite,
34 | handleRename,
35 | refreshRecords
36 | } = useHistoryRecords();
37 |
38 | const {
39 | setLLMResult,
40 | setCurrentProblemId,
41 | setShowLLMWindow,
42 | showLLMWindow,
43 | currentProblemId
44 | } = useLLMContext();
45 |
46 | const { resetCompletion } = useCompletionContext(); // 获取重置方法
47 | const { clearEditor } = useEditorContext(); // 引入 clearEditor
48 |
49 | // 当选择一个历史记录时
50 | const handleSelectRecord = async (id: number) => {
51 | try {
52 | // 从 IndexedDB 获取完整记录
53 | const { getProblemById } = await import('@/services/recordsIndexDB');
54 | const problem = await getProblemById(id);
55 |
56 | if (problem) {
57 | console.log('加载问题成功:', problem);
58 |
59 | // 更新上下文
60 | setCurrentProblemId(id);
61 | resetCompletion(); // 重置完成状态
62 | clearEditor(); // 清空编辑器内容
63 |
64 | // 构造 DifyResponse 格式的数据
65 | const difyResponse = {
66 | data: {
67 | outputs: problem.data
68 | }
69 | };
70 |
71 | console.log('设置 LLM 结果:', difyResponse);
72 |
73 | // 先设置为 null 然后再设置新值,强制触发组件重新渲染
74 | setLLMResult(null);
75 | setTimeout(() => {
76 | setLLMResult(difyResponse);
77 | setShowLLMWindow(false); // 关闭 LLM 窗口,显示内容
78 | }, 50);
79 | }
80 | } catch (error) {
81 | console.error('加载问题失败:', error);
82 | }
83 | };
84 |
85 | // 当组件挂载或 currentProblemId 变化时刷新列表
86 | useEffect(() => {
87 | refreshRecords();
88 | }, [refreshRecords, currentProblemId]);
89 |
90 | // 过滤记录的函数
91 | const filterRecords = (records: any[]) => {
92 | if (!searchQuery) return records;
93 | return records.filter(record =>
94 | record.title.toLowerCase().includes(searchQuery.toLowerCase())
95 | );
96 | };
97 |
98 | const renderList = (records: any[]) => {
99 | if (loading) {
100 | return ;
101 | }
102 |
103 | if (records.length === 0 && !loading) {
104 | return ;
109 | }
110 |
111 | return (
112 | (
116 |
125 | )}
126 | />
127 | );
128 | };
129 |
130 | const handleNewChat = () => {
131 | // 检查是否已经在新建对话界面
132 | if (showLLMWindow && !currentProblemId) {
133 | messageApi.info('您已处于新建的对话当中');
134 | return;
135 | }
136 |
137 | setLLMResult(null);
138 | setCurrentProblemId(null);
139 | setShowLLMWindow(true);
140 | };
141 |
142 | return (
143 |
144 | {contextHolder}
145 |
146 |
147 |
148 |
149 |
150 | }
153 | className={styles.actionButton}
154 | onClick={handleNewChat}
155 | style={{marginRight:'1em'}}
156 | />
157 |
158 |
159 |
160 |
167 |
168 | 最近
169 |
172 |
173 | ),
174 | children: renderList(filterRecords(recentRecords))
175 | },
176 | {
177 | key: 'favorite',
178 | label: (
179 |
180 |
181 | 收藏
182 |
185 |
186 | ),
187 | children: renderList(filterRecords(favoriteRecords))
188 | },
189 | ]}
190 | />
191 |
192 |
193 | );
194 | };
195 |
196 | export default HistoryPanel;
197 |
--------------------------------------------------------------------------------
/chat-sql/src/components/History/SearchBar.module.css:
--------------------------------------------------------------------------------
1 | .searchContainer {
2 | margin-left: 1rem;
3 | }
4 |
5 | .searchIcon {
6 | cursor: pointer;
7 | font-size: 1em;
8 | padding: 8px;
9 | transition: all 0.3s;
10 | color: var(--icon-color);
11 | }
12 |
13 | .searchIcon:hover {
14 | color: var(--link-color);
15 | }
16 |
17 | .searchInput input::placeholder {
18 | color: var(--tertiary-text) !important;
19 | opacity: 1 !important;
20 | }
21 |
22 | .searchInput {
23 | width: 8em;
24 | transition: all 0.3s;
25 | border-radius: 10px;
26 | background-color: var(--input-bg) ;
27 | border: 1px solid var(--input-border);
28 | }
29 |
30 | .searchInput:hover {
31 | background-color: var(--input-bg);
32 | }
33 |
34 | /* .searchInput:focus {
35 | width: 10em;
36 | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
37 | } */
38 |
--------------------------------------------------------------------------------
/chat-sql/src/components/History/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useEffect, useRef } from 'react';
4 | import { Input, Tooltip } from 'antd';
5 | import { SearchOutlined } from '@ant-design/icons';
6 | import { KeyboardCommandKey } from '@mui/icons-material';
7 | import styles from './SearchBar.module.css';
8 |
9 | interface SearchBarProps {
10 | onSearch: (value: string) => void;
11 | }
12 |
13 | const SearchBar: React.FC = ({ onSearch }) => {
14 | const inputRef = useRef(null);
15 |
16 | useEffect(() => {
17 | const handleKeyPress = (e: KeyboardEvent) => {
18 | if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
19 | e.preventDefault();
20 | inputRef.current?.focus();
21 | }
22 | };
23 |
24 | document.addEventListener('keydown', handleKeyPress);
25 | return () => document.removeEventListener('keydown', handleKeyPress);
26 | }, []);
27 |
28 | return (
29 |
30 |
33 | 搜索
34 | (
35 | +
36 | K )
37 |
38 | }
39 | placement="top"
40 | >
41 | }
45 | onChange={(e) => onSearch(e.target.value)}
46 | className={styles.searchInput}
47 | />
48 |
49 |
50 | );
51 | };
52 |
53 | export default SearchBar;
54 |
--------------------------------------------------------------------------------
/chat-sql/src/components/History/SearchRecords.module.css:
--------------------------------------------------------------------------------
1 | .searchModal {
2 | background-color: var(--card-bg) !important;
3 | border-radius: 8px;
4 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
5 | }
6 |
7 | [data-theme="dark"] .searchModal {
8 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
9 | }
10 |
11 | .searchInput {
12 | width: 100%;
13 | margin-bottom: 16px;
14 | background-color: var(--input-bg) !important;
15 | color: var(--input-text) !important;
16 | border-color: var(--input-border) !important;
17 | }
18 |
19 | .searchInput input {
20 | color: var(--input-text) !important;
21 | }
22 |
23 | .searchInput:hover {
24 | border-color: var(--link-color) !important;
25 | }
26 |
27 | .searchInput:focus-within {
28 | border-color: var(--link-color) !important;
29 | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
30 | }
31 |
32 | [data-theme="dark"] .searchInput:focus-within {
33 | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3);
34 | }
35 |
36 | .resultList {
37 | max-height: 400px;
38 | overflow-y: auto;
39 | }
40 |
41 | .resultItem {
42 | padding: 12px;
43 | border-radius: 8px;
44 | margin-bottom: 8px;
45 | background-color: var(--card-bg);
46 | border: 1px solid var(--card-border);
47 | cursor: pointer;
48 | transition: all 0.2s ease;
49 | }
50 |
51 | .resultItem:hover {
52 | background-color: var(--button-hover);
53 | }
54 |
55 | .resultTitle {
56 | font-weight: 500;
57 | margin-bottom: 4px;
58 | color: var(--primary-text);
59 | }
60 |
61 | .resultDate {
62 | font-size: 12px;
63 | color: var(--tertiary-text);
64 | }
65 |
66 | .noResults {
67 | text-align: center;
68 | padding: 20px;
69 | color: var(--tertiary-text);
70 | }
71 |
72 | .modalHeader {
73 | display: flex;
74 | justify-content: space-between;
75 | align-items: center;
76 | margin-bottom: 16px;
77 | padding-bottom: 8px;
78 | border-bottom: 1px solid var(--divider-color);
79 | }
80 |
81 | .modalTitle {
82 | font-size: 18px;
83 | font-weight: 600;
84 | color: var(--primary-text);
85 | }
86 |
87 | .closeButton {
88 | background: none;
89 | border: none;
90 | cursor: pointer;
91 | color: var(--tertiary-text);
92 | font-size: 20px;
93 | }
94 |
95 | .closeButton:hover {
96 | color: var(--primary-text);
97 | }
98 |
99 | .shortcutHint {
100 | display: flex;
101 | align-items: center;
102 | justify-content: center;
103 | margin-top: 16px;
104 | color: var(--tertiary-text);
105 | font-size: 12px;
106 | }
107 |
108 | .keyboardKey {
109 | display: inline-flex;
110 | align-items: center;
111 | justify-content: center;
112 | min-width: 20px;
113 | height: 20px;
114 | padding: 0 4px;
115 | margin: 0 4px;
116 | background-color: var(--button-hover);
117 | border-radius: 4px;
118 | border: 1px solid var(--card-border);
119 | color: var(--secondary-text);
120 | font-size: 11px;
121 | font-family: var(--font-mono);
122 | }
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/LLMWindow/LLMWindow.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | height: 100%;
3 | position: relative;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | border: none;
8 | background: transparent;
9 | }
10 |
11 | .windowContainer {
12 | padding: 24px;
13 | border: none;
14 | border-radius: 0;
15 | box-shadow: none;
16 | height: 100%;
17 | display: flex;
18 | flex-direction: column;
19 | width: 100%;
20 | margin: 0 auto;
21 | transition: all 0.3s ease;
22 | }
23 |
24 | .contentWrapper {
25 | display: flex;
26 | flex-direction: column;
27 | height: 100%;
28 | justify-content: center; /* 默认竖直居中显示 */
29 | gap: 20px; /* 添加间距 */
30 | transition: all 0.5s ease;
31 | }
32 |
33 | .withResultContent {
34 | justify-content: flex-start; /* 有结果时改为从顶部开始 */
35 | }
36 |
37 | .withResult {
38 | justify-content: flex-start; /* 有结果时改为从顶部开始 */
39 | }
40 |
41 | .headerArea {
42 | display: flex;
43 | align-items: center !important;
44 | justify-content: center!important;
45 | margin-bottom: 24px;
46 | padding-bottom: 12px;
47 | text-align: center;
48 | width: 100%;
49 | transition: all 0.5s ease;
50 | }
51 |
52 | .inputArea {
53 | margin-bottom: 0;
54 | display: flex;
55 | flex-direction: column;
56 | position: relative;
57 | margin-left: auto;
58 | margin-right: auto;
59 | width: 100%;
60 | transition: all 0.5s ease;
61 | }
62 |
63 | .inputAreaWithResult {
64 | margin-top: auto; /* 有结果时将输入区域推到底部 */
65 | }
66 |
67 | .textAreaWrapper {
68 | position: relative;
69 | margin-top: 10px;
70 | min-width: 45vw;
71 | margin-left: auto;
72 | margin-right: auto;
73 | color: var(--card-bg);
74 | }
75 |
76 | .buttonGroup {
77 | display: flex;
78 | align-items: center;
79 | margin-top: 12px;
80 | gap: 2em;
81 | justify-content: center; /* 确保按钮居中对齐 */
82 | }
83 |
84 | .actionButtonContainer {
85 | position: absolute;
86 | right: 8px;
87 | bottom: 24px;
88 | z-index: 2;
89 | }
90 |
91 | .resultArea {
92 | flex: 1;
93 | overflow: auto;
94 | margin-top: 16px;
95 | padding-top: 16px;
96 | max-height: calc(100% - 100px); /* 为输入区域留出空间 */
97 | }
98 |
99 | .chatBubble {
100 | background-color: var(--card-bg);
101 | border-radius: 18px;
102 | padding: 16px;
103 | margin-bottom: 20px;
104 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
105 | position: relative;
106 | max-width: 95%;
107 | margin-left: auto;
108 | margin-right: auto;
109 | border: 1px solid var(--card-border);
110 | }
111 |
112 | .resultHeader {
113 | display: flex;
114 | justify-content: space-between;
115 | align-items: center;
116 | margin-bottom: 16px;
117 | }
118 |
119 | /* 添加一个全局样式容器类 */
120 | .globalStylesContainer {
121 | /* 可以为空,仅用于包装全局样式 */
122 | }
123 |
124 | /* 修复弹出框样式 */
125 | .globalStylesContainer :global(.ant-popover-inner) {
126 | background-color: var(--card-bg) !important;
127 | border: 1px solid var(--card-border) !important;
128 | }
129 |
130 | .globalStylesContainer :global(.ant-popover-title),
131 | .globalStylesContainer :global(.ant-popover-inner-content) {
132 | color: var(--primary-text) !important;
133 | }
134 |
135 | .globalStylesContainer :global(.ant-popover-arrow-content) {
136 | background-color: var(--card-bg) !important;
137 | border: 1px solid var(--card-border) !important;
138 | }
139 |
140 | /* 修复标签输入框样式 */
141 | .tagInputContainer input {
142 | background-color: var(--input-bg) !important;
143 | color: var(--input-text) !important;
144 | border-color: var(--input-border) !important;
145 | }
146 |
147 | .tagInputContainer input::placeholder {
148 | color: var(--secondary-text) !important;
149 | opacity: 1 !important;
150 | }
151 |
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/Container.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useState, useMemo } from 'react';
4 | import { Box, SpeedDial, SpeedDialIcon, SpeedDialAction, Tooltip } from '@mui/material';
5 | import DatabaseFlow from './DatabaseFlow';
6 | import TupleViewer from './TupleViewer';
7 | import ProblemViewer from './ProblemViewer';
8 | import { useLLMContext } from '@/contexts/LLMContext';
9 | import { parseJSONToTables } from '@/lib/parseMySQL';
10 | import SchemaIcon from '@mui/icons-material/Schema';
11 | import TableChartIcon from '@mui/icons-material/TableChart';
12 |
13 | type ViewMode = 'schema' | 'data';
14 |
15 | export const Container: React.FC = () => {
16 | const { llmResult } = useLLMContext();
17 | const [viewMode, setViewMode] = useState('schema');
18 |
19 | // 只在 tableStructure 变化时更新 tables
20 | const tables = useMemo(() => {
21 | if (!llmResult?.data?.outputs?.tableStructure) {
22 | return [];
23 | }
24 | return parseJSONToTables(llmResult.data.outputs.tableStructure.map(table => ({
25 | tableName: table.tableName,
26 | columns: table.columns,
27 | foreignKeys: table.foreignKeys
28 | })));
29 | }, [llmResult?.data?.outputs?.tableStructure]);
30 |
31 | // 使用 useMemo 包装渲染内容,避免不必要的重渲染
32 | const schemaContent = useMemo(() => (
33 |
34 |
35 |
36 | ), [tables]);
37 |
38 | const actions = [
39 | { icon: , name: '数据库结构', value: 'schema' },
40 | { icon: , name: '元组表格', value: 'data' },
41 | ];
42 |
43 | return (
44 |
51 | {/* 左侧区域 */}
52 |
62 | {/* SpeedDial 切换按钮 */}
63 | }
85 | direction="right"
86 | >
87 | {actions.map((action) => (
88 | setViewMode(action.value as ViewMode)}
93 | sx={{
94 | backgroundColor: viewMode === action.value ? 'primary.main' : 'background.paper',
95 | width: '36px', // 设置按钮宽度
96 | height: '36px', // 设置按钮高度
97 | '& .MuiSvgIcon-root': {
98 | color: viewMode === action.value ? 'white' : 'inherit',
99 | fontSize: '20px', // 设置图标大小
100 | },
101 | '& .MuiFab-root': { // 设置内部 Fab 按钮的大小
102 | width: '20px',
103 | height: '20px',
104 | minHeight: 'unset',
105 | }
106 | }}
107 | />
108 | ))}
109 |
110 |
111 | {/* 内容区域 */}
112 |
122 | {viewMode === 'schema' ? (
123 | {/* 添加明确的宽高 */}
124 |
125 |
126 | ) : (
127 |
128 |
129 |
130 | )}
131 |
132 |
133 |
134 | {/* 右侧区域 */}
135 |
145 |
146 |
147 |
148 | );
149 | };
150 |
151 | export default Container;
152 |
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/ProblemViewer.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 20px;
3 | height: 100%;
4 | overflow: auto;
5 | background-color: var(--background);
6 | }
7 |
8 | .problemCard {
9 | background-color: var(--card-bg);
10 | border-radius: 8px;
11 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
12 | padding: 20px;
13 | margin-bottom: 20px;
14 | }
15 |
16 | [data-theme="dark"] .problemCard {
17 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
18 | }
19 |
20 | .title {
21 | font-size: 24px;
22 | font-weight: 600;
23 | margin-bottom: 16px;
24 | color: var(--primary-text);
25 | }
26 |
27 | .description {
28 | font-size: 16px;
29 | line-height: 1.6;
30 | margin-bottom: 20px;
31 | color: var(--secondary-text);
32 | }
33 |
34 | .problemList {
35 | margin-bottom: 20px;
36 | }
37 |
38 | .problemItem {
39 | background-color: var(--button-hover);
40 | border-radius: 6px;
41 | padding: 12px 16px;
42 | margin-bottom: 10px;
43 | color: var(--primary-text);
44 | }
45 |
46 | .hintTitle {
47 | font-size: 18px;
48 | font-weight: 600;
49 | margin-bottom: 10px;
50 | color: var(--primary-text);
51 | }
52 |
53 | .hint {
54 | background-color: rgba(66, 133, 244, 0.1);
55 | border-left: 4px solid var(--link-color);
56 | padding: 12px 16px;
57 | border-radius: 0 6px 6px 0;
58 | color: var(--secondary-text);
59 | }
60 |
61 | .tagContainer {
62 | display: flex;
63 | flex-wrap: wrap;
64 | gap: 8px;
65 | margin-top: 20px;
66 | }
67 |
68 | .tag {
69 | padding: 4px 10px;
70 | border-radius: 16px;
71 | font-size: 12px;
72 | font-weight: 500;
73 | }
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/ProblemViewer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useMemo } from 'react';
4 | import { Box, Typography, Paper, List, ListItem, ListItemText } from '@mui/material';
5 | import AssignmentIcon from '@mui/icons-material/Assignment';
6 | import { useLLMContext } from '@/contexts/LLMContext';
7 | import { useCompletionContext } from '@/contexts/CompletionContext';
8 |
9 | const ProblemViewer: React.FC = () => {
10 | const { llmResult } = useLLMContext();
11 | const { completedProblems } = useCompletionContext();
12 |
13 | // 使用 useMemo 缓存数据,避免不必要的重新计算
14 | const problem = useMemo(() => llmResult?.data?.outputs?.problem || [], [llmResult?.data?.outputs?.problem]);
15 | const description = useMemo(() => llmResult?.data?.outputs?.description || '', [llmResult?.data?.outputs?.description]);
16 |
17 | // 移除调试日志
18 | // console.log('ProblemViewer state:', {
19 | // completedProblems: Array.from(completedProblems),
20 | // problem: llmResult?.data?.outputs?.problem,
21 | // expectedResults: llmResult?.data?.outputs?.expected_result
22 | // });
23 |
24 | return (
25 |
32 |
39 |
44 |
49 |
50 | 查询要求
51 |
52 |
53 |
54 | {description && (
55 |
56 |
65 | 问题描述
66 |
67 |
76 | {description}
77 |
78 |
79 | )}
80 |
81 |
90 | 具体要求
91 |
92 |
93 |
94 | {problem.map((item, index) => (
95 |
116 |
126 |
127 | ))}
128 |
129 |
130 |
131 | );
132 | };
133 |
134 | export default ProblemViewer;
135 |
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/TableDisplay.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useState } from 'react';
4 | import { Box, Paper } from '@mui/material';
5 | import { DataGrid, GridColDef, GridRowsProp } from '@mui/x-data-grid';
6 | import { TableTuple } from '@/types/dify';
7 |
8 | interface TableDisplayProps {
9 | tableInfo: TableTuple;
10 | }
11 |
12 | const TableDisplay: React.FC = ({ tableInfo }) => {
13 | // 添加防御性检查,确保 tableInfo 和 tableInfo.tupleData 存在
14 | const tupleData = tableInfo?.tupleData || [];
15 |
16 | const columns: GridColDef[] = tupleData.length > 0
17 | ? Object.keys(tupleData[0]).map((key) => ({
18 | field: key,
19 | headerName: key,
20 | flex: 1,
21 | minWidth: 80,
22 | align: 'center',
23 | headerAlign: 'center',
24 | }))
25 | : [];
26 |
27 | const rows: GridRowsProp = tupleData.map((row, idx) => ({ id: idx, ...row }));
28 | const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 5 });
29 |
30 | return (
31 |
39 |
81 |
82 | );
83 | };
84 |
85 | export default TableDisplay;
86 |
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/TableNavigator.module.css:
--------------------------------------------------------------------------------
1 | .tableListContainer {
2 | min-width: 200px !important;
3 | max-width: 300px !important;
4 | max-height: 300px !important;
5 | overflow-y: auto;
6 | overflow-x: hidden;
7 | border-radius: 12px;
8 | background: var(--card-bg) !important;
9 | box-shadow: 0 4px 24px rgba(0,0,0,0.08);
10 | padding: 8px 0 8px 0;
11 | color: var(--primary-text);
12 | }
13 |
14 | .searchInput {
15 | padding: 4px 12px;
16 | width: 190px !important;
17 | background-color: var(--button-bg);
18 | border-radius: 15px;
19 | border: none !important;
20 | color: var(--primary-text) !important;
21 | transition: all 0.3s !important;
22 | }
23 |
24 | [data-theme="dark"] .searchInput {
25 | background-color: #504d4d;
26 | }
27 |
28 | .tableList {
29 | padding: 0;
30 | margin: 0;
31 | }
32 |
33 | .tableListItem {
34 | transition: background 0.2s;
35 | border-radius: 8px;
36 | margin: 2px 8px;
37 | }
38 |
39 | .tableListItemButton {
40 | border-radius: 8px;
41 | padding: 6px 18px;
42 | min-width: 0;
43 | justify-content: flex-start;
44 | }
45 |
46 | .tableListItemButton:hover {
47 | background: #f5f7fa;
48 | }
49 |
50 | .tableListText {
51 | font-size: 1.08em;
52 | font-weight: 500;
53 | color: var(--secondary-text);
54 | letter-spacing: 0.01em;
55 | }
56 |
57 | .noMatch {
58 | color: #aaa;
59 | text-align: center;
60 | padding: 16px 0;
61 | font-size: 1em;
62 | }
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/TableNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import { useReactFlow } from '@xyflow/react';
3 | import IconButton from '@mui/material/IconButton';
4 | import List from '@mui/material/List';
5 | import ListItem from '@mui/material/ListItem';
6 | import ListItemButton from '@mui/material/ListItemButton';
7 | import ListItemText from '@mui/material/ListItemText';
8 | import Popover from '@mui/material/Popover';
9 | import Paper from '@mui/material/Paper';
10 | import TextField from '@mui/material/TextField';
11 | import ListIcon from '@mui/icons-material/List';
12 | import InputAdornment from '@mui/material/InputAdornment';
13 | import SearchIcon from '@mui/icons-material/Search';
14 | import { styled } from '@mui/material/styles';
15 | import { Table } from '@/types/database';
16 | import styles from './TableNavigator.module.css';
17 |
18 | // 组件 Props 类型
19 | interface TableNavigatorProps {
20 | tables: Table[];
21 | }
22 |
23 | // 自定义主按钮样式
24 | const FloatingButton = styled(IconButton)(({ theme }) => ({
25 | position: 'absolute',
26 | top: 16,
27 | left: 6,
28 | zIndex: 20,
29 | background: theme.palette.background.paper,
30 | backgroundColor: 'transparent',
31 | color:'var(--secondary-text)',
32 | boxShadow: theme.shadows[3],
33 | '&:hover': {
34 | color:'var(--primary-text)',
35 | },
36 | width: 30,
37 | height: 30,
38 | }));
39 |
40 | // 自定义弹窗样式
41 | const StyledPaper = styled(Paper)(({ theme }) => ({
42 | minWidth: 420,
43 | // maxWidth: 320,
44 | maxHeight: 400,
45 | overflow: 'auto',
46 | borderRadius: 12,
47 | boxShadow: theme.shadows[6],
48 | padding: theme.spacing(1, 0, 1, 0),
49 | }));
50 |
51 | /**
52 | * TableNavigator 组件
53 | * 左上角点击按钮弹出可搜索的表格列表,点击可定位表格
54 | */
55 | export const TableNavigator: React.FC = ({ tables }) => {
56 | const { setCenter, getNode } = useReactFlow();
57 | const [anchorEl, setAnchorEl] = useState(null);
58 | const [search, setSearch] = useState('');
59 | const buttonRef = useRef(null);
60 |
61 | // 过滤表格
62 | const filteredTables = tables.filter(table =>
63 | table.tableName.toLowerCase().includes(search.toLowerCase())
64 | );
65 |
66 | // 定位表格并关闭弹窗
67 | const handleTableClick = (tableId: string) => {
68 | const node = getNode(tableId);
69 | if (node) {
70 | // 尽可能定位到中心
71 | const offsetX = 100;
72 | const offsetY = 100;
73 | setCenter(node.position.x + offsetX, node.position.y + offsetY, {
74 | duration: 500,
75 | zoom: 0.8,
76 | });
77 | }
78 | setAnchorEl(null);
79 | };
80 |
81 | // 点击按钮时打开/关闭弹窗
82 | const handleButtonClick = () => {
83 | if (anchorEl) {
84 | setAnchorEl(null);
85 | } else if (buttonRef.current) {
86 | setAnchorEl(buttonRef.current);
87 | }
88 | };
89 |
90 | // 关闭弹窗
91 | const handlePopoverClose = () => setAnchorEl(null);
92 |
93 | return (
94 | <>
95 | {/* 点击按钮 */}
96 |
102 |
103 |
104 | {/* 弹出面板 */}
105 |
117 | setSearch(e.target.value)}
122 | fullWidth
123 | className={styles.searchInput}
124 | sx={{ m: 1, mb: 0.5, minWidth: 0, maxWidth: '100%' }} // 应用外边距和宽度约束
125 | InputProps={{ // 定制输入框内部样式和内容
126 | startAdornment: ( // 在输入框开头添加搜索图标
127 |
128 |
129 |
130 | ),
131 | sx: { // 应用样式到输入框容器,移除边框、轮廓和下划线
132 | border: 'none',
133 | outline: 'none',
134 | '& fieldset': { border: 'none' }, // 移除 Outline variant 的边框
135 | '&::before, &::after': { display: 'none' }, // 移除 Standard/Filled variants 的下划线
136 | },
137 | }}
138 | />
139 |
140 | {filteredTables.length === 0 ? (
141 |
142 |
143 |
144 | ) : (
145 | filteredTables.map(table => (
146 |
147 | handleTableClick(table.id)} className={styles.tableListItemButton}>
148 |
149 |
150 |
151 | ))
152 | )}
153 |
154 |
155 | >
156 | );
157 | };
158 |
159 | export default TableNavigator;
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/TupleViewer.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 20px;
3 | height: 100%;
4 | overflow: auto;
5 | background-color: var(--background);
6 | }
7 |
8 | .tableContainer {
9 | margin-bottom: 30px;
10 | background-color: var(--card-bg) ;
11 | border-radius: 8px;
12 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
13 | overflow: hidden;
14 | }
15 |
16 | [data-theme="dark"] .tableContainer {
17 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
18 | }
19 |
20 | .tableHeader {
21 | background-color: var(--link-color);
22 | color: var(--card-bg) !important;
23 | padding: 12px 16px;
24 | font-weight: 600;
25 | font-size: 16px;
26 | display: flex;
27 | justify-content: space-between;
28 | align-items: center;
29 | }
30 |
31 | .tableContent {
32 | overflow-x: auto;
33 | }
34 |
35 | .dataTable {
36 | width: 100%;
37 | border-collapse: collapse;
38 | }
39 |
40 | .dataTable th {
41 | background-color: var(--card-bg);
42 | color: var(--primary-text);
43 | font-weight: 600;
44 | text-align: left;
45 | padding: 12px 16px;
46 | border-bottom: 2px solid var(--divider-color);
47 | }
48 |
49 | .dataTable td {
50 | padding: 10px 16px;
51 | border-bottom: 1px solid var(--divider-color);
52 | color: var(--secondary-text);
53 | }
54 |
55 | .dataTable tr:nth-child(even) {
56 | background-color: var(--button-hover);
57 | }
58 |
59 | .dataTable tr:hover {
60 | background-color: var(--button-hover);
61 | }
62 |
63 | .emptyTable {
64 | padding: 20px;
65 | text-align: center;
66 | color: var(--tertiary-text);
67 | }
68 |
69 | .tableCount {
70 | font-size: 14px;
71 | color: rgba(255, 255, 255, 0.8);
72 | }
73 |
74 | .primaryKey {
75 | position: relative;
76 | padding-left: 16px;
77 | }
78 |
79 | .primaryKey::before {
80 | content: "🔑";
81 | position: absolute;
82 | left: 0;
83 | top: 50%;
84 | transform: translateY(-50%);
85 | font-size: 12px;
86 | }
87 |
88 | .foreignKey {
89 | position: relative;
90 | padding-left: 16px;
91 | }
92 |
93 | .foreignKey::before {
94 | content: "🔗";
95 | position: absolute;
96 | left: 0;
97 | top: 50%;
98 | transform: translateY(-50%);
99 | font-size: 12px;
100 | }
--------------------------------------------------------------------------------
/chat-sql/src/components/LLMInteractive/renderedArea/TupleViewer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Box, Typography, IconButton, Paper } from '@mui/material';
4 | import { useLLMContext } from '@/contexts/LLMContext';
5 | import TableDisplay from './TableDisplay';
6 | import { useState, useEffect } from 'react';
7 | import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
8 | import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
9 |
10 | export default function TupleViewer() {
11 | const { llmResult } = useLLMContext();
12 | const [currentIndex, setCurrentIndex] = useState(0);
13 |
14 | const tupleData = llmResult?.data?.outputs?.tuples || [];
15 |
16 | // 确保 currentIndex 在有效范围内
17 | useEffect(() => {
18 | if (currentIndex >= tupleData.length && tupleData.length > 0) {
19 | setCurrentIndex(0);
20 | }
21 | }, [tupleData, currentIndex]);
22 |
23 | const handlePrevious = () => {
24 | setCurrentIndex((prev) => (prev > 0 ? prev - 1 : tupleData.length - 1));
25 | };
26 |
27 | const handleNext = () => {
28 | setCurrentIndex((prev) => (prev < tupleData.length - 1 ? prev + 1 : 0));
29 | };
30 |
31 | if (tupleData.length === 0) {
32 | return (
33 |
39 |
40 | 暂无数据
41 |
42 |
43 | );
44 | }
45 |
46 | return (
47 |
56 | {/* 表名显示区域 - 固定高度和位置 */}
57 |
74 |
75 | {tupleData[currentIndex]?.tableName || '无表名'}
76 |
77 |
78 |
79 | {/* 表格展示区域 */}
80 |
87 |
99 |
100 |
101 |
102 |
112 |
113 |
114 |
115 |
127 |
128 |
129 |
130 |
131 | {/* 页码指示器 */}
132 |
139 | {tupleData.map((_, index) => (
140 | setCurrentIndex(index)}
150 | />
151 | ))}
152 |
153 |
154 | );
155 | }
156 |
--------------------------------------------------------------------------------
/chat-sql/src/components/NavBar/NavBar.module.css:
--------------------------------------------------------------------------------
1 | .navBar {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | height: var(--navbar-height);
7 | background-color: var(--background);
8 | box-shadow: 0 1px 0px rgba(0, 0, 0, 0.1);
9 | display: flex;
10 | align-items: center;
11 | padding: 0 20px;
12 | z-index: 1000;
13 | }
14 |
15 | .leftSection {
16 | flex: 0 0 auto;
17 | }
18 |
19 | .middleSection {
20 | flex: 1;
21 | display: flex;
22 | justify-content: center;
23 | align-items: center;
24 | }
25 |
26 | .rightSection {
27 | flex: 0 0 auto;
28 | display: flex;
29 | gap: 8px;
30 | align-items: center;
31 | }
32 |
33 | .logoContainer {
34 | display: flex;
35 | align-items: center;
36 | gap: 12px;
37 | }
38 |
39 | .logoImage {
40 | width: 30px;
41 | height: 30px;
42 | object-fit: contain;
43 | }
44 |
45 | .logoText {
46 | font-size: 1.2em;
47 | font-weight: 600;
48 | color: #4285f4;
49 | margin: 0;
50 | font-family: 'Maple Mono', monospace;
51 | }
52 |
53 | .navButton{
54 | border-radius: 50%;
55 | border: 1px solid var(--button-border);
56 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
57 | height: 30px;
58 | width: 30px;
59 | color: var(--icon-color) !important;
60 | }
61 |
62 | .navButton:hover {
63 | background-color: var(--button-hover) !important;
64 | color: var(--icon-color-hover) !important;
65 | }
66 |
67 | [data-theme="dark"] .navButton {
68 | color: var(--icon-color-dark) !important;
69 | border-color: var(--button-border);
70 | box-shadow: 0 0 10px rgba(255, 255, 255, 0.05);
71 | }
72 |
73 | [data-theme="dark"] .navButton:hover {
74 | color: var(--icon-color-hover) !important;
75 | }
76 |
--------------------------------------------------------------------------------
/chat-sql/src/components/NavBar/NavBar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react';
4 | import { Button, Tooltip } from 'antd';
5 | import { HomeOutlined, HistoryOutlined } from '@ant-design/icons';
6 | import { useRouter } from 'next/navigation';
7 | import styles from './NavBar.module.css';
8 | import ShareButton from './ShareButton';
9 |
10 | const NavBar: React.FC = () => {
11 | const router = useRouter();
12 |
13 | return (
14 |
15 |
16 |
17 |
22 |
ChatSQL
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | }
36 | onClick={() => router.push('/')}
37 | className={styles.navButton}
38 | />
39 |
40 |
41 | }
44 | onClick={() => router.push('/changelog')}
45 | className={styles.navButton}
46 | />
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default NavBar;
54 |
--------------------------------------------------------------------------------
/chat-sql/src/components/NavBar/ShareButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useState, useRef } from 'react';
4 | import { Button, Tooltip, message, Dropdown, Menu, Upload } from 'antd';
5 | import { ShareAltOutlined, CopyOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons';
6 | import type { MenuProps } from 'antd';
7 | import styles from './NavBar.module.css';
8 |
9 | const ShareButton: React.FC = () => {
10 | const [messageApi, contextHolder] = message.useMessage();
11 | const fileInputRef = useRef(null);
12 |
13 | // 辅助函数:UTF-8 编码为 URL 安全的 Base64
14 | const utf8_to_b64url = (str: string) => {
15 | return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
16 | function (match, p1) {
17 | return String.fromCharCode(parseInt(p1, 16))
18 | }))
19 | .replace(/\+/g, '-')
20 | .replace(/\//g, '_')
21 | .replace(/=+$/, '');
22 | };
23 |
24 | // 辅助函数:URL 安全的 Base64 解码为 UTF-8
25 | const b64url_to_utf8 = (str: string) => {
26 | // 还原标准 Base64
27 | str = str.replace(/-/g, '+').replace(/_/g, '/');
28 | // 添加补位
29 | while (str.length % 4) {
30 | str += '=';
31 | }
32 |
33 | try {
34 | return decodeURIComponent(
35 | atob(str).split('').map(function(c) {
36 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
37 | }).join('')
38 | );
39 | } catch (e) {
40 | throw new Error('解码失败:无效的数据');
41 | }
42 | };
43 |
44 | // 获取数据库数据
45 | const getDatabaseData = () => {
46 | return new Promise((resolve, reject) => {
47 | try {
48 | const request = indexedDB.open('llm_problems_db');
49 |
50 | request.onerror = () => {
51 | reject(new Error('无法访问数据库'));
52 | };
53 |
54 | request.onsuccess = (event) => {
55 | const db = (event.target as IDBOpenDBRequest).result;
56 | const transaction = db.transaction(['problems'], 'readonly');
57 | const store = transaction.objectStore('problems');
58 | const request = store.getAll();
59 |
60 | request.onsuccess = () => {
61 | resolve(request.result);
62 | };
63 |
64 | request.onerror = () => {
65 | reject(new Error('获取数据失败'));
66 | };
67 | };
68 | } catch (error) {
69 | reject(error);
70 | }
71 | });
72 | };
73 |
74 | // 复制分享链接
75 | const handleCopyLink = async () => {
76 | try {
77 | const data = await getDatabaseData();
78 | // 使用 URL 安全的 Base64 编码
79 | const shareData = utf8_to_b64url(JSON.stringify(data));
80 | const shareUrl = `${window.location.origin}/share?data=${shareData}`;
81 | await navigator.clipboard.writeText(shareUrl);
82 | messageApi.success('分享链接已复制到剪贴板');
83 | } catch (error) {
84 | console.error('复制链接失败:', error);
85 | messageApi.error('复制链接失败');
86 | }
87 | };
88 |
89 | // 导出数据到文件
90 | const handleExportFile = async () => {
91 | try {
92 | const data = await getDatabaseData();
93 | const jsonString = JSON.stringify(data, null, 2);
94 | const blob = new Blob([jsonString], { type: 'application/json' });
95 | const url = URL.createObjectURL(blob);
96 |
97 | const a = document.createElement('a');
98 | a.href = url;
99 | a.download = `chatSQL-data-${new Date().toISOString().slice(0, 10)}.json`;
100 | document.body.appendChild(a);
101 | a.click();
102 | document.body.removeChild(a);
103 | URL.revokeObjectURL(url);
104 |
105 | messageApi.success('数据已导出到文件');
106 | } catch (error) {
107 | console.error('导出文件失败:', error);
108 | messageApi.error('导出文件失败');
109 | }
110 | };
111 |
112 | // 处理文件选择
113 | const handleFileSelect = (event: React.ChangeEvent) => {
114 | const file = event.target.files?.[0];
115 | if (!file) return;
116 |
117 | const reader = new FileReader();
118 | reader.onload = async (e) => {
119 | try {
120 | const content = e.target?.result as string;
121 | const data = JSON.parse(content);
122 |
123 | // 打开数据库
124 | const request = indexedDB.open('llm_problems_db');
125 |
126 | request.onerror = () => {
127 | messageApi.error('无法访问数据库');
128 | };
129 |
130 | request.onsuccess = (event) => {
131 | const db = (event.target as IDBOpenDBRequest).result;
132 | const transaction = db.transaction(['problems'], 'readwrite');
133 | const store = transaction.objectStore('problems');
134 |
135 | // 清除现有数据
136 | store.clear().onsuccess = () => {
137 | // 导入处理后的数据
138 | data.forEach((item: any) => {
139 | // 处理日期字段
140 | const processedItem = {
141 | ...item,
142 | createdAt: item.createdAt ? new Date(item.createdAt) : new Date(),
143 | updatedAt: item.updatedAt ? new Date(item.updatedAt) : new Date()
144 | };
145 | store.add(processedItem);
146 | });
147 | };
148 |
149 | transaction.oncomplete = () => {
150 | messageApi.success('数据导入成功');
151 | // 重定向到主页
152 | setTimeout(() => {
153 | window.location.href = '/';
154 | }, 1500);
155 | };
156 | };
157 | } catch (error) {
158 | console.error('导入失败:', error);
159 | messageApi.error('导入失败:文件格式不正确');
160 | }
161 |
162 | // 重置文件输入,以便可以再次选择同一文件
163 | if (fileInputRef.current) {
164 | fileInputRef.current.value = '';
165 | }
166 | };
167 |
168 | reader.readAsText(file);
169 | };
170 |
171 | // 触发文件选择对话框
172 | const handleImportFile = () => {
173 | if (fileInputRef.current) {
174 | fileInputRef.current.click();
175 | }
176 | };
177 |
178 | const items: MenuProps['items'] = [
179 | {
180 | key: '1',
181 | icon: ,
182 | label: '复制分享链接',
183 | onClick: handleCopyLink,
184 | },
185 | {
186 | key: '2',
187 | icon: ,
188 | label: '导出到文件',
189 | onClick: handleExportFile,
190 | },
191 | {
192 | key: '3',
193 | icon: ,
194 | label: '从文件导入',
195 | onClick: handleImportFile,
196 | },
197 | ];
198 |
199 | return (
200 | <>
201 | {contextHolder}
202 |
203 | }
206 | className={styles.navButton}
207 | />
208 |
209 |
216 | >
217 | );
218 | };
219 |
220 | export default ShareButton;
221 |
--------------------------------------------------------------------------------
/chat-sql/src/components/SideBar/GuidingModal.module.css:
--------------------------------------------------------------------------------
1 | .guidingModal {
2 | max-width: 90vw;
3 | }
4 |
5 | .steps {
6 | margin-bottom: 24px;
7 | }
8 |
9 | .stepsContent {
10 | min-height: 300px;
11 | margin-top: 16px;
12 | padding: 20px;
13 | background-color: #fafafa;
14 | border: 1px dashed #e9e9e9;
15 | border-radius: 8px;
16 | position: relative; /* 添加相对定位 */
17 | }
18 |
19 | .stepContent {
20 | display: flex;
21 | flex-direction: column;
22 | }
23 |
24 | .stepContent h3 {
25 | margin-top: 0;
26 | margin-bottom: 16px;
27 | color: #333;
28 | }
29 |
30 | .stepContent p {
31 | margin-bottom: 8px;
32 | color: #666;
33 | }
34 |
35 | .gifContainer {
36 | margin-top: 16px;
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | background: transparent;
41 | width: 100%; /* 添加宽度100% */
42 | }
43 |
44 | .videoPlayer {
45 | width: 100%;
46 | height: auto;
47 | border-radius: 8px;
48 | object-fit: cover; /* 改为cover */
49 | background: transparent; /* 添加透明背景 */
50 | display: block; /* 添加块级显示 */
51 | max-height: 400px; /* 添加最大高度限制 */
52 | }
53 |
54 |
55 | .placeholderImage {
56 | width: 100%;
57 | height: 200px;
58 | background-color: #f0f0f0;
59 | display: flex;
60 | justify-content: center;
61 | align-items: center;
62 | color: #999;
63 | border-radius: 4px;
64 | }
65 |
66 | .stepsAction {
67 | margin-top: 24px;
68 | display: flex;
69 | justify-content: center; /* 居中对齐 */
70 | align-items: center;
71 | gap: 16px; /* 按钮之间的间距 */
72 | }
73 |
74 | .stepsAction button {
75 | width: 30px; /* 固定按钮大小 */
76 | height: 30px;
77 | display: flex;
78 | align-items: center;
79 | justify-content: center;
80 | transition: all 0.3s ease;
81 | }
82 |
83 | .stepsAction button:hover {
84 | transform: scale(1.05); /* 悬停时轻微放大效果 */
85 | }
86 |
--------------------------------------------------------------------------------
/chat-sql/src/components/SideBar/GuidingModal.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useState, type ReactElement } from 'react';
4 | import { Modal, Button, Steps } from 'antd';
5 | import { LeftOutlined, RightOutlined, CheckOutlined } from '@ant-design/icons';
6 | import styles from './GuidingModal.module.css';
7 |
8 | interface GuidingModalProps {
9 | isOpen: boolean;
10 | onClose: () => void;
11 | }
12 |
13 | const { Step } = Steps;
14 |
15 | const GuidingModal: React.FC = ({ isOpen, onClose }) => {
16 | const [currentStep, setCurrentStep] = useState(0);
17 |
18 | const steps = [
19 | {
20 | title: '欢迎使用',
21 | content: (
22 |
23 |
👋 欢迎使用 ChatSQL
24 |
这是一个帮助您学习和使用SQL的交互式工具。
25 |
通过以下步骤,您将了解如何使用本应用的主要功能。
26 |
27 |
28 |
29 |
30 | ),
31 | },
32 | {
33 | title: '创建问题',
34 | content: (
35 |
36 |
如何创建SQL问题
37 |
1. 点击侧边栏的"+"按钮创建新对话
38 |
2. 输入您想要的SQL问题描述
39 |
3. 选择难度级别和标签
40 |
4. 点击提交按钮生成问题
41 |
42 |
49 |
50 | Your browser does not support the video tag.
51 |
52 |
53 |
54 | ),
55 | },
56 | {
57 | title: '查看结果',
58 | content: (
59 |
60 |
查看和保存结果
61 |
1. 系统会生成SQL问题和相应的数据库结构
62 |
2. 您可以查看表结构和关系
63 |
64 |
65 |
66 |
67 | ),
68 | },
69 | {
70 | title: '查询与测试',
71 | content: (
72 |
73 |
编辑和测试SQL
74 |
1. 系统会生成SQL问题和相应的数据库结构
75 |
2. 您可以查看表结构和关系
76 |
3. 在SQL编辑器中编写查询
77 |
4. 点击运行按钮执行查询并点击比较来判断是否正确
78 |
79 |
80 |
81 |
82 | ),
83 | },
84 | ];
85 |
86 | const next = () => {
87 | setCurrentStep(currentStep + 1);
88 | };
89 |
90 | const prev = () => {
91 | setCurrentStep(currentStep - 1);
92 | };
93 |
94 | const handleClose = () => {
95 | setCurrentStep(0);
96 | onClose();
97 | };
98 |
99 | return (
100 |
109 |
110 | {steps.map(item => (
111 |
112 | ))}
113 |
114 |
115 |
116 | {steps[currentStep].content}
117 |
118 |
119 |
120 | {currentStep > 0 && (
121 | }
123 | shape="circle"
124 | onClick={prev}
125 | />
126 | )}
127 | {currentStep < steps.length - 1 && (
128 | }
131 | shape="circle"
132 | onClick={next}
133 | />
134 | )}
135 | {currentStep === steps.length - 1 && (
136 | }
139 | shape="circle"
140 | onClick={handleClose}
141 | />
142 | )}
143 |
144 |
145 | );
146 | };
147 |
148 | export default GuidingModal;
149 |
--------------------------------------------------------------------------------
/chat-sql/src/components/SideBar/SideBar.module.css:
--------------------------------------------------------------------------------
1 | .sideBarContainer {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100%;
5 | background: var(--sidebar-bg);
6 | border-right: 1px solid var(--sidebar-border);
7 | padding: 8px;
8 | }
9 |
10 | .topButtons, .bottomButtons {
11 | display: flex;
12 | flex-direction: column;
13 | gap: 8px;
14 | padding: 8px 0;
15 | }
16 |
17 | .topButtons {
18 | border-bottom: 1px solid var(--sidebar-border);
19 | }
20 |
21 | .bottomButtons {
22 | border-top: 1px solid var(--sidebar-border);
23 | }
24 |
25 | .actionButton {
26 | width: 2em !important;
27 | height: 2em!important;
28 | padding: 0;
29 | display: flex;
30 | align-items: center;
31 | justify-content: center;
32 | border-radius: 8px;
33 | transition: all 0.3s ease;
34 | color: var(--icon-color) !important;
35 | background: transparent;
36 | }
37 |
38 | .actionButton:hover {
39 | background: var(--button-hover);
40 | color: var(--icon-color-hover) !important;
41 | }
42 |
43 | [data-theme="dark"] .actionButton {
44 | color: var(--icon-color-dark) !important;
45 | }
46 |
47 | [data-theme="dark"] .actionButton:hover {
48 | color: var(--icon-color-hover) !important;
49 | }
50 |
51 |
52 | .menuContainer {
53 | flex: 1;
54 | overflow-y: auto;
55 | margin-top:2em;
56 | }
57 |
58 | .menuItems {
59 | padding: 4px 0;
60 | font-size: 14px !important;
61 | }
62 |
63 | .bottomButtons {
64 | display: flex;
65 | flex-direction: column;
66 | gap: 8px;
67 | padding: 8px 0;
68 | }
69 |
70 | .bottomButtons button {
71 | width: 40px;
72 | height: 40px;
73 | padding: 0;
74 | display: flex;
75 | align-items: center;
76 | justify-content: center;
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/chat-sql/src/components/SideBar/SideBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Tooltip, message } from 'antd';
3 | import {
4 | QuestionCircleOutlined,
5 | GithubOutlined,
6 | PlusCircleOutlined,
7 | MenuFoldOutlined,
8 | MenuUnfoldOutlined,
9 | } from '@ant-design/icons';
10 | import { useLLMContext } from '@/contexts/LLMContext';
11 | import styles from './SideBar.module.css';
12 | import GuidingModal from './GuidingModal';
13 | import InitTutorialButton from '../Tutorial/InitTutorialButton';
14 | import ThemeToggle from './ThemeToggle';
15 |
16 | const SideBar: React.FC<{ onToggleHistory?: () => void }> = ({ onToggleHistory }) => {
17 | const [messageApi, contextHolder] = message.useMessage();
18 | const [isGuidingModalOpen, setIsGuidingModalOpen] = useState(false);
19 | const [isHistoryCollapsed, setIsHistoryCollapsed] = useState(false);
20 | const {
21 | setLLMResult,
22 | setCurrentProblemId,
23 | setShowLLMWindow,
24 | showLLMWindow,
25 | currentProblemId,
26 | } = useLLMContext();
27 |
28 | const handleOpenGuide = () => {
29 | setIsGuidingModalOpen(true);
30 | };
31 |
32 | const handleCloseGuide = () => {
33 | setIsGuidingModalOpen(false);
34 | };
35 |
36 | const handleNewChat = () => {
37 | // 检查是否已经在新建对话界面
38 | if (showLLMWindow && !currentProblemId) {
39 | messageApi.info('您已处于新建对话当中');
40 | return;
41 | }
42 |
43 | setLLMResult(null);
44 | setCurrentProblemId(null);
45 | setShowLLMWindow(true);
46 | };
47 |
48 | const handleToggleHistory = () => {
49 | setIsHistoryCollapsed(!isHistoryCollapsed);
50 | onToggleHistory?.();
51 | };
52 |
53 | const handleGithubClick = () => {
54 | window.open('https://github.com/ffy6511/chatSQL', '_blank');
55 | };
56 |
57 | return (
58 |
59 | {contextHolder}
60 |
61 |
62 | }
65 | className={styles.actionButton}
66 | onClick={handleNewChat}
67 | />
68 |
69 |
70 |
71 | : }
74 | className={styles.actionButton}
75 | onClick={handleToggleHistory}
76 | />
77 |
78 |
79 |
80 |
81 |
82 |
86 |
87 |
88 |
89 |
90 | }
93 | className={styles.actionButton}
94 | onClick={handleOpenGuide}
95 | />
96 |
97 |
98 |
99 | }
102 | className={styles.actionButton}
103 | onClick={handleGithubClick}
104 | />
105 |
106 |
107 |
108 |
112 |
113 | );
114 | };
115 |
116 | export default SideBar;
117 |
--------------------------------------------------------------------------------
/chat-sql/src/components/SideBar/ThemeToggle.module.css:
--------------------------------------------------------------------------------
1 | .themeToggleContainer {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | font-size: 14px!important;
6 | }
7 |
8 |
9 | .actionButton svg {
10 | font-size: 14px !important;
11 | /* 直接设置 SVG 的字体大小 */
12 | }
13 |
14 | .actionButton {
15 | padding: 0;
16 | display: flex;
17 | align-items: center;
18 | justify-content: center;
19 | border-radius: 8px;
20 | transition: all 0.3s ease;
21 | color: var(--icon-color) !important;
22 | background: transparent;
23 | }
24 |
25 | .actionButton:hover {
26 | background: var(--button-hover);
27 | }
28 |
29 | [data-theme="dark"] .actionButton {
30 | color: var(--icon-color-dark) !important;
31 | }
32 |
33 | /* 添加全局样式容器 */
34 | .globalStylesContainer {
35 | /* 可以为空,仅用于包装全局样式 */
36 | }
37 |
38 | /* 下拉菜单暗色模式适配 */
39 | .globalStylesContainer [data-theme="dark"] :global(.ant-dropdown-menu) {
40 | background-color: #1f1f1f !important;
41 | border-color: #333 !important;
42 | }
43 |
44 | .globalStylesContainer [data-theme="dark"] :global(.ant-dropdown-menu-item) {
45 | color: #e0e0e0 !important;
46 | }
47 |
48 | .globalStylesContainer [data-theme="dark"] :global(.ant-dropdown-menu-item:hover) {
49 | background-color: #333 !important;
50 | }
51 |
--------------------------------------------------------------------------------
/chat-sql/src/components/SideBar/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useState, useEffect } from 'react';
4 | import { Button, Tooltip, Dropdown } from 'antd';
5 | import LightModeIcon from '@mui/icons-material/LightMode';
6 | import DarkModeIcon from '@mui/icons-material/DarkMode';
7 | import {
8 | DesktopOutlined
9 | } from '@ant-design/icons';
10 | import type { MenuProps } from 'antd';
11 | import styles from './ThemeToggle.module.css';
12 |
13 | type ThemeType = 'light' | 'dark' | 'system';
14 |
15 | const ThemeToggle: React.FC = () => {
16 | const [theme, setTheme] = useState('system');
17 | const [actualTheme, setActualTheme] = useState<'light' | 'dark'>('light');
18 |
19 | // 初始化主题
20 | useEffect(() => {
21 | // 从localStorage读取主题设置
22 | const savedTheme = localStorage.getItem('theme') as ThemeType || 'system';
23 | setTheme(savedTheme);
24 |
25 | // 应用主题
26 | applyTheme(savedTheme);
27 |
28 | // 如果是system主题,添加媒体查询监听器
29 | if (savedTheme === 'system') {
30 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
31 | const handleChange = (e: MediaQueryListEvent) => {
32 | setActualTheme(e.matches ? 'dark' : 'light');
33 | document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
34 | };
35 |
36 | // 初始设置
37 | setActualTheme(mediaQuery.matches ? 'dark' : 'light');
38 |
39 | // 添加监听器
40 | mediaQuery.addEventListener('change', handleChange);
41 | return () => mediaQuery.removeEventListener('change', handleChange);
42 | }
43 | }, []);
44 |
45 | // 应用主题
46 | const applyTheme = (themeType: ThemeType) => {
47 | if (themeType === 'system') {
48 | // 系统
49 | const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
50 | document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
51 | setActualTheme(isDarkMode ? 'dark' : 'light');
52 | } else {
53 | // 手动设置
54 | document.documentElement.setAttribute('data-theme', themeType);
55 | setActualTheme(themeType);
56 | }
57 | };
58 |
59 | // 切换主题
60 | const handleThemeChange = (type: ThemeType) => {
61 | setTheme(type);
62 | localStorage.setItem('theme', type);
63 | applyTheme(type);
64 | };
65 |
66 | // 获取当前主题图标
67 | const getThemeIcon = () => {
68 | if (actualTheme === 'dark') {
69 | return ;
70 | }
71 | return ;
72 | };
73 |
74 | // 下拉菜单项
75 | const items: MenuProps['items'] = [
76 | {
77 | key: 'light',
78 | icon: ,
79 | label: '浅色',
80 | onClick: () => handleThemeChange('light'),
81 | },
82 | {
83 | key: 'dark',
84 | icon: ,
85 | label: '深色',
86 | onClick: () => handleThemeChange('dark'),
87 | },
88 | {
89 | key: 'system',
90 | icon: ,
91 | label: '系统',
92 | onClick: () => handleThemeChange('system'),
93 | },
94 | ];
95 |
96 | return (
97 |
98 |
99 |
104 |
105 |
106 | );
107 | };
108 |
109 | export default ThemeToggle;
110 |
--------------------------------------------------------------------------------
/chat-sql/src/components/SideBar/index.ts:
--------------------------------------------------------------------------------
1 | import SideBar from './SideBar';
2 |
3 | export default SideBar;
4 |
--------------------------------------------------------------------------------
/chat-sql/src/components/Tutorial/InitTutorialButton.module.css:
--------------------------------------------------------------------------------
1 | .actionButton {
2 | width: 2em !important;
3 | height: 2em !important;
4 | padding: 0;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | border-radius: 8px;
9 | transition: all 0.3s ease;
10 | color: #666 !important;
11 | font-size: 0.5em !important;
12 | background: transparent;
13 | }
14 | .actionButton:hover {
15 | background: #f5f5f5;
16 | /* color: #1677ff; */
17 | }
--------------------------------------------------------------------------------
/chat-sql/src/components/Tutorial/InitTutorialButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Tooltip, message } from 'antd';
3 | import { ReadOutlined } from '@ant-design/icons';
4 | import { tutorials } from './tutorialData';
5 | import { useSimpleStorage } from '@/hooks/useRecords';
6 | import { useLLMContext } from '@/contexts/LLMContext';
7 | import { LLMProblem } from '@/services/recordsIndexDB';
8 |
9 |
10 | const InitTutorialButton: React.FC<{ className?: string }> = ({ className }) => {
11 | const [messageApi, contextHolder] = message.useMessage();
12 | const [isProcessing, setIsProcessing] = useState(false);
13 | const { storeProblem } = useSimpleStorage();
14 | const { setCurrentProblemId, setLLMResult } = useLLMContext();
15 |
16 | const handleInitTutorials = async () => {
17 | const { getAllProblems } = await import('@/services/recordsIndexDB');
18 |
19 | try {
20 | setIsProcessing(true);
21 | const existingProblems = await getAllProblems();
22 |
23 | const existingTutorials = new Set(
24 | existingProblems
25 | .filter(p => p.data?.isBuiltIn)
26 | .map(p => p.data?.order)
27 | );
28 |
29 | const missingTutorials = tutorials.filter(t => !existingTutorials.has(t.data.order));
30 |
31 | if (missingTutorials.length === 0) {
32 | messageApi.info('教程已经完整初始化');
33 | return;
34 | }
35 |
36 | // 保存最后一个教程的 ID,用于触发更新
37 | let lastSavedId = null;
38 |
39 | for (const tutorial of missingTutorials) {
40 | const formattedTutorial = {
41 | description: tutorial.description,
42 | problem: tutorial.problem,
43 | hint: tutorial.hint,
44 | tags: tutorial.tags,
45 | tableStructure: tutorial.tableStructure,
46 | tuples: tutorial.tuples,
47 | expected_result: tutorial.expected_result, // 添加预期结果
48 | isBuiltIn: true,
49 | order: tutorial.data.order,
50 | category: tutorial.data.category
51 | };
52 |
53 | // 创建完整的记录对象,包括 isTutorial 标志
54 | const tutorialRecord: LLMProblem = {
55 | title: tutorial.title,
56 | isTutorial: true, // 设置教程标志
57 | createdAt: new Date(),
58 | data: formattedTutorial
59 | };
60 |
61 | lastSavedId = await storeProblem(formattedTutorial, tutorialRecord);
62 | }
63 |
64 | // 使用最后保存的教程 ID 触发更新
65 | if (lastSavedId) {
66 | setCurrentProblemId(lastSavedId);
67 | setLLMResult(null); // 清除之前的结果
68 | }
69 |
70 | messageApi.success(`已添加 ${missingTutorials.length} 个教程`);
71 | } catch (error) {
72 | messageApi.error('教程初始化失败');
73 | console.error('初始化教程失败:', error);
74 | } finally {
75 | setIsProcessing(false);
76 | }
77 | };
78 |
79 | return (
80 | <>
81 | {contextHolder}
82 |
83 | }
86 | className={className}
87 | onClick={handleInitTutorials}
88 | loading={isProcessing}
89 | />
90 |
91 | >
92 | );
93 | };
94 |
95 | export default InitTutorialButton;
96 |
--------------------------------------------------------------------------------
/chat-sql/src/components/codeEditing/EmptyQueryState.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | justify-content: center;
6 | padding: 2rem;
7 | height: 100%;
8 | background-color: var(--card-bg) !important;
9 | border-radius: 8px;
10 | border: 1px dashed var(--card-border) !important;
11 | transition: all 0.3s ease;
12 | }
13 |
14 |
15 | .icon {
16 | font-size: 6rem !important;
17 | color: #f0c14b; /* 金黄色,保持不变 */
18 | opacity: 0.8;
19 | margin-bottom: 1rem;
20 | animation: pulse 2s infinite ease-in-out;
21 | }
22 |
23 | .title {
24 | font-weight: 500;
25 | color: var(--primary-text);
26 | text-align: center;
27 | margin-bottom: 0.5rem;
28 | }
29 |
30 | .subtitle {
31 | color: var(--secondary-text);
32 | text-align: center;
33 | max-width: 80%;
34 | font-size: 0.875rem;
35 | }
36 |
37 | @keyframes pulse {
38 | 0% {
39 | transform: scale(1);
40 | opacity: 0.7;
41 | }
42 | 50% {
43 | transform: scale(1.05);
44 | opacity: 0.9;
45 | }
46 | 100% {
47 | transform: scale(1);
48 | opacity: 0.7;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/chat-sql/src/components/codeEditing/EmptyQueryState.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react';
4 | import { Paper, Typography } from '@mui/material';
5 | import QueryStatsIcon from '@mui/icons-material/QueryStats';
6 | import styles from './EmptyQueryState.module.css';
7 |
8 | /**
9 | * SQL查询区域的空状态组件
10 | * 当没有查询结果时显示
11 | */
12 | const EmptyQueryState: React.FC = () => {
13 | return (
14 |
18 |
19 |
23 | 执行 SQL 查询以查看结果
24 |
25 |
29 | 在右侧编辑器中编写 SQL 语句并点击"执行查询"按钮
30 |
31 |
32 | );
33 | };
34 |
35 | export default EmptyQueryState;
36 |
--------------------------------------------------------------------------------
/chat-sql/src/components/codeEditing/MonacoEditorStyles.css:
--------------------------------------------------------------------------------
1 | /* Monaco 编辑器自定义样式 */
2 |
3 | /* 自动补全窗口 */
4 | .monaco-editor .suggest-widget {
5 | z-index: 9999 !important; /* 确保自动补全窗口显示在最上层 */
6 | }
7 |
8 | /* 悬停提示窗口 */
9 | .monaco-editor .monaco-hover {
10 | z-index: 9999 !important; /* 确保悬停提示窗口显示在最上层 */
11 | }
12 |
13 | /* 参数提示窗口 */
14 | .monaco-editor .parameter-hints-widget {
15 | z-index: 9999 !important; /* 确保参数提示窗口显示在最上层 */
16 | }
17 |
18 | /* 编辑器内容区域 */
19 | .monaco-editor .monaco-editor-background,
20 | .monaco-editor .monaco-editor-background .margin,
21 | .monaco-editor .monaco-editor-background .monaco-editor-hover {
22 | z-index: 1 !important; /* 编辑器背景层级 */
23 | }
24 |
25 | /* 确保编辑器内容可见 */
26 | .monaco-editor .view-lines {
27 | z-index: 2 !important; /* 编辑器文本内容层级 */
28 | }
29 |
30 | /* 确保光标可见 */
31 | .monaco-editor .cursor {
32 | z-index: 3 !important; /* 光标层级 */
33 | }
34 |
35 | /* 自动补全提示文字颜色 - 确保在黑色背景下文字为白色 */
36 | .monaco-editor .suggest-widget .monaco-list .monaco-list-row {
37 | color: #ffffff !important; /* 强制设置为白色 */
38 | }
39 |
40 | .monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label {
41 | color: #ffffff !important; /* 高亮文字也设置为白色 */
42 | }
43 |
44 | .monaco-editor .suggest-widget .monaco-list .monaco-list-row .suggest-icon {
45 | color: #ffffff !important; /* 图标颜色也设置为白色 */
46 | }
47 |
48 | /* 悬停提示文字颜色 */
49 | .monaco-editor .monaco-hover .hover-contents {
50 | color: #ffffff !important; /* 悬停提示文字为白色 */
51 | }
52 |
53 | .monaco-editor .monaco-hover .hover-contents .monaco-tokenized-source {
54 | color: #ffffff !important; /* 代码示例文字为白色 */
55 | }
56 |
57 | /* 参数提示文字颜色 */
58 | .monaco-editor .parameter-hints-widget .parameter-hints-widget-wrapper {
59 | color: #ffffff !important; /* 参数提示文字为白色 */
60 | }
61 |
62 | /* 自动补全详情面板文字颜色 */
63 | .monaco-editor .suggest-widget .details {
64 | color: #ffffff !important; /* 详情面板文字为白色 */
65 | }
66 |
67 | .monaco-editor .suggest-widget .details .header {
68 | color: #ffffff !important; /* 详情面板标题为白色 */
69 | }
70 |
71 | .monaco-editor .suggest-widget .details .body {
72 | color: #ffffff !important; /* 详情面板内容为白色 */
73 | }
74 |
--------------------------------------------------------------------------------
/chat-sql/src/components/codeEditing/QueryResultTable.module.css:
--------------------------------------------------------------------------------
1 | .resultContainer {
2 | height: 100%;
3 | overflow: auto;
4 | background-color: var(--background);
5 | }
6 |
7 | .resultTable {
8 | width: 100%;
9 | border-collapse: collapse;
10 | border-radius: 8px;
11 | overflow:auto;
12 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
13 | }
14 |
15 | [data-theme="dark"] .resultTable {
16 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
17 | }
18 |
19 | .resultTable th {
20 | background-color: var(--card-bg);
21 | color: var(--primary-text);
22 | font-weight: 600;
23 | text-align: left;
24 | padding: 12px 16px;
25 | border-bottom: 2px solid var(--divider-color);
26 | }
27 |
28 | .resultTable td {
29 | padding: 10px 16px;
30 | border-bottom: 1px solid var(--divider-color);
31 | color: var(--secondary-text);
32 | }
33 |
34 | .resultTable tr:nth-child(even) {
35 | background-color: var(--card-bg);
36 | }
37 |
38 | .resultTable tr:hover {
39 | background-color: var(--button-hover);
40 | }
41 |
42 | .emptyResult {
43 | display: flex;
44 | flex-direction: column;
45 | align-items: center;
46 | justify-content: center;
47 | height: 100%;
48 | color: var(--tertiary-text);
49 | text-align: center;
50 | }
51 |
52 | .emptyIcon {
53 | font-size: 48px;
54 | margin-bottom: 16px;
55 | opacity: 0.5;
56 | }
57 |
58 | .resultHeader {
59 | display: flex;
60 | justify-content: space-between;
61 | align-items: center;
62 | margin-bottom: 0;
63 | padding: 16px;
64 | background-color: var(--card-bg);
65 | border-bottom: 1px solid var(--divider-color);
66 | }
67 |
68 | .resultIcon {
69 | color: var(--icon-color);
70 | margin-right: 8px;
71 | }
72 |
73 | .resultTitle {
74 | font-size: 18px;
75 | font-weight: 600;
76 | color: var(--primary-text);
77 | display: flex;
78 | align-items: center;
79 | flex: 1;
80 | }
81 |
82 | .resultCount {
83 | color: var(--tertiary-text);
84 | font-size: 14px;
85 | margin-left: auto;
86 | }
87 |
88 | .tableContainer {
89 | height: calc(100% - 56px); /* 减去头部高度 */
90 | max-height: calc(100vh - 200px); /* 设置最大高度,防止超出视图 */
91 | background-color: var(--card-bg);
92 | overflow: auto; /* 确保内容溢出时可以滚动 */
93 | position: relative; /* 添加相对定位 */
94 | }
95 |
96 | .nullValue {
97 | color: var(--tertiary-text);
98 | font-style: italic;
99 | }
100 |
--------------------------------------------------------------------------------
/chat-sql/src/components/codeEditing/SQLEditor.css:
--------------------------------------------------------------------------------
1 | .sql-editor-container {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 |
7 | .sql-editor {
8 | width: 100%;
9 | height: 100%;
10 | overflow: hidden;
11 | border-radius: 4px;
12 | }
13 |
14 | /* 移除原有的 toolbar 相关样式 */
15 |
16 | /* 快捷键提示样式 */
17 | .shortcut-tooltip {
18 | display: flex;
19 | align-items: center;
20 | padding: 4px 0;
21 | }
22 |
23 | .shortcut-icon {
24 | font-size: 1.5em !important;
25 | margin-left: 4px;
26 | margin-right: 2px;
27 | }
28 |
29 | .shortcut-plus {
30 | margin: 0 2px;
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/chat-sql/src/components/common/CustomTooltip.css:
--------------------------------------------------------------------------------
1 | /* 自定义 Tooltip 样式 */
2 | .custom-tooltip .ant-tooltip-inner {
3 | background-color: var(--card-bg);
4 | color: var(--primary-text);
5 | border: 1px solid var(--card-border);
6 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
7 | }
8 |
9 | /* 自定义 Tooltip 箭头样式 */
10 | .custom-tooltip .ant-tooltip-arrow .ant-tooltip-arrow-content {
11 | background-color: var(--card-bg);
12 | border: 1px solid var(--card-border);
13 | }
14 |
15 | /* 全局覆盖所有 Antd Tooltip 样式 */
16 | :global(.ant-tooltip-inner) {
17 | background-color: var(--card-bg) !important;
18 | color: var(--primary-text) !important;
19 | border: 1px solid var(--card-border) !important;
20 | }
21 |
22 | :global(.ant-tooltip-arrow-content) {
23 | background-color: var(--card-bg) !important;
24 | border: 1px solid var(--card-border) !important;
25 | box-shadow: none !important;
26 | }
27 |
28 | /* 暗色模式下的 Tooltip 样式调整 */
29 | [data-theme="dark"] :global(.ant-tooltip-inner) {
30 | background-color: #1e1e1e !important; /* 使用固定的深色背景 */
31 | color: #f0f0f0 !important; /* 使用固定的浅色文字 */
32 | border-color: #333333 !important;
33 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
34 | }
35 |
36 | [data-theme="dark"] :global(.ant-tooltip-arrow-content) {
37 | background-color: #1e1e1e !important; /* 使用固定的深色背景 */
38 | border-color: #333333 !important;
39 | }
40 |
41 | /* 快捷键提示样式 */
42 | .shortcut-tooltip {
43 | display: flex;
44 | align-items: center;
45 | padding: 4px 0;
46 | color: var(--primary-text);
47 | }
48 |
49 | .shortcut-icon {
50 | font-size: 1em !important;
51 | margin-left: 4px;
52 | margin-right: 2px;
53 | color: var(--primary-text);
54 | }
55 |
56 | .shortcut-plus {
57 | margin: 0 2px;
58 | opacity: 0.7;
59 | }
60 |
61 | /* 确保信息提示图标在暗色模式下可见 */
62 | [data-theme="dark"] :global(.anticon-info-circle) {
63 | color: #1677ff !important;
64 | }
65 |
66 | /* 确保所有 Tooltip 内容在暗色模式下可见 */
67 | [data-theme="dark"] :global(.ant-tooltip-inner *) {
68 | color: #f0f0f0 !important;
69 | }
70 |
--------------------------------------------------------------------------------
/chat-sql/src/components/common/CustomTooltip.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip as AntdTooltip } from 'antd';
3 | import type { TooltipProps } from 'antd';
4 | import './CustomTooltip.css';
5 |
6 | const CustomTooltip: React.FC = (props) => {
7 | return (
8 |
12 | );
13 | };
14 |
15 | export default CustomTooltip;
--------------------------------------------------------------------------------
/chat-sql/src/components/utils/ShinyText.css:
--------------------------------------------------------------------------------
1 | .shiny-text {
2 | background: linear-gradient(120deg,
3 | #000000 20%,
4 | #f07474 50%,
5 | #000000 80%);
6 | background-size: 200% auto;
7 | -webkit-background-clip: text;
8 | background-clip: text;
9 | -webkit-text-fill-color: transparent;
10 | text-shadow: none;
11 | font-weight: 600;
12 | font-size: 1em;
13 | display: inline-block;
14 | animation: shine 2s linear infinite;
15 | }
16 |
17 | @keyframes shine {
18 | 0% {
19 | background-position: 200% center;
20 | }
21 |
22 | 100% {
23 | background-position: -200% center;
24 | }
25 | }
26 |
27 | .shiny-text.disabled {
28 | animation: none;
29 | }
--------------------------------------------------------------------------------
/chat-sql/src/components/utils/ShinyText.js:
--------------------------------------------------------------------------------
1 | import './ShinyText.css';
2 |
3 | const ShinyText = ({ text, disabled = false, speed = 5, className = '', styles = {} }) => {
4 | const animationDuration = `${speed}s`;
5 |
6 | return (
7 |
11 | {text}
12 |
13 | );
14 | };
15 |
16 | export default ShinyText;
17 |
--------------------------------------------------------------------------------
/chat-sql/src/contexts/CompletionContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
4 | import { areResultsEqual } from '@/lib/resultComparator';
5 | import { useLLMContext } from './LLMContext';
6 | import { useQueryContext } from './QueryContext';
7 | import { useEditorContext } from './EditorContext';
8 | import { TableTuple } from '@/types/dify';
9 |
10 | interface CompletionContextType {
11 | completedProblems: Set;
12 | setCompletedProblems: React.Dispatch>>;
13 | checkQueryResult: () => boolean;
14 | resetCompletion: () => void;
15 | }
16 |
17 | const CompletionContext = createContext(null);
18 |
19 | export const useCompletionContext = () => {
20 | const context = useContext(CompletionContext);
21 | if (!context) {
22 | throw new Error('useCompletionContext must be used within CompletionProvider');
23 | }
24 | return context;
25 | };
26 |
27 | export const CompletionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
28 | const [completedProblems, setCompletedProblems] = useState>(new Set());
29 | const { llmResult, currentProblemId } = useLLMContext();
30 | const { queryResult } = useQueryContext();
31 |
32 | // 移除 clearEditor 的依赖,避免循环依赖
33 | const resetCompletion = useCallback(() => {
34 | setCompletedProblems(new Set());
35 | }, []);
36 |
37 | const checkQueryResult = useCallback(() => {
38 | if (!queryResult || !llmResult?.data?.outputs?.expected_result) {
39 | return false;
40 | }
41 |
42 | let isAnyMatch = false;
43 |
44 | llmResult.data.outputs.expected_result.forEach((expected: TableTuple, index: number) => {
45 | if (!expected.tupleData) return;
46 |
47 | try {
48 | const isMatch = areResultsEqual(queryResult, expected.tupleData);
49 | if (isMatch) {
50 | isAnyMatch = true;
51 | setCompletedProblems(prev => {
52 | const newSet = new Set(prev);
53 | newSet.add(index);
54 | return newSet;
55 | });
56 | }
57 | } catch (error) {
58 | console.error(`Error comparing results:`, error);
59 | }
60 | });
61 |
62 | return isAnyMatch;
63 | }, [queryResult, llmResult]);
64 |
65 | // 只在问题ID变化时重置完成状态
66 | useEffect(() => {
67 | if (currentProblemId !== null) {
68 | resetCompletion();
69 | }
70 | }, [currentProblemId, resetCompletion]);
71 |
72 | return (
73 |
79 | {children}
80 |
81 | );
82 | };
83 |
--------------------------------------------------------------------------------
/chat-sql/src/contexts/EditorContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { createContext, useContext, useState } from 'react';
4 |
5 | interface EditorContextType {
6 | sqlEditorValue: string;
7 | setSqlEditorValue: (value: string) => void;
8 | clearEditor: () => void;
9 | }
10 |
11 | const EditorContext = createContext(null);
12 |
13 | export const useEditorContext = () => {
14 | const ctx = useContext(EditorContext);
15 | if (!ctx) throw new Error('useEditorContext must be used within EditorProvider');
16 | return ctx;
17 | };
18 |
19 | export const EditorProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
20 | const [sqlEditorValue, setSqlEditorValue] = useState('');
21 |
22 | const clearEditor = () => {
23 | setSqlEditorValue('');
24 | };
25 |
26 | return (
27 |
32 | {children}
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/chat-sql/src/contexts/LLMContext.tsx:
--------------------------------------------------------------------------------
1 | // LLMContext.tsx
2 | 'use client'
3 |
4 | import React, { createContext, useContext, useState } from 'react';
5 | import { DifyResponse } from '@/types/dify';
6 |
7 | interface LLMContextType {
8 | showLLMWindow: boolean;
9 | setShowLLMWindow: (v: boolean) => void;
10 | llmResult: DifyResponse | null;
11 | setLLMResult: (v: DifyResponse | null) => void;
12 | currentProblemId: number | null;
13 | setCurrentProblemId: (v: number | null) => void;
14 | refreshRecords?: () => void;
15 | }
16 |
17 | const LLMContext = createContext(null);
18 |
19 | export const useLLMContext = () => {
20 | const ctx = useContext(LLMContext);
21 | if (!ctx) throw new Error('useLLMContext must be used within LLMProvider');
22 | return ctx;
23 | };
24 |
25 | export const LLMProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
26 | const [showLLMWindow, setShowLLMWindow] = useState(true);
27 | const [llmResult, setLLMResult] = useState(null);
28 | const [currentProblemId, setCurrentProblemId] = useState(null);
29 |
30 | const refreshRecords = () => {};
31 |
32 | return (
33 |
42 | {children}
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/chat-sql/src/contexts/QueryContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { createContext, useContext, useState } from 'react';
4 |
5 | interface QueryContextType {
6 | queryResult: any[] | null;
7 | setQueryResult: (result: any[] | null) => void;
8 | }
9 |
10 | const QueryContext = createContext(null);
11 |
12 | export const useQueryContext = () => {
13 | const ctx = useContext(QueryContext);
14 | if (!ctx) throw new Error('useQueryContext must be used within QueryProvider');
15 | return ctx;
16 | };
17 |
18 | export const QueryProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
19 | const [queryResult, setQueryResult] = useState(null);
20 |
21 | return (
22 |
26 | {children}
27 |
28 | );
29 | };
--------------------------------------------------------------------------------
/chat-sql/src/contexts/ThemeContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { createContext, useContext, useEffect, useState } from 'react';
4 |
5 | type ThemeContextType = {
6 | theme: string;
7 | setTheme: (theme: string) => void;
8 | toggleTheme: () => void;
9 | };
10 |
11 | const ThemeContext = createContext({
12 | theme: 'light',
13 | setTheme: () => {},
14 | toggleTheme: () => {},
15 | });
16 |
17 | export const useThemeContext = () => useContext(ThemeContext);
18 |
19 | export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
20 | const [mounted, setMounted] = useState(false);
21 | const [theme, setThemeState] = useState('light');
22 |
23 | // 在客户端挂载后获取当前主题
24 | useEffect(() => {
25 | setMounted(true);
26 | const savedTheme = localStorage.getItem('theme') || 'light';
27 | setThemeState(savedTheme);
28 | document.documentElement.setAttribute('data-theme', savedTheme);
29 | }, []);
30 |
31 | const setTheme = (newTheme: string) => {
32 | setThemeState(newTheme);
33 | localStorage.setItem('theme', newTheme);
34 | document.documentElement.setAttribute('data-theme', newTheme);
35 | };
36 |
37 | const toggleTheme = () => {
38 | const newTheme = theme === 'light' ? 'dark' : 'light';
39 | setTheme(newTheme);
40 | };
41 |
42 | // 避免服务器端渲染时的闪烁
43 | if (!mounted) {
44 | return <>{children}>;
45 | }
46 |
47 | return (
48 |
49 | {children}
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/chat-sql/src/hooks/useHistoryRecords.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useEffect, useCallback } from 'react';
4 | import {
5 | getAllProblems,
6 | deleteProblem,
7 | toggleFavorite,
8 | renameProblem,
9 | LLMProblem
10 | } from '@/services/recordsIndexDB';
11 | import { message } from 'antd';
12 |
13 | export const useHistoryRecords = () => {
14 | const [records, setRecords] = useState([]);
15 | const [loading, setLoading] = useState(true);
16 | const [error, setError] = useState(null);
17 |
18 | // 加载所有记录
19 | const loadRecords = useCallback(async () => {
20 | try {
21 | setLoading(true);
22 | setError(null);
23 | const problems = await getAllProblems();
24 | // 按创建时间排序,最新的在前面
25 | problems.sort((a, b) => {
26 | const dateA = new Date(a.createdAt).getTime();
27 | const dateB = new Date(b.createdAt).getTime();
28 | return dateB - dateA;
29 | });
30 | setRecords(problems);
31 | } catch (err) {
32 | setError('加载历史记录失败');
33 | message.error('加载历史记录失败');
34 | console.error('加载历史记录失败:', err);
35 | } finally {
36 | setLoading(false);
37 | }
38 | }, []);
39 |
40 | // 删除记录
41 | const handleDelete = useCallback(async (id: number) => {
42 | try {
43 | await deleteProblem(id);
44 | message.success('删除成功');
45 | // 更新本地状态
46 | setRecords(prev => prev.filter(record => record.id !== id));
47 | } catch (err) {
48 | message.error('删除失败');
49 | console.error('删除失败:', err);
50 | }
51 | }, []);
52 |
53 | // 切换收藏状态
54 | const handleToggleFavorite = useCallback(async (id: number) => {
55 | try {
56 | await toggleFavorite(id);
57 | // 更新本地状态
58 | setRecords(prev => prev.map(record => {
59 | if (record.id === id) {
60 | return { ...record, isFavorite: !record.isFavorite };
61 | }
62 | return record;
63 | }));
64 | } catch (err) {
65 | message.error('更新收藏状态失败');
66 | console.error('更新收藏状态失败:', err);
67 | }
68 | }, []);
69 |
70 | // 重命名记录
71 | const handleRename = useCallback(async (id: number, newTitle: string) => {
72 | try {
73 | await renameProblem(id, newTitle);
74 | message.success('重命名成功');
75 | // 更新本地状态
76 | setRecords(prev => prev.map(record => {
77 | if (record.id === id) {
78 | return { ...record, title: newTitle };
79 | }
80 | return record;
81 | }));
82 | } catch (err) {
83 | message.error('重命名失败');
84 | console.error('重命名失败:', err);
85 | }
86 | }, []);
87 |
88 | // 初始加载
89 | useEffect(() => {
90 | loadRecords();
91 | }, [loadRecords]);
92 |
93 | return {
94 | records,
95 | loading,
96 | error,
97 | recentRecords: records.filter(r => !r.isFavorite),
98 | favoriteRecords: records.filter(r => r.isFavorite),
99 | handleDelete,
100 | handleToggleFavorite,
101 | handleRename,
102 | refreshRecords: loadRecords
103 | };
104 | };
105 |
--------------------------------------------------------------------------------
/chat-sql/src/hooks/useRecords.tsx:
--------------------------------------------------------------------------------
1 |
2 |
3 | import { useState } from 'react';
4 | import { saveLLMProblem, LLMProblem } from '@/services/recordsIndexDB';
5 | import { message } from 'antd';
6 |
7 | export const useSimpleStorage = () => {
8 | const [isSaving, setIsSaving] = useState(false);
9 |
10 | const storeProblem = async (data: any, recordProps: Partial = {}) => {
11 | setIsSaving(true);
12 | try {
13 | // 如果提供了额外的记录属性,使用它们创建完整记录
14 | if (Object.keys(recordProps).length > 0) {
15 | const record: LLMProblem = {
16 | data,
17 | createdAt: new Date(), // 确保提供必需的 createdAt 字段
18 | ...recordProps
19 | };
20 | const id = await saveLLMProblem(record);
21 | message.success('问题保存成功');
22 | return id;
23 | } else {
24 | // 保持原有行为
25 | const id = await saveLLMProblem(data);
26 | message.success('问题保存成功');
27 | return id;
28 | }
29 | } catch (error) {
30 | message.error('保存失败: ' + (error instanceof Error ? error.message : String(error)));
31 | throw error;
32 | } finally {
33 | setIsSaving(false);
34 | }
35 | };
36 |
37 | return {
38 | storeProblem,
39 | isSaving
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/chat-sql/src/hooks/useTagsManager.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useEffect } from 'react';
4 |
5 | const DEFAULT_TAGS = {
6 | 'SELECT': '#f50',
7 | 'JOIN': '#108ee9',
8 | 'GROUP BY': '#87d068',
9 | 'WHERE': '#2db7f5',
10 | 'ORDER BY': '#673ab7',
11 | 'HAVING': '#ff9800',
12 | '子查询': '#795548',
13 | '聚合函数': '#607d8b',
14 | '窗口函数': '#e91e63',
15 | };
16 |
17 | export const useTagsManager = () => {
18 | const [tags, setTags] = useState>({});
19 |
20 | // 初始化标签
21 | useEffect(() => {
22 | const storedTags = localStorage.getItem('tags');
23 | if (storedTags) {
24 | setTags(JSON.parse(storedTags));
25 | } else {
26 | setTags(DEFAULT_TAGS);
27 | localStorage.setItem('tags', JSON.stringify(DEFAULT_TAGS));
28 | }
29 | }, []);
30 |
31 | // 添加新标签
32 | const addTag = (tagName: string) => {
33 | const newTags = {
34 | ...tags,
35 | [tagName]: '#d9d9d9' // 使用统一的默认颜色
36 | };
37 | setTags(newTags);
38 | localStorage.setItem('tags', JSON.stringify(newTags));
39 | };
40 |
41 | // 删除标签
42 | const deleteTag = (tagName: string) => {
43 | const newTags = { ...tags };
44 | delete newTags[tagName];
45 | setTags(newTags);
46 | localStorage.setItem('tags', JSON.stringify(newTags));
47 | };
48 |
49 | return {
50 | tags,
51 | addTag,
52 | deleteTag
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/LLMResultView.module.css:
--------------------------------------------------------------------------------
1 | .resultCard {
2 | max-width: 700px;
3 | margin: 0 auto;
4 | }
5 |
6 | .titleSection {
7 | margin-top: 24px;
8 | color: var(--primary-text);
9 | }
10 |
11 | .tagContainer {
12 | margin-bottom: 16px;
13 | display: flex;
14 | flex-wrap: wrap;
15 | gap: 8px;
16 | }
17 |
18 | .tableContainer {
19 | margin-bottom: 24px;
20 | background-color: var(--card-bg);
21 | border: 1px solid var(--card-border);
22 | border-radius: 8px;
23 | overflow: hidden;
24 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
25 | }
26 |
27 | .tableTitle {
28 | display: block;
29 | text-align: center;
30 | padding: 12px;
31 | background-color: var(--link-color);
32 | color: white;
33 | font-weight: 600;
34 | }
35 |
36 | .tableList {
37 | margin-top: 8px;
38 | padding: 16px;
39 | }
40 |
41 | .listSection {
42 | margin-bottom: 24px;
43 | }
44 |
45 | /* 暗色模式适配 */
46 | [data-theme="dark"] .tableContainer {
47 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
48 | }
49 |
50 | [data-theme="dark"] .tableTitle {
51 | background-color: var(--link-color);
52 | }
53 |
54 | /* 表格样式 */
55 | .tableStyles :global(.ant-table) {
56 | background-color: var(--card-bg) !important;
57 | color: var(--primary-text) !important;
58 | }
59 |
60 | .tableStyles :global(.ant-table-thead > tr > th) {
61 | background-color: var(--button-hover) !important;
62 | color: var(--primary-text) !important;
63 | border-bottom: 1px solid var(--divider-color) !important;
64 | }
65 |
66 | .tableStyles :global(.ant-table-tbody > tr > td) {
67 | border-bottom: 1px solid var(--divider-color) !important;
68 | color: var(--secondary-text) !important;
69 | }
70 |
71 | .tableStyles :global(.ant-table-tbody > tr:hover > td) {
72 | background-color: var(--button-hover) !important;
73 | }
74 |
75 | .tableStyles :global(.ant-typography) {
76 | color: var(--primary-text) !important;
77 | }
78 |
79 | .tableStyles :global(.ant-list-item) {
80 | border-bottom: none !important;
81 | }
82 |
83 | .tableStyles :global(.ant-divider) {
84 | border-top: 1px solid var(--divider-color) !important;
85 | }
86 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/LLMResultView.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | // TODO: 修改可视化, 考虑hint和期望结果的展示
3 |
4 | import React from 'react';
5 | import { Card, Tag, List, Typography, Divider, Table } from 'antd';
6 | import { ProblemOutput, TableStructure, TableTuple } from '@/types/dify';
7 | import styles from './LLMResultView.module.css';
8 |
9 | const { Title, Paragraph, Text } = Typography;
10 |
11 | interface LLMResultViewProps {
12 | outputs: ProblemOutput;
13 | }
14 |
15 | const colorMap = [
16 | 'magenta', 'red', 'volcano', 'orange', 'gold',
17 | 'lime', 'green', 'cyan', 'blue', 'geekblue', 'purple'
18 | ];
19 |
20 | function getTagColor(idx: number) {
21 | return colorMap[idx % colorMap.length];
22 | }
23 |
24 | const renderTable = (table: TableStructure) => (
25 |
26 | {table.tableName}
27 | 字段结构
}
30 | dataSource={table.columns}
31 | renderItem={col => (
32 |
33 | {col.name}
34 | {col.isPrimary && 主键 }
35 | {col.type}
36 |
37 | )}
38 | className={styles.tableList}
39 | />
40 |
41 | );
42 |
43 | const renderTupleTable = (table: TableTuple) => {
44 | // 1. 如果没有数据,直接返回
45 | if (!table.tupleData || table.tupleData.length === 0) return null;
46 |
47 | // 2. 生成 columns
48 | const columns = Object.keys(table.tupleData[0]).map(key => ({
49 | title: {key} ,
50 | dataIndex: key,
51 | key,
52 | render: (value: any) => {String(value)} ,
53 | }));
54 |
55 | return (
56 |
57 |
{table.tableName}
58 |
({ ...row, key: idx }))}
61 | pagination={false}
62 | size="small"
63 | className={styles.tableList}
64 | bordered
65 | />
66 |
67 | );
68 | };
69 |
70 | const LLMResultView: React.FC = ({ outputs }) => {
71 | return (
72 |
73 | {/* 描述 */}
74 |
题目描述
75 |
{outputs.description}
76 |
77 | {/* 题目要求 */}
78 |
题目要求
79 |
(
83 |
90 | {item}
91 |
92 | )}
93 | className={styles.listSection}
94 | />
95 |
96 | {/* 标签 */}
97 | 标签
98 |
99 | {outputs.tags.map((tag, idx) =>
100 | {tag}
106 | )}
107 |
108 |
109 |
110 |
111 | {/* 表结构 */}
112 | {outputs.tableStructure && (
113 | <>
114 | 表结构
115 | {outputs.tableStructure.map(table => renderTable(table))}
116 | >
117 | )}
118 |
119 | {/* 样例数据 */}
120 | {outputs.tuples && (
121 | <>
122 | 样例数据
123 | {outputs.tuples.map(table => renderTupleTable(table))}
124 | >
125 | )}
126 |
127 | {/* 期望结果 */}
128 | {outputs.expected_result && (
129 | <>
130 | 期望结果
131 | {outputs.expected_result.map(table => renderTupleTable(table))}
132 | >
133 | )}
134 |
135 | {/* 提示 */}
136 | {outputs.hint && (
137 | <>
138 |
139 | 提示
140 | {outputs.hint}
141 | >
142 | )}
143 |
144 | );
145 | };
146 |
147 | export default LLMResultView;
148 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/aggregateFunctions.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 聚合函数处理模块
3 | */
4 |
5 | type AggregateFunction = (values: any[]) => number;
6 |
7 | const aggregateFunctions: Record = {
8 | COUNT: (values: any[]) => values.filter(v => v !== null && v !== undefined).length,
9 | SUM: (values: any[]) => values.reduce((sum, val) => sum + (Number(val) || 0), 0),
10 | AVG: (values: any[]) => {
11 | const validValues = values.filter(v => v !== null && v !== undefined);
12 | return validValues.length ? aggregateFunctions.SUM(validValues) / validValues.length : 0;
13 | },
14 | MAX: (values: any[]) => Math.max(...values.map(v => Number(v) || 0)),
15 | MIN: (values: any[]) => Math.min(...values.map(v => Number(v) || 0))
16 | };
17 |
18 | export function executeAggregateFunction(
19 | functionName: string,
20 | values: any[],
21 | columnName?: string
22 | ): number {
23 | const fn = aggregateFunctions[functionName.toUpperCase()];
24 | if (!fn) {
25 | throw new Error(`不支持的聚合函数: ${functionName}`);
26 | }
27 |
28 | // 如果指定了列名,先提取该列的值
29 | const columnValues = columnName
30 | ? values.map(row => {
31 | // 确保能正确获取列值,即使列名带有表前缀
32 | const value = row[columnName];
33 | if (value !== undefined) {
34 | return value;
35 | }
36 |
37 | // 如果直接访问失败,尝试查找匹配的键
38 | // 这种情况可能发生在JOIN操作后,列名可能带有表前缀
39 | console.log(`聚合函数 ${functionName} 尝试查找列 ${columnName} 的值`);
40 | return undefined;
41 | }).filter(v => v !== undefined)
42 | : values;
43 |
44 | console.log(`聚合函数 ${functionName} 处理列 ${columnName},找到 ${columnValues.length} 个有效值`);
45 | return fn(columnValues);
46 | }
47 |
48 | export function isAggregateFunction(expr: any): boolean {
49 | return expr?.type === 'aggr_func' && expr?.name in aggregateFunctions;
50 | }
--------------------------------------------------------------------------------
/chat-sql/src/lib/conditionEvaluator.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 条件评估模块
3 | *
4 | * 该模块提供了用于评估SQL查询条件的函数集合,包括WHERE子句、表达式和比较操作的处理。
5 | */
6 |
7 | /**
8 | * 评估WHERE子句
9 | * @param row 当前数据行
10 | * @param where WHERE子句的AST
11 | * @returns 布尔值,表示该行是否满足WHERE条件
12 | */
13 | export function evaluateWhereClause(row: any, where: any): boolean {
14 | if (!where) return true;
15 | // 直接调用 evaluateCondition
16 | return evaluateCondition(row, where);
17 | }
18 |
19 | /**
20 | * 评估条件表达式
21 | * @param row 当前数据行
22 | * @param condition 条件表达式的AST
23 | * @returns 布尔值,表示该条件是否为真
24 | */
25 | export function evaluateCondition(row: any, condition: any): boolean {
26 | if (!condition) return true;
27 |
28 | console.log('Evaluating condition:', condition);
29 |
30 | // 处理逻辑运算符的特殊情况
31 | if (condition.operator === 'AND' || condition.operator === 'OR') {
32 | const leftResult = evaluateCondition(row, condition.left);
33 | const rightResult = evaluateCondition(row, condition.right);
34 | return condition.operator === 'AND' ? leftResult && rightResult : leftResult || rightResult;
35 | }
36 |
37 | // 处理二元表达式
38 | if (condition.type === 'binary_expr') {
39 | const leftValue = evaluateExpression(condition.left, row);
40 | const rightValue = evaluateExpression(condition.right, row);
41 | console.log('Binary expression values:', {
42 | leftValue,
43 | operator: condition.operator,
44 | rightValue,
45 | leftExpr: condition.left,
46 | rightExpr: condition.right
47 | });
48 | return evaluateComparison(leftValue, condition.operator, rightValue);
49 | }
50 |
51 | console.log('不支持的条件类型:', condition.type);
52 | return false;
53 | }
54 |
55 | /**
56 | * 评估比较操作
57 | * @param left 左侧值
58 | * @param operator 比较运算符
59 | * @param right 右侧值
60 | * @returns 布尔值,表示比较结果
61 | */
62 | export function evaluateComparison(left: any, operator: string, right: any): boolean {
63 | console.log('Comparing:', { left, operator, right }); // 调试日志
64 |
65 | switch (operator.toUpperCase()) {
66 | case '=': return left === right;
67 | case '>': return left > right;
68 | case '<': return left < right;
69 | case '>=': return left >= right;
70 | case '<=': return left <= right;
71 | case '<>':
72 | case '!=': return left !== right;
73 | case 'LIKE': return evaluateLike(left, right);
74 | case 'IN': return Array.isArray(right) && right.includes(left);
75 | case 'IS': return (left === null && right === null) || left === right;
76 | case 'IS NOT': return (left !== null || right !== null) && left !== right;
77 | default:
78 | console.log('Unsupported operator:', operator);
79 | return false;
80 | }
81 | }
82 |
83 | /**
84 | * 评估表达式并返回其值
85 | * @param expr 表达式的AST
86 | * @param row 当前数据行(可选)
87 | * @returns 表达式的值
88 | */
89 | export function evaluateExpression(expr: any, row?: any): any {
90 | if (!expr) return null;
91 |
92 | console.log('Evaluating expression:', expr);
93 |
94 | // 处理数字字面量
95 | if (expr.type === 'number') {
96 | return expr.value;
97 | }
98 |
99 | // 处理字符串字面量
100 | if (expr.type === 'string') {
101 | return expr.value;
102 | }
103 |
104 | // 处理布尔字面量
105 | if (expr.type === 'bool') {
106 | return expr.value;
107 | }
108 |
109 | // 处理NULL字面量
110 | if (expr.type === 'null') {
111 | return null;
112 | }
113 |
114 | // 处理聚合函数
115 | if (expr.type === 'aggr_func') {
116 | if (!row) return null;
117 |
118 | const funcName = expr.name;
119 | let argName = '*';
120 |
121 | if (expr.args && expr.args.expr) {
122 | if (expr.args.expr.type === 'column_ref') {
123 | argName = expr.args.expr.column;
124 | } else if (expr.args.expr.type === 'star') {
125 | argName = '*';
126 | }
127 | }
128 |
129 | // 尝试从行中获取聚合函数的结果
130 | const funcKey = `${funcName}(${argName})`;
131 |
132 | // 1. 尝试精确匹配
133 | if (row[funcKey] !== undefined) {
134 | console.log(`找到聚合函数结果 ${funcKey}:`, row[funcKey]);
135 | return row[funcKey];
136 | }
137 |
138 | // 2. 尝试别名匹配
139 | if (funcName === 'COUNT' && row['student_count'] !== undefined) {
140 | console.log(`找到COUNT别名 student_count:`, row['student_count']);
141 | return row['student_count'];
142 | }
143 |
144 | if (funcName === 'AVG' && row['avg_age'] !== undefined) {
145 | console.log(`找到AVG别名 avg_age:`, row['avg_age']);
146 | return row['avg_age'];
147 | }
148 |
149 | // 3. 尝试模糊匹配
150 | const possibleKey = Object.keys(row).find(k =>
151 | k.startsWith(`${funcName}(`) ||
152 | (funcName === 'COUNT' && k.includes('count')) ||
153 | (funcName === 'AVG' && k.includes('avg'))
154 | );
155 |
156 | if (possibleKey) {
157 | console.log(`找到可能的聚合函数结果 ${possibleKey}:`, row[possibleKey]);
158 | return row[possibleKey];
159 | }
160 |
161 | console.log(`未找到聚合函数 ${funcKey} 的结果`);
162 | return null;
163 | }
164 |
165 | // 处理子查询
166 | if (expr.ast) {
167 | console.log('检测到子查询表达式:', expr);
168 | return { __isSubQuery: true, ast: expr.ast };
169 | }
170 |
171 | if (expr.type === 'column_ref') {
172 | if (!row) return null;
173 |
174 | const columnName = expr.column;
175 | const tableAlias = expr.table;
176 |
177 | // 1. 如果有表别名,先尝试完整的"表别名.列名"形式
178 | if (tableAlias) {
179 | const fullColumnName = `${tableAlias}.${columnName}`;
180 | if (row[fullColumnName] !== undefined) {
181 | console.log(`找到列 ${fullColumnName}:`, row[fullColumnName]);
182 | return row[fullColumnName];
183 | }
184 | }
185 |
186 | // 2. 尝试直接匹配列名(不带表别名)
187 | if (row[columnName] !== undefined) {
188 | console.log(`找到列 ${columnName}:`, row[columnName]);
189 | return row[columnName];
190 | }
191 |
192 | // 3. 尝试查找任何表别名下的该列名
193 | const matchingKey = Object.keys(row).find(k =>
194 | k.endsWith(`.${columnName}`)
195 | );
196 |
197 | if (matchingKey) {
198 | console.log(`找到匹配列 ${matchingKey}:`, row[matchingKey]);
199 | return row[matchingKey];
200 | }
201 |
202 | console.log(`列 ${tableAlias ? tableAlias + '.' : ''}${columnName} 未找到`);
203 | return null;
204 | }
205 |
206 | return expr;
207 | }
208 |
209 | /**
210 | * 评估LIKE操作符
211 | * @param value 要比较的值
212 | * @param pattern LIKE模式
213 | * @returns 布尔值,表示是否匹配
214 | */
215 | export function evaluateLike(value: string, pattern: string): boolean {
216 | console.log('Evaluating LIKE:', { value, pattern });
217 |
218 | if (typeof value !== 'string' || typeof pattern !== 'string') {
219 | console.log('Invalid types for LIKE:', {
220 | valueType: typeof value,
221 | patternType: typeof pattern,
222 | value,
223 | pattern
224 | });
225 | return false;
226 | }
227 |
228 | try {
229 | // 将SQL LIKE模式转换为正则表达式
230 | let regexPattern = pattern
231 | .replace(/%/g, '.*') // % 匹配任意字符序列
232 | .replace(/_/g, '.'); // _ 匹配单个字符
233 |
234 | // 转义特殊字符,但保留已转换的 .* 和 .
235 | regexPattern = regexPattern
236 | .split('.*').map(part =>
237 | part.split('.').map(subPart =>
238 | subPart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
239 | ).join('.')
240 | ).join('.*');
241 |
242 | console.log('Converted regex pattern:', regexPattern);
243 |
244 | // 使用 RegExp 构造函数,不添加 ^ 和 $,允许部分匹配
245 | const regex = new RegExp(regexPattern);
246 | const result = regex.test(value);
247 |
248 | console.log('LIKE evaluation result:', {
249 | value,
250 | pattern,
251 | regexPattern,
252 | result
253 | });
254 |
255 | return result;
256 | } catch (error) {
257 | console.error('Error in LIKE evaluation:', error);
258 | return false;
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/constraintValidator.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 约束验证模块
3 | *
4 | * 该模块提供了用于验证数据库约束(如主键、外键)的函数集合。
5 | */
6 |
7 | import { TableData } from '../types/sqlExecutor';
8 |
9 | /**
10 | * 验证主键约束
11 | * @param table 表数据
12 | * @param row 要验证的数据行
13 | * @throws 如果违反主键约束,则抛出错误
14 | */
15 | export function validatePrimaryKey(table: TableData, row: any): void {
16 | const primaryKeys = table.structure.columns
17 | .filter(col => col.isPrimary)
18 | .map(col => col.name);
19 |
20 | if (primaryKeys.length === 0) return;
21 |
22 | const existingRow = table.data.find(existing =>
23 | primaryKeys.every(key => existing[key] === row[key])
24 | );
25 |
26 | if (existingRow) {
27 | throw new Error('违反主键约束');
28 | }
29 | }
30 |
31 | /**
32 | * 验证外键约束
33 | * @param table 表数据
34 | * @param row 要验证的数据行
35 | * @param getTable 获取表的函数
36 | * @throws 如果违反外键约束,则抛出错误
37 | */
38 | export function validateForeignKeys(
39 | table: TableData,
40 | row: any,
41 | getTable: (tableName: string) => TableData | undefined
42 | ): void {
43 | // 实现外键约束验证
44 | table.structure.columns.forEach(column => {
45 | if (column.foreignKeyRefs) {
46 | column.foreignKeyRefs.forEach(ref => {
47 | const refTable = getTable(ref.tableName);
48 | if (!refTable) {
49 | throw new Error(`引用的表 ${ref.tableName} 不存在`);
50 | }
51 |
52 | const value = row[column.name];
53 | const exists = refTable.data.some(refRow =>
54 | refRow[ref.columnName] === value
55 | );
56 |
57 | if (!exists) {
58 | throw new Error(`违反外键约束: ${column.name}`);
59 | }
60 | });
61 | }
62 | });
63 | }
64 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * SQL执行器辅助函数索引
3 | */
4 |
5 | // 条件评估
6 | export * from './conditionEvaluator';
7 |
8 | // 约束验证
9 | export * from './constraintValidator';
10 |
11 | // 查询辅助
12 | export * from './queryHelpers';
13 |
14 | // 事务管理
15 | export * from './transactionManager';
16 |
17 | // 聚合函数
18 | export * from './aggregateFunctions';
19 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/parseMySQL.tsx:
--------------------------------------------------------------------------------
1 | // 用于解析JSON格式的表结构, 为可视化组件提供输入
2 | import { InputTable, Table } from '@/types/database';
3 |
4 | // 将 JSON 数组转换为 Table 数组的函数
5 | export const parseJSONToTables = (jsonTables: InputTable[]): Table[] => {
6 | const tableNameToId: Map = new Map();
7 | const tables: Table[] = [];
8 |
9 | // 第一步:建立表名到ID的映射
10 | jsonTables.forEach((inputTable, index) => {
11 | const tableId = `table-${index}`;
12 | tableNameToId.set(inputTable.tableName, tableId);
13 | });
14 |
15 | // 第二步:创建基本表结构
16 | jsonTables.forEach((inputTable, index) => {
17 | const tableId = tableNameToId.get(inputTable.tableName)!;
18 |
19 | tables.push({
20 | id: tableId,
21 | tableName: inputTable.tableName,
22 | position: { x: index * 350, y: 100 },
23 | columns: inputTable.columns.map((col) => ({
24 | name: col.name,
25 | type: col.type,
26 | isPrimary: col.isPrimary,
27 | foreignKeyRefs: undefined // 初始化为 undefined
28 | })),
29 | isReferenced: false
30 | });
31 | });
32 |
33 | // 第三步:处理外键关系
34 | jsonTables.forEach((inputTable) => {
35 | if (inputTable.foreignKeys) {
36 | inputTable.foreignKeys.forEach((fk) => {
37 | const sourceTableId = tableNameToId.get(fk.fromTable);
38 | const targetTableId = tableNameToId.get(fk.toTable);
39 |
40 | if (sourceTableId && targetTableId) {
41 | // 找到源表和源列
42 | const sourceTable = tables.find(t => t.id === sourceTableId);
43 | if (sourceTable) {
44 | const sourceColumn = sourceTable.columns.find(c => c.name === fk.fromColumn);
45 | if (sourceColumn) {
46 | // 设置外键引用
47 | sourceColumn.foreignKeyRefs = [{
48 | tableId: targetTableId, // 使用 tableId 而不是 tableName
49 | columnName: fk.toColumn
50 | }];
51 | }
52 | }
53 |
54 | // 标记目标表为被引用
55 | const targetTable = tables.find(t => t.id === targetTableId);
56 | if (targetTable) {
57 | targetTable.isReferenced = true;
58 | }
59 | }
60 | });
61 | }
62 | });
63 |
64 | return tables;
65 | };
66 |
67 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/resultComparator.ts:
--------------------------------------------------------------------------------
1 | export function areResultsEqual(actual: any[], expected: any[]): boolean {
2 | console.log('Comparing results:', { actual, expected });
3 |
4 | if (!actual || !expected) {
5 | console.log('Invalid input: actual or expected is null/undefined');
6 | return false;
7 | }
8 |
9 | if (actual.length !== expected.length) {
10 | console.log('Length mismatch:', {
11 | actualLength: actual.length,
12 | expectedLength: expected.length
13 | });
14 | return false;
15 | }
16 |
17 | // 标准化行数据
18 | function normalizeRow(row: any): any {
19 | const normalized: any = {};
20 | for (const [key, value] of Object.entries(row)) {
21 | // 转换键为小写并移除空格
22 | const normalizedKey = String(key).toLowerCase().trim();
23 | // 转换值为字符串并移除前后空格
24 | const normalizedValue = value === null ? 'null' : String(value).trim();
25 | normalized[normalizedKey] = normalizedValue;
26 | }
27 | return normalized;
28 | }
29 |
30 | // 标准化并排序两个结果集
31 | const normalizedActual = actual.map(normalizeRow)
32 | .sort((a, b) => JSON.stringify(a) > JSON.stringify(b) ? 1 : -1);
33 | const normalizedExpected = expected.map(normalizeRow)
34 | .sort((a, b) => JSON.stringify(a) > JSON.stringify(b) ? 1 : -1);
35 |
36 | // 详细的比较日志
37 | console.log('Normalized results:', {
38 | actual: normalizedActual,
39 | expected: normalizedExpected
40 | });
41 |
42 | // 逐行比较
43 | for (let i = 0; i < normalizedActual.length; i++) {
44 | const actualRow = normalizedActual[i];
45 | const expectedRow = normalizedExpected[i];
46 |
47 | // 检查键是否匹配
48 | const actualKeys = Object.keys(actualRow).sort();
49 | const expectedKeys = Object.keys(expectedRow).sort();
50 |
51 | if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) {
52 | console.log('Column mismatch:', {
53 | actualColumns: actualKeys,
54 | expectedColumns: expectedKeys
55 | });
56 | return false;
57 | }
58 |
59 | // 检查值是否匹配
60 | for (const key of actualKeys) {
61 | if (actualRow[key] !== expectedRow[key]) {
62 | console.log('Value mismatch:', {
63 | column: key,
64 | actualValue: actualRow[key],
65 | expectedValue: expectedRow[key]
66 | });
67 | return false;
68 | }
69 | }
70 | }
71 |
72 | return true;
73 | }
74 |
--------------------------------------------------------------------------------
/chat-sql/src/lib/sqlHoverProvider.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * SQL编辑器悬停提示提供器
3 | *
4 | * 该模块提供了基于当前表结构生成SQL悬停提示的功能
5 | */
6 |
7 | import { TableStructure } from '@/types/dify';
8 |
9 | /**
10 | * SQL关键字列表,包含详细的用法说明
11 | * 从sqlCompletionProvider.ts导入以保持一致性
12 | */
13 | import { SQL_KEYWORDS } from './sqlCompletionProvider';
14 |
15 | /**
16 | * 创建SQL悬停提示提供器
17 | * 当用户将鼠标悬停在SQL关键字、表名或列名上时,显示相关文档
18 | * @param monaco Monaco编辑器实例
19 | * @param tableStructures 表结构数组
20 | * @returns 悬停提示提供器
21 | */
22 | export function createSQLHoverProvider(monaco: any, tableStructures: TableStructure[] | undefined) {
23 | return {
24 | provideHover: (model: any, position: any) => {
25 | // 获取当前光标所在的单词
26 | const word = model.getWordAtPosition(position);
27 | if (!word) return null;
28 |
29 | const wordText = word.word.toUpperCase();
30 |
31 | // 获取当前行内容,用于检测多词关键字
32 | const lineContent = model.getLineContent(position.lineNumber);
33 | const lineUntilPosition = lineContent.substring(0, position.column);
34 | const lineAfterPosition = lineContent.substring(position.column);
35 |
36 | // 检查是否是SQL关键字
37 | for (const keyword of SQL_KEYWORDS) {
38 | if (keyword.label.toUpperCase() === wordText) {
39 | return {
40 | contents: [
41 | { value: `**${keyword.label}**` },
42 | { value: keyword.documentation }
43 | ],
44 | range: {
45 | startLineNumber: position.lineNumber,
46 | endLineNumber: position.lineNumber,
47 | startColumn: word.startColumn,
48 | endColumn: word.endColumn
49 | }
50 | };
51 | }
52 |
53 | // 向前查找可能的关键字开始
54 | if (keyword.label.includes(' ')) {
55 | const parts = keyword.label.split(' ');
56 | // 如果当前单词匹配多词关键字的任何部分
57 | if (parts.some(part => part.toUpperCase() === wordText)) {
58 | // 尝试在当前行查找完整的多词关键字
59 | const keywordPattern = new RegExp(`\\b${keyword.label.replace(/ /g, '\\s+')}\\b`, 'i');
60 | const match = lineContent.match(keywordPattern);
61 |
62 | if (match) {
63 | const startIndex = match.index || 0;
64 | const endIndex = startIndex + match[0].length;
65 |
66 | // 检查当前光标是否在多词关键字范围内
67 | const cursorPos = position.column - 1; // 转为0-based索引
68 | if (cursorPos >= startIndex && cursorPos <= endIndex) {
69 | return {
70 | contents: [
71 | { value: `**${keyword.label}**` },
72 | { value: keyword.documentation }
73 | ],
74 | range: {
75 | startLineNumber: position.lineNumber,
76 | endLineNumber: position.lineNumber,
77 | startColumn: startIndex + 1, // 转回1-based索引
78 | endColumn: endIndex + 1
79 | }
80 | };
81 | }
82 | }
83 | }
84 | }
85 | }
86 |
87 | // 如果没有表结构,只检查关键字
88 | if (!tableStructures || tableStructures.length === 0) {
89 | return null;
90 | }
91 |
92 | // 检查是否是表名
93 | const table = tableStructures.find(t => t.tableName.toUpperCase() === wordText);
94 | if (table) {
95 | const columnsList = table.columns.map(col =>
96 | `- **${col.name}** (${col.type})${col.isPrimary ? ' [Primary Key]' : ''}`
97 | ).join('\n');
98 |
99 | return {
100 | contents: [
101 | { value: `**Table: ${table.tableName}**` },
102 | { value: `Contains the following columns:\n${columnsList}` }
103 | ],
104 | range: {
105 | startLineNumber: position.lineNumber,
106 | endLineNumber: position.lineNumber,
107 | startColumn: word.startColumn,
108 | endColumn: word.endColumn
109 | }
110 | };
111 | }
112 |
113 | // 检查是否是列名(不带表名前缀)
114 | for (const table of tableStructures) {
115 | const column = table.columns.find(col => col.name.toUpperCase() === wordText);
116 | if (column) {
117 | return {
118 | contents: [
119 | { value: `**Column: ${column.name}**` },
120 | { value: `Type: ${column.type}${column.isPrimary ? ' [Primary Key]' : ''}\nFrom table: ${table.tableName}` }
121 | ],
122 | range: {
123 | startLineNumber: position.lineNumber,
124 | endLineNumber: position.lineNumber,
125 | startColumn: word.startColumn,
126 | endColumn: word.endColumn
127 | }
128 | };
129 | }
130 | }
131 |
132 | // 检查是否是带表名前缀的列名(如 table.column)
133 | const dotMatch = lineContent.substring(0, position.column).match(/([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)$/);
134 | if (dotMatch) {
135 | const tableName = dotMatch[1];
136 | const columnName = dotMatch[2];
137 |
138 | const table = tableStructures.find(t => t.tableName.toUpperCase() === tableName.toUpperCase());
139 | if (table) {
140 | const column = table.columns.find(col => col.name.toUpperCase() === columnName.toUpperCase());
141 | if (column) {
142 | return {
143 | contents: [
144 | { value: `**Column: ${column.name}**` },
145 | { value: `Type: ${column.type}${column.isPrimary ? ' [Primary Key]' : ''}\nFrom table: ${table.tableName}` }
146 | ],
147 | range: {
148 | startLineNumber: position.lineNumber,
149 | endLineNumber: position.lineNumber,
150 | startColumn: word.startColumn - tableName.length - 1,
151 | endColumn: word.endColumn
152 | }
153 | };
154 | }
155 | }
156 | }
157 |
158 | return null;
159 | }
160 | };
161 | }
--------------------------------------------------------------------------------
/chat-sql/src/lib/transactionManager.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 事务管理模块
3 | *
4 | * 该模块提供了用于管理SQL事务的函数集合,包括开始、提交和回滚事务。
5 | */
6 |
7 | import { TableData } from '../types/sqlExecutor';
8 |
9 | /**
10 | * 开始事务
11 | * @param tables 当前表集合
12 | * @param transactionData 当前事务数据(如果存在)
13 | * @returns 事务操作结果和新的事务数据
14 | */
15 | export function beginTransaction(
16 | tables: Map,
17 | transactionData: Map | null
18 | ): {
19 | result: { success: boolean; message: string },
20 | newTransactionData: Map
21 | } {
22 | if (transactionData) {
23 | throw new Error('事务已经开始');
24 | }
25 |
26 | // 创建事务数据的深拷贝
27 | const newTransactionData = new Map(
28 | Array.from(tables.entries()).map(([name, table]) => [
29 | name,
30 | {
31 | structure: { ...table.structure },
32 | data: [...table.data]
33 | }
34 | ])
35 | );
36 |
37 | return {
38 | result: { success: true, message: '事务开始' },
39 | newTransactionData
40 | };
41 | }
42 |
43 | /**
44 | * 提交事务
45 | * @param transactionData 当前事务数据
46 | * @returns 事务操作结果
47 | */
48 | export function commitTransaction(
49 | transactionData: Map | null
50 | ): { success: boolean; message: string } {
51 | if (!transactionData) {
52 | throw new Error('没有活动的事务');
53 | }
54 |
55 | return { success: true, message: '事务提交成功' };
56 | }
57 |
58 | /**
59 | * 回滚事务
60 | * @param transactionData 当前事务数据
61 | * @returns 事务操作结果和回滚后的表数据
62 | */
63 | export function rollbackTransaction(
64 | transactionData: Map | null
65 | ): {
66 | result: { success: boolean; message: string },
67 | tables: Map | null
68 | } {
69 | if (!transactionData) {
70 | throw new Error('没有活动的事务');
71 | }
72 |
73 | return {
74 | result: { success: true, message: '事务回滚成功' },
75 | tables: new Map(transactionData)
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/chat-sql/src/services/recordsIndexDB.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export type LLMProblem = {
4 | id?: number; // IndexedDB自动生成的键
5 | data: any; // 直接存储整个LLM返回的数据
6 | createdAt: Date; // 必需字段
7 | title?: string; // 自定义标题
8 | isFavorite?: boolean; // 是否收藏
9 | isTutorial?: boolean; // 是否为教程
10 | };
11 |
12 | const DB_NAME = 'llm_problems_db';
13 | const DB_VERSION = 1;
14 | const STORE_NAME = 'problems';
15 |
16 | // 初始化数据库
17 | export const initDB = (): Promise => {
18 | return new Promise((resolve, reject) => {
19 | const request = indexedDB.open(DB_NAME, DB_VERSION);
20 |
21 | request.onerror = () => {
22 | reject('IndexedDB 打开失败');
23 | };
24 |
25 | request.onsuccess = () => {
26 | resolve(request.result);
27 | };
28 |
29 | request.onupgradeneeded = () => {
30 | const db = request.result;
31 | if (!db.objectStoreNames.contains(STORE_NAME)) {
32 | db.createObjectStore(STORE_NAME, {
33 | keyPath: 'id',
34 | autoIncrement: true
35 | });
36 | }
37 | };
38 | });
39 | };
40 |
41 | // 保存问题数据
42 | export const saveLLMProblem = async (data: any): Promise => {
43 | try {
44 | const db = await initDB();
45 | return new Promise((resolve, reject) => {
46 | // 从描述中提取前15个字符作为默认标题
47 | const defaultTitle = data.description ?
48 | (data.description.length > 15 ? data.description.substring(0, 15) + '...' : data.description) :
49 | '无标题问题';
50 |
51 | const problemData = {
52 | data,
53 | createdAt: new Date(),
54 | title: defaultTitle,
55 | isFavorite: false
56 | };
57 |
58 | const transaction = db.transaction([STORE_NAME], 'readwrite');
59 | const store = transaction.objectStore(STORE_NAME);
60 | const request = store.add(problemData);
61 |
62 | request.onsuccess = () => {
63 | resolve(request.result as number);
64 | };
65 |
66 | request.onerror = () => {
67 | reject('保存问题失败');
68 | };
69 | });
70 | } catch (error) {
71 | console.error('IndexedDB操作失败:', error);
72 | throw error;
73 | }
74 | };
75 |
76 | // 获取所有问题
77 | export const getAllProblems = async (): Promise => {
78 | try {
79 | const db = await initDB();
80 | return new Promise((resolve, reject) => {
81 | const transaction = db.transaction([STORE_NAME], 'readonly');
82 | const store = transaction.objectStore(STORE_NAME);
83 | const request = store.getAll();
84 |
85 | request.onsuccess = () => {
86 | resolve(request.result);
87 | };
88 |
89 | request.onerror = () => {
90 | reject('获取问题列表失败');
91 | };
92 | });
93 | } catch (error) {
94 | console.error('IndexedDB操作失败:', error);
95 | throw error;
96 | }
97 | };
98 |
99 | // 根据ID获取问题
100 | export const getProblemById = async (id: number): Promise => {
101 | try {
102 | const db = await initDB();
103 | return new Promise((resolve, reject) => {
104 | const transaction = db.transaction([STORE_NAME], 'readonly');
105 | const store = transaction.objectStore(STORE_NAME);
106 | const request = store.get(id);
107 |
108 | request.onsuccess = () => {
109 | resolve(request.result || null);
110 | };
111 |
112 | request.onerror = () => {
113 | reject('获取问题失败');
114 | };
115 | });
116 | } catch (error) {
117 | console.error('IndexedDB操作失败:', error);
118 | throw error;
119 | }
120 | };
121 |
122 | // 更新问题数据
123 | export const updateProblem = async (problem: LLMProblem): Promise => {
124 | try {
125 | const db = await initDB();
126 | return new Promise((resolve, reject) => {
127 | const transaction = db.transaction([STORE_NAME], 'readwrite');
128 | const store = transaction.objectStore(STORE_NAME);
129 | const request = store.put(problem);
130 |
131 | request.onsuccess = () => {
132 | resolve();
133 | };
134 |
135 | request.onerror = () => {
136 | reject('更新问题失败');
137 | };
138 | });
139 | } catch (error) {
140 | console.error('IndexedDB操作失败:', error);
141 | throw error;
142 | }
143 | };
144 |
145 | // 删除问题
146 | export const deleteProblem = async (id: number): Promise => {
147 | try {
148 | const db = await initDB();
149 | return new Promise((resolve, reject) => {
150 | const transaction = db.transaction([STORE_NAME], 'readwrite');
151 | const store = transaction.objectStore(STORE_NAME);
152 | const request = store.delete(id);
153 |
154 | request.onsuccess = () => {
155 | resolve();
156 | };
157 |
158 | request.onerror = () => {
159 | reject('删除问题失败');
160 | };
161 | });
162 | } catch (error) {
163 | console.error('IndexedDB操作失败:', error);
164 | throw error;
165 | }
166 | };
167 |
168 | // 切换收藏状态
169 | export const toggleFavorite = async (id: number): Promise => {
170 | try {
171 | const problem = await getProblemById(id);
172 | if (!problem) {
173 | throw new Error('问题不存在');
174 | }
175 |
176 | problem.isFavorite = !problem.isFavorite;
177 | await updateProblem(problem);
178 | } catch (error) {
179 | console.error('切换收藏状态失败:', error);
180 | throw error;
181 | }
182 | };
183 |
184 | // 重命名问题
185 | export const renameProblem = async (id: number, newTitle: string): Promise => {
186 | try {
187 | const problem = await getProblemById(id);
188 | if (!problem) {
189 | throw new Error('问题不存在');
190 | }
191 |
192 | problem.title = newTitle;
193 | await updateProblem(problem);
194 | } catch (error) {
195 | console.error('重命名问题失败:', error);
196 | throw error;
197 | }
198 | };
199 |
--------------------------------------------------------------------------------
/chat-sql/src/types/database.ts:
--------------------------------------------------------------------------------
1 | export interface ForeignKeyRef {
2 | tableId: string;
3 | tableName?: string;
4 | columnName: string;
5 | }
6 |
7 | export interface Column {
8 | name: string;
9 | type: string;
10 | isPrimary: boolean;
11 | foreignKeyRefs?: ForeignKeyRef[];
12 | }
13 |
14 | export interface Table {
15 | id: string;
16 | tableName: string;
17 | position: { x: number; y: number };
18 | columns: Column[];
19 | isReferenced: boolean;
20 | }
21 |
22 | export interface Edge {
23 | id: string;
24 | source: string;
25 | target: string;
26 | sourceHandle: string;
27 | targetHandle: string;
28 | type: string;
29 | label?: string;
30 | markerEnd?: any;
31 | style?: any;
32 | }
33 |
34 | // 输入 JSON 的类型定义
35 | export interface InputColumn {
36 | name: string;
37 | type: string;
38 | isPrimary: boolean;
39 | }
40 |
41 | export interface InputForeignKey {
42 | fromTable: string;
43 | fromColumn: string;
44 | toTable: string;
45 | toColumn: string;
46 | }
47 |
48 | export interface InputTable {
49 | tableName: string;
50 | columns: InputColumn[];
51 | foreignKeys?: InputForeignKey[];
52 | }
53 |
--------------------------------------------------------------------------------
/chat-sql/src/types/dify.ts:
--------------------------------------------------------------------------------
1 | // Dify API 返回的数据结构
2 | export interface DifyResponse {
3 | data: {
4 | outputs: {
5 | description: string;
6 | problem: string[];
7 | expected_result: any[];
8 | hint: string;
9 | tableStructure: {
10 | tableName: string;
11 | columns: any[];
12 | foreignKeys: any[];
13 | }[];
14 | tuples: {
15 | tableName: string;
16 | tupleData: any[];
17 | }[];
18 | tags: string[];
19 | isBuiltIn?: boolean; // 新增字段,用于标识内置教程
20 | order?: number; // 新增字段,用于教程排序
21 | category?: string; // 新增字段,用于教程分类
22 | }
23 | }
24 | }
25 |
26 | // dify返回的问题输出(显示在前端等待确认的部分)
27 | export interface ProblemOutput {
28 | description: string;
29 | problem: string[];
30 | tags: string[];
31 | tableStructure?: TableStructure[];
32 | tuples?: TableTuple[];
33 | expected_result?: TableTuple[];
34 | hint?: string;
35 | }
36 |
37 | // schema渲染组件的输入要求(通过parse函数将dify的外键关系进行转换)
38 | export interface TableStructure {
39 | tableName: string;
40 | columns: {
41 | name: string;
42 | type: string;
43 | isPrimary: boolean;
44 | }[];
45 | foreignKeys?: {
46 | fromTable: string;
47 | fromColumn: string;
48 | toTable: string;
49 | toColumn: string;
50 | }[];
51 | }
52 |
53 | export interface TableTuple {
54 | tableName: string;
55 | tupleData: Record[];
56 | }
57 |
--------------------------------------------------------------------------------
/chat-sql/src/types/git.ts:
--------------------------------------------------------------------------------
1 | export interface GitCommit {
2 | hash: string;
3 | date: string;
4 | message: string;
5 | description?: string; // 添加可选的详细描述字段
6 | author: string;
7 | }
8 |
9 | export interface BranchDetails {
10 | totalBranches: number;
11 | currentBranch: string;
12 | branches: string[];
13 | }
14 |
15 | export interface GitStats {
16 | totalCommits: number;
17 | contributors: string[];
18 | lastUpdate: string;
19 | activeBranches: number;
20 | branchDetails: BranchDetails;
21 | }
22 |
23 | export interface GitInfo {
24 | history: GitCommit[];
25 | stats: GitStats;
26 | }
27 |
--------------------------------------------------------------------------------
/chat-sql/src/types/sqlExecutor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * SQL执行器类型定义
3 | *
4 | * 该文件定义了SQL执行器使用的类型。
5 | */
6 |
7 | import { TableStructure as DifyTableStructure } from './dify';
8 |
9 | /**
10 | * 扩展列定义,添加外键引用
11 | */
12 | export interface ColumnDefinition {
13 | name: string;
14 | type: string;
15 | isPrimary: boolean;
16 | foreignKeyRefs?: ForeignKeyRef[];
17 | }
18 |
19 | /**
20 | * 扩展表结构定义
21 | */
22 | export interface TableStructure extends Omit {
23 | columns: ColumnDefinition[];
24 | }
25 |
26 | /**
27 | * 表数据接口
28 | * 包含表结构和表数据
29 | */
30 | export interface TableData {
31 | structure: TableStructure;
32 | data: any[];
33 | }
34 |
35 | /**
36 | * SQL查询结果接口
37 | */
38 | export interface SQLQueryResult {
39 | success: boolean;
40 | data?: any[];
41 | message?: string;
42 | columns?: string[]; // 添加列名数组属性
43 | }
44 |
45 | /**
46 | * 外键引用接口
47 | */
48 | export interface ForeignKeyRef {
49 | tableName: string;
50 | columnName: string;
51 | }
52 |
--------------------------------------------------------------------------------
/chat-sql/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------