├── .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 | Next.js 7 | React 8 | TypeScript 9 | License 10 | Ask DeepWiki 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 | ![](https://my-blog-img-1358266118.cos.ap-guangzhou.myqcloud.com/undefined20250508164908220.png?imageSlim) 42 | 43 | - 点击侧边栏中的“初始化教程”, 可以同预设的数据库表结构进行交互; 44 | - 点击侧边栏中的“帮助”, 可以查看基本的操作演示. 45 | 46 | ### 数据库结构可视化 47 | 48 | ![](https://my-blog-img-1358266118.cos.ap-guangzhou.myqcloud.com/undefined20250508165221364.png?imageSlim) 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 | Next.js 67 | React 68 | TypeScript 69 | Ant Design 70 | Material-UI 71 | Monaco Editor 72 | XY Flow 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 | Next.js 7 | React 8 | TypeScript 9 | License 10 | Ask DeepWiki 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 | ![](https://my-blog-img-1358266118.cos.ap-guangzhou.myqcloud.com/undefined20250508164908220.png?imageSlim) 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 | ![](https://my-blog-img-1358266118.cos.ap-guangzhou.myqcloud.com/undefined20250508165221364.png?imageSlim) 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 | Editor Demo 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 | Next.js 56 | React 57 | TypeScript 58 | Ant Design 59 | Material-UI 60 | Monaco Editor 61 | XY Flow 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 | - Import Workflow 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 | 149 | 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 |
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 |
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 | 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 |