Skip to main content
Glama

MCPDemo - Visual SQL Chat Platform

by Ayi456
SqlConsole.tsx15.1 kB
import React, { useEffect, useState, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import useAuth from '@/store/useAuth'; import ConversationView, { ConversationViewRef } from './components/ConversationView'; import ExecutionView from './components/ExecutionView'; import { ConnectionList } from './components/ConnectionList'; import { useSqlChatStore } from './stores/useSqlChat'; import ResizablePanel from './components/ResizablePanel'; import { useConnectionStatus } from './hooks'; import { PANEL_CONFIG, STYLE_CLASSES } from './config/constants'; import { ConnectionStatusIndicator } from './components/common'; import type { Connection, Schema } from './types'; import './styles/sql-console.css'; const SqlConsole: React.FC = () => { const { user } = useAuth(); const [isLoading, setIsLoading] = useState(true); const navigate = useNavigate(); const [showExecutionView, setShowExecutionView] = useState(true); // 默认显示结果面板 const [selectedConnection, setSelectedConnection] = useState<Connection | null>(null); const [selectedDatabase, setSelectedDatabase] = useState<string | null>(null); const [selectedTable, setSelectedTable] = useState<Schema | null>(null); const [showSettings, setShowSettings] = useState(false); // 设置面板 // ConversationView 的 ref,用于插入表名 const conversationViewRef = useRef<ConversationViewRef>(null); const { initSession, getCurrentSession, updateContext } = useSqlChatStore(); const currentSession = getCurrentSession(); const autoExecuteMode = currentSession?.context.preferences?.autoExecuteMode || 'manual'; // 使用自定义Hook管理连接状态 const { status: connStatus, message: connMessage } = useConnectionStatus(selectedConnection); // 处理数据库选择 const handleDatabaseSelect = (database: string) => { setSelectedDatabase(database); console.log('已选择数据库:', database); }; // 处理表选择(现在由 ConnectionList 直接处理) const handleTableSelect = (table: Schema) => { setSelectedTable(table); console.log('已选择表:', table.name, '列数:', table.columns?.length); }; useEffect(() => { // 初始化 const init = async () => { if (!user?.id) { // 未登录也允许进入页面(仅禁用需要用户的功能),避免无限 Loading setIsLoading(false); return; } try { setIsLoading(true); // TODO: 加载用户连接 // await loadConnections(user.id); // 初始化聊天会话 await initSession(user.id); } catch (error) { console.error('Failed to initialize SQL Console:', error); } finally { setIsLoading(false); } }; init(); }, [user?.id]); if (isLoading) { return ( <div className="flex items-center justify-center h-screen"> <div className="text-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div> <p className="mt-4 text-gray-600">正在初始化 SQL Console...</p> </div> </div> ); } return ( <div className="sql-console flex h-screen bg-gray-50 dark:bg-gray-900"> {/* 左侧连接管理栏 */} <div className="w-80 border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 overflow-y-auto"> <h2 className="text-lg font-semibold mb-4">数据库连接</h2> {/* 连接管理组件 */} <div className="flex-1 overflow-hidden"> <ConnectionList onConnectionSelect={(conn) => { setSelectedConnection(conn); console.log('已选择连接:', conn); }} selectedConnectionId={selectedConnection?.id} onDatabaseSelect={handleDatabaseSelect} onTableSelect={handleTableSelect} /> </div> </div> {/* 中间主要内容区 */} <div className="flex-1 flex flex-col min-w-0 overflow-hidden"> {/* 顶部工具栏 */} <div className="border-b border-gray-200 dark:border-gray-700 px-4 py-3 bg-white dark:bg-gray-800"> <div className="flex items-center justify-between gap-4"> <div className="flex items-center space-x-4 min-w-0 flex-1"> <button onClick={() => navigate(-1)} title="返回上一页" aria-label="返回上一页" className="px-2 py-1 inline-flex items-center gap-1 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex-shrink-0" > <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> </svg> <span className="text-sm">返回</span> </button> <h1 className="text-lg font-semibold text-gray-800 dark:text-gray-200 flex-shrink-0"> SQL Chat </h1> {/* 连接状态 - 与标题并排 */} {selectedConnection ? ( <div className="min-w-0 flex-1"> <ConnectionStatusIndicator status={connStatus} message={connStatus === 'connected' ? `✓ ${selectedConnection.title} (${selectedConnection.host}:${selectedConnection.port}${selectedDatabase ? ` • ${selectedDatabase}` : ''})` : connMessage } /> </div> ) : ( <span className="text-sm text-gray-500 flex-shrink-0"> AI 驱动的 SQL 查询助手 </span> )} </div> <div className="flex items-center space-x-2 flex-shrink-0"> <button onClick={() => setShowSettings(true)} className="px-3 py-1.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors flex items-center gap-1.5" title="设置" > <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg> 设置 </button> <button onClick={() => setShowExecutionView(!showExecutionView)} className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors" > {showExecutionView ? '隐藏结果' : '显示结果'} </button> </div> </div> </div> <div className="flex-1 min-h-0"> {showExecutionView ? ( <ResizablePanel defaultTopHeight={PANEL_CONFIG.DEFAULT_TOP_HEIGHT} minTopHeight={PANEL_CONFIG.MIN_TOP_HEIGHT} maxTopHeight={PANEL_CONFIG.MAX_TOP_HEIGHT} storageKey={PANEL_CONFIG.STORAGE_KEY} topContent={ <ConversationView ref={conversationViewRef} currentConnection={selectedConnection} selectedDatabase={selectedDatabase} /> } bottomContent={ <ExecutionView currentConnection={selectedConnection} /> } /> ) : ( <ConversationView ref={conversationViewRef} currentConnection={selectedConnection} selectedDatabase={selectedDatabase} /> )} </div> </div> {/* 设置弹窗 */} {showSettings && ( <div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="absolute inset-0 bg-black/40" onClick={() => setShowSettings(false)} /> <div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-md p-6"> <div className="flex items-center justify-between mb-4"> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">SQL Chat 设置</h3> <button onClick={() => setShowSettings(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" > <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> <div className="space-y-4"> {/* SQL 自动执行模式 */} <div> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> SQL 自动执行模式 </label> <p className="text-xs text-gray-500 dark:text-gray-400 mb-3"> 控制 AI 生成 SQL 语句后的执行行为 </p> <div className="space-y-2"> <label className="flex items-start p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"> <input type="radio" name="autoExecuteMode" value="manual" checked={autoExecuteMode === 'manual'} onChange={(e) => { updateContext({ preferences: { ...currentSession?.context.preferences, autoExecuteMode: e.target.value as 'manual' | 'auto' | 'ask' } }); }} className="mt-1 mr-3" /> <div> <div className="font-medium text-gray-900 dark:text-white">手动执行(推荐)</div> <div className="text-xs text-gray-500 dark:text-gray-400"> 生成 SQL 后需要手动点击执行按钮,更安全可控 </div> </div> </label> <label className="flex items-start p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"> <input type="radio" name="autoExecuteMode" value="auto" checked={autoExecuteMode === 'auto'} onChange={(e) => { updateContext({ preferences: { ...currentSession?.context.preferences, autoExecuteMode: e.target.value as 'manual' | 'auto' | 'ask' } }); }} className="mt-1 mr-3" /> <div> <div className="font-medium text-gray-900 dark:text-white">自动执行</div> <div className="text-xs text-gray-500 dark:text-gray-400"> 仅自动执行安全的 SELECT 查询,危险操作需手动确认 </div> </div> </label> <label className="flex items-start p-3 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors opacity-50"> <input type="radio" name="autoExecuteMode" value="ask" checked={autoExecuteMode === 'ask'} onChange={(e) => { updateContext({ preferences: { ...currentSession?.context.preferences, autoExecuteMode: e.target.value as 'manual' | 'auto' | 'ask' } }); }} className="mt-1 mr-3" disabled /> <div> <div className="font-medium text-gray-900 dark:text-white"> 询问模式 <span className="ml-2 text-xs text-gray-400">(即将推出)</span> </div> <div className="text-xs text-gray-500 dark:text-gray-400"> 每次生成 SQL 后弹窗询问是否执行 </div> </div> </label> </div> </div> {/* 当前模式提示 */} <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800"> <div className="flex items-start gap-2"> <svg className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <div className="text-sm text-blue-800 dark:text-blue-300"> <strong>当前模式:</strong> {autoExecuteMode === 'manual' && '手动执行'} {autoExecuteMode === 'auto' && '自动执行'} {autoExecuteMode === 'ask' && '询问模式'} </div> </div> </div> </div> <div className="flex justify-end mt-6"> <button onClick={() => setShowSettings(false)} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" > 确定 </button> </div> </div> </div> )} </div> ); }; export default SqlConsole;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ayi456/visual-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server