Skip to main content
Glama
ChatPageAgent.tsx19.3 kB
import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Send, Loader2, Sparkles, Activity, Copy, Check, Bot, User, HelpCircle, } from "lucide-react"; import { useTheme } from "@/components/theme-provider"; import DOMPurify from "dompurify"; import { marked } from "marked"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; interface Model { id: string; name: string; provider: string; supports_tools: boolean; context_window: number; type: string; } interface Message { role: "user" | "assistant"; content: string; tool_calls?: Array<{ tool: string; args: any; result: any; }>; trace_id?: string; } interface ChatPageAgentProps { onViewTrace?: (traceId: string) => void; } export function ChatPageAgent({ onViewTrace }: ChatPageAgentProps) { const [models, setModels] = useState<Model[]>([]); const [selectedModel, setSelectedModel] = useState<string>(""); const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [copiedIndex, setCopiedIndex] = useState<number | null>(null); const [showHelp, setShowHelp] = useState(false); const textareaRef = useRef<HTMLTextAreaElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null); const { theme } = useTheme(); const isDark = theme === "dark"; useEffect(() => { fetchModels(); }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const fetchModels = async () => { try { const response = await fetch("/api/chat/models"); const data = await response.json(); setModels(data.models || []); setSelectedModel(data.default || ""); } catch (error) { console.error("Failed to fetch models:", error); } }; const sendMessage = async () => { if (!input.trim() || loading || !selectedModel) return; const userMessage: Message = { role: "user", content: input, }; const newMessages = [...messages, userMessage]; setMessages(newMessages); setInput(""); setLoading(true); try { // Call the agent chat API const response = await fetch("/api/agent/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ messages: newMessages.map((m) => ({ role: m.role, content: m.content, })), model: selectedModel, max_tokens: 2048, temperature: 0.7, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: "Unknown error" })); throw new Error(errorData.detail || `HTTP ${response.status}`); } const data = await response.json(); // Add assistant response const assistantMessage: Message = { role: "assistant", content: data.response || "I apologize, but I couldn't generate a response.", tool_calls: data.tool_calls, trace_id: data.trace_id, }; setMessages((prev) => [...prev, assistantMessage]); } catch (error) { console.error("Failed to send message:", error); setMessages((prev) => [ ...prev, { role: "assistant", content: `Sorry, I encountered an error: ${error instanceof Error ? error.message : "Unknown error"}`, }, ]); } finally { setLoading(false); } }; const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const copyMessage = (content: string, index: number) => { navigator.clipboard.writeText(content); setCopiedIndex(index); setTimeout(() => setCopiedIndex(null), 2000); }; const renderMarkdown = (content: string) => { const html = marked.parse(content) as string; const sanitized = DOMPurify.sanitize(html); return { __html: sanitized }; }; return ( <div className={`flex flex-col h-full ${ isDark ? "bg-[#1C3D42]" : "bg-gray-50" }`} > {/* Top Bar with Model Selection */} <div className={`flex items-center justify-between px-6 py-3 ${ isDark ? "bg-[#16343A]" : "bg-white" } border-b ${isDark ? "border-white/10" : "border-gray-200"}`} > <div className="flex items-center gap-2"> <Sparkles className={`h-5 w-5 ${isDark ? "text-blue-400" : "text-blue-600"}`} /> <span className={`font-semibold ${isDark ? "text-white" : "text-gray-900"}`}> Dataverse MCP Chat </span> </div> <div className="flex items-center gap-4"> <div className="flex items-center gap-2"> <span className={`text-sm ${isDark ? "text-gray-400" : "text-gray-600"}`}> Model: </span> <Select value={selectedModel} onValueChange={setSelectedModel}> <SelectTrigger className={`w-[280px] ${ isDark ? "bg-white/5 border-white/10 text-white" : "bg-gray-50 border-gray-300 text-gray-900" }`} > <SelectValue placeholder="Select a model"> {models.find((m) => m.id === selectedModel)?.name || "No model selected"} </SelectValue> </SelectTrigger> <SelectContent> {models.length === 0 ? ( <div className="p-4 text-center text-sm text-muted-foreground"> No models configured </div> ) : ( models.map((model) => ( <SelectItem key={model.id} value={model.id} disabled={!model.supports_tools} > <div className="flex flex-col"> <div className="flex items-center gap-2"> <span className={`font-medium ${ !model.supports_tools ? "text-muted-foreground" : "" }`} > {model.name} </span> {!model.supports_tools && ( <span className="text-[10px] px-1.5 py-0.5 rounded bg-muted text-muted-foreground border border-border"> Not tool-enabled </span> )} </div> <span className="text-xs text-muted-foreground"> {model.provider} • {model.context_window.toLocaleString()} tokens </span> </div> </SelectItem> )) )} </SelectContent> </Select> </div> </div> </div> {/* Chat Messages */} <div className="flex-1 overflow-y-auto p-6"> {messages.length === 0 ? ( /* Empty State */ <div className="flex flex-col items-center justify-center h-full"> <div className="max-w-2xl w-full space-y-6 text-center"> <div className="space-y-4"> <h1 className={`text-5xl font-bold ${ isDark ? "text-white" : "text-gray-900" }`} > What can I help you with today? </h1> <p className={`text-lg ${ isDark ? "text-white/80" : "text-gray-600" }`} > I can help you explore and interact with your Microsoft Dataverse data </p> </div> </div> </div> ) : ( /* Conversation View */ <div className="max-w-4xl mx-auto space-y-6"> {messages.map((message, index) => ( <div key={index} className={`flex gap-4 ${ message.role === "user" ? "justify-end" : "justify-start" }`} > {message.role === "assistant" && ( <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ isDark ? "bg-blue-500/20" : "bg-blue-100" }`} > <Bot className="h-4 w-4 text-blue-500" /> </div> )} <div className={`max-w-[80%] rounded-2xl px-5 py-3 ${ message.role === "user" ? "bg-blue-500 text-white" : isDark ? "bg-white/10 text-white border border-white/20" : "bg-white text-gray-900 border border-gray-200" }`} > <div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={renderMarkdown(message.content)} /> {/* Tool Calls */} {message.tool_calls && message.tool_calls.length > 0 && ( <div className="mt-3 flex flex-wrap gap-2"> {message.tool_calls.map((toolCall, tcIndex) => ( <span key={tcIndex} className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium ${ message.role === "user" ? "bg-white/20" : isDark ? "bg-blue-500/20 text-blue-300" : "bg-blue-100 text-blue-700" }`} > <Sparkles className="h-3 w-3" /> {toolCall.tool} </span> ))} </div> )} {/* Trace Link */} {message.trace_id && onViewTrace && ( <button onClick={() => onViewTrace(message.trace_id!)} className={`mt-2 flex items-center gap-1 text-xs ${ message.role === "user" ? "text-white/80 hover:text-white" : isDark ? "text-blue-400 hover:text-blue-300" : "text-blue-600 hover:text-blue-700" }`} > <Activity className="h-3 w-3" /> View Trace </button> )} {/* Copy Button */} {message.role === "assistant" && ( <button onClick={() => copyMessage(message.content, index)} className={`mt-2 flex items-center gap-1 text-xs ${ isDark ? "text-gray-400 hover:text-white" : "text-gray-500 hover:text-gray-900" }`} > {copiedIndex === index ? ( <> <Check className="h-3 w-3" /> Copied! </> ) : ( <> <Copy className="h-3 w-3" /> Copy </> )} </button> )} </div> {message.role === "user" && ( <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ "bg-blue-500" }`} > <User className="h-4 w-4 text-white" /> </div> )} </div> ))} {loading && ( <div className="flex gap-4 justify-start"> <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ isDark ? "bg-blue-500/20" : "bg-blue-100" }`} > <Bot className="h-4 w-4 text-blue-500" /> </div> <div className={`max-w-[80%] rounded-2xl px-5 py-3 ${ isDark ? "bg-white/10 border border-white/20" : "bg-white border border-gray-200" }`} > <div className="flex items-center gap-2"> <Loader2 className="h-4 w-4 animate-spin" /> <span className="text-sm">Thinking...</span> </div> </div> </div> )} <div ref={messagesEndRef} /> </div> )} </div> {/* Bottom Input */} <div className={`p-4 ${ isDark ? "bg-[#16343A]" : "bg-white" } border-t ${isDark ? "border-white/10" : "border-gray-200"}`} > <div className="max-w-4xl mx-auto relative"> <Textarea ref={textareaRef} placeholder="Ask me about your Dataverse data..." value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} className={`min-h-[80px] pr-14 resize-none ${ isDark ? "bg-white/5 border-white/10 text-white placeholder:text-white/60" : "bg-gray-50 border-gray-300 text-gray-900 placeholder:text-gray-500" }`} disabled={loading} /> <Button onClick={sendMessage} disabled={loading || !input.trim()} size="icon" className="absolute bottom-3 right-3 rounded-full bg-blue-500 hover:bg-blue-600 text-white" > {loading ? ( <Loader2 className="h-5 w-5 animate-spin" /> ) : ( <Send className="h-5 w-5" /> )} </Button> </div> </div> {/* User Guide Button - Bottom Left Corner */} <Dialog open={showHelp} onOpenChange={setShowHelp}> <DialogTrigger asChild> <Button className={`fixed bottom-6 left-6 rounded-full shadow-lg ${ isDark ? "bg-white/10 hover:bg-white/20 text-white border border-white/20" : "bg-white hover:bg-gray-50 text-gray-900 border border-gray-200" }`} size="lg" > <HelpCircle className="h-5 w-5 mr-2" /> User Guide </Button> </DialogTrigger> <DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto"> <DialogHeader> <DialogTitle className="text-2xl">Dataverse MCP Server</DialogTitle> <DialogDescription> Model Context Protocol tools for Microsoft Dataverse </DialogDescription> </DialogHeader> <div className="space-y-6 py-4"> {/* Example Prompts */} <div> <h3 className="text-lg font-semibold mb-3 flex items-center gap-2"> 💬 Example Prompts </h3> <ul className="space-y-2 text-sm"> <li className="ml-4">• "List all tables in my Dataverse environment"</li> <li className="ml-4">• "Show me the schema for the Account table"</li> <li className="ml-4">• "Query the Contact table for all active contacts"</li> <li className="ml-4">• "Create a new account record with name 'Contoso'"</li> <li className="ml-4">• "Update contact record with ID xyz123"</li> </ul> </div> {/* Available Tools */} <div> <h3 className="text-lg font-semibold mb-3 flex items-center gap-2"> 🔧 Available Tools </h3> <div className="space-y-3"> <div> <h4 className="font-semibold text-sm mb-1">list_tables</h4> <p className="text-sm text-muted-foreground ml-4"> Browse and discover all available Dataverse tables in your environment </p> </div> <div> <h4 className="font-semibold text-sm mb-1">describe_table</h4> <p className="text-sm text-muted-foreground ml-4"> Get detailed schema information, column types, and metadata for a specific table </p> </div> <div> <h4 className="font-semibold text-sm mb-1">read_query</h4> <p className="text-sm text-muted-foreground ml-4"> Query Dataverse data using FetchXML to retrieve records matching your criteria </p> </div> <div> <h4 className="font-semibold text-sm mb-1">create_record</h4> <p className="text-sm text-muted-foreground ml-4"> Create new records in Dataverse tables with specified field values </p> </div> <div> <h4 className="font-semibold text-sm mb-1">update_record</h4> <p className="text-sm text-muted-foreground ml-4"> Update existing Dataverse records by ID with new field values </p> </div> </div> </div> {/* Configuration Info */} <div className={`p-4 rounded-lg ${ isDark ? "bg-blue-500/10 border border-blue-500/20" : "bg-blue-50 border border-blue-200" }`}> <h3 className="text-sm font-semibold mb-2">ℹ️ Configuration</h3> <p className="text-sm text-muted-foreground"> This MCP server connects to your Microsoft Dataverse environment using Service Principal authentication. All operations are performed through the Dataverse Web API. </p> </div> </div> </DialogContent> </Dialog> </div> ); }

Latest Blog Posts

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/lucamilletti99/dataverse_mcp_server'

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