Skip to main content
Glama

MCPDemo - Visual SQL Chat Platform

by Ayi456
index.tsx10.5 kB
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; import MessageView from './MessageView'; import MessageInput, { MessageInputRef } from './MessageInput'; import SqlEditor from '../SqlEditor'; import { useSqlChatStore } from '../../stores/useSqlChat'; import { sqlApiService } from '../../services/sqlApiService'; import { EDITOR_CONFIG, ERROR_MESSAGES, CHAT_CONFIG } from '../../config/constants'; import { LoadingSpinner, EmptyState } from '../common'; import type { ChatMessage, Connection } from '../../types'; interface Props { currentConnection?: Connection | null; selectedDatabase?: string | null; } export interface ConversationViewRef { insertText: (text: string) => void; } const ConversationView = forwardRef<ConversationViewRef, Props>(({ currentConnection, selectedDatabase }, ref) => { const messagesEndRef = useRef<HTMLDivElement>(null); const messageInputRef = useRef<MessageInputRef>(null); const [showSqlEditor, setShowSqlEditor] = useState(false); const [sqlEditorValue, setSqlEditorValue] = useState(''); const [isExecuting, setIsExecuting] = useState(false); // 暴露插入文本的方法给父组件 useImperativeHandle(ref, () => ({ insertText: (text: string) => { messageInputRef.current?.insertText(text); } })); const { getCurrentSession, sendMessage, addMessage, isLoading, error } = useSqlChatStore(); const currentSession = getCurrentSession(); const messages = currentSession?.messages || []; const displayMessages = messages.filter(m => m.role !== 'system'); // 自动滚动到底部 const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [displayMessages]); const handleSendMessage = async (message: string) => { if (!message.trim()) return; // 传递当前选中的数据库信息给 AI // 需要修改 sendMessage 的签名来接收 database 参数 const database = selectedDatabase || currentConnection?.database; console.log('发送消息给 AI,使用数据库:', database); await sendMessage(message, currentConnection || undefined, database); }; const handleExecuteSql = async () => { if (!sqlEditorValue.trim()) return; if (!currentConnection) { console.error(ERROR_MESSAGES.NO_CONNECTION); alert(ERROR_MESSAGES.NO_CONNECTION); return; } console.log('Execute SQL: 开始执行', sqlEditorValue); setIsExecuting(true); try { // 使用当前选中的数据库,如果没有则使用连接配置中的数据库 const databaseToUse = selectedDatabase || currentConnection.database || ''; if (!databaseToUse) { throw new Error('No database selected. Please select a database first.'); } console.log('使用数据库:', databaseToUse); const execResult = await sqlApiService.executeQuery( currentConnection, databaseToUse, sqlEditorValue ); const rows = execResult.rows || execResult.data || []; const columns = execResult.fields?.map((f: any) => f.name) || (rows[0] ? Object.keys(rows[0]) : []); const queryResult = { columns, rows, rowCount: Array.isArray(rows) ? rows.length : 0, executionTime: execResult.executionTime || 0, affectedRows: execResult.affectedRows }; // 添加系统类结果消息(仅用于 ExecutionView 展示,不进入对话区) const resultMessage = { id: Date.now().toString() + '-result', role: 'system' as const, content: `SQL 执行完成,返回 ${queryResult.rowCount} 行数据`, sql: sqlEditorValue, result: queryResult, timestamp: new Date() }; console.log('添加结果消息到 store:', resultMessage); addMessage(resultMessage); console.log(`查询成功,返回 ${queryResult.rowCount} 行数据, 结果:`, queryResult); // 触发一个事件或者回调来显示结果面板 // 由于结果已经存储在store中,面板会自动更新 // 清空编辑器(可选) // setSqlEditorValue(''); } catch (error: any) { console.error('SQL execution error:', error); // 添加错误消息 addMessage({ id: Date.now().toString() + '-error', role: 'assistant', content: `查询失败: ${error.message || '未知错误'}`, timestamp: new Date() }); console.error(error.message || 'SQL 执行失败'); } finally { setIsExecuting(false); } }; const handleSqlEditorChange = (value: string | undefined) => { setSqlEditorValue(value || ''); }; return ( <div className="conversation-view flex flex-col h-full overflow-hidden"> {/* SQL Editor Toggle Button */} <div className="flex-shrink-0 p-3 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center"> <div className="flex items-center gap-2"> <svg className="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> </svg> <span className="font-medium text-gray-700 dark:text-gray-300">SQL Console</span> </div> <button onClick={() => setShowSqlEditor(!showSqlEditor)} className="px-3 py-1.5 text-sm bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors flex items-center gap-2" > <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={showSqlEditor ? "M19 9l-7 7-7-7" : "M9 5l7 7-7 7"} /> </svg> {showSqlEditor ? 'Hide' : 'Show'} SQL Editor </button> </div> {/* SQL Editor Panel */} {showSqlEditor && ( <div className="flex-shrink-0 p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> <div className="mb-2 flex items-center justify-between"> <h3 className="text-sm font-medium text-gray-700 dark:text-gray-300"> Write and execute SQL queries directly </h3> <button onClick={handleExecuteSql} disabled={!sqlEditorValue.trim() || isExecuting || !currentConnection} className="px-4 py-1.5 text-sm bg-green-500 hover:bg-green-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg transition-colors flex items-center gap-2"> {isExecuting ? ( <> <svg className="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> Executing... </> ) : ( <> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> Execute Query </> )} </button> </div> <div className="overflow-hidden"> <SqlEditor value={sqlEditorValue} onChange={handleSqlEditorChange} onExecute={handleExecuteSql} height={EDITOR_CONFIG.DEFAULT_HEIGHT} /> </div> </div> )} {/* 消息列表 */} <div className="message-list flex-1 overflow-y-auto"> {displayMessages.length === 0 ? ( <EmptyState icon={ <svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /> </svg> } title="开始对话" description="输入您的 SQL 查询需求,AI 将为您生成相应的 SQL 语句" /> ) : ( <> {displayMessages.map((message) => ( <MessageView key={message.id} message={message} connection={currentConnection || undefined} database={selectedDatabase || currentConnection?.database} /> ))} {isLoading && ( <div className="flex justify-start p-4"> <div className="bg-gray-100 dark:bg-gray-700 rounded-lg p-3"> <LoadingSpinner size="sm" /> </div> </div> )} <div ref={messagesEndRef} /> </> )} </div> {/* 错误提示 */} {error && ( <div className="px-4 py-2 bg-red-50 dark:bg-red-900/20 border-t border-red-200 dark:border-red-800"> <p className="text-sm text-red-600 dark:text-red-400">{error}</p> </div> )} {/* 输入区域 */} <div className="flex-shrink-0"> <MessageInput ref={messageInputRef} onSend={handleSendMessage} disabled={isLoading || !currentSession} /> </div> </div> ); }); ConversationView.displayName = 'ConversationView'; export default ConversationView;

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