UNPKG

digitalocean-mcp-chatbot-sdk

Version:
278 lines (261 loc) 8.43 kB
import React, { useState, useEffect } from "react"; import PropTypes from "prop-types"; import axios from "axios"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import "./styles.css"; const LS_API_URL_KEY = "mcpchat_api_url"; const LS_DO_TOKEN_KEY = "mcpchat_do_token"; const MCPChatbot = ({ defaultApiUrl, position = "bottom-right", title = "Chat with DigitalOcean Assistant", placeholder = "Type your message...", toggleButtonLabel = "Chat with DigitalOcean Assistant", darkMode = false, }) => { const [input, setInput] = useState(""); const [chatHistory, setChatHistory] = useState([]); const [open, setOpen] = useState(false); const [showConfig, setShowConfig] = useState(false); const [apiUrl, setApiUrl] = useState(""); const [token, setToken] = useState(""); const [error, setError] = useState(""); const [isConfigured, setIsConfigured] = useState(false); const [loading, setLoading] = useState(false); useEffect(() => { const savedApiUrl = localStorage.getItem(LS_API_URL_KEY); const savedToken = localStorage.getItem(LS_DO_TOKEN_KEY); if (savedApiUrl && savedToken) { setApiUrl(savedApiUrl); setToken(savedToken); setIsConfigured(true); } else if (defaultApiUrl) { setApiUrl(defaultApiUrl); } }, [defaultApiUrl]); const configureClient = async () => { setError(""); if (!apiUrl || !token) { setError("API URL and token are required."); return; } const configureUrl = `${apiUrl.replace(/\/chat$/, "")}/configure`; try { const res = await axios.post(configureUrl, { digitalocean_token: token, }); if ( res.data.status === "configured" || res.data.status === "already_configured" ) { setIsConfigured(true); setShowConfig(false); localStorage.setItem(LS_API_URL_KEY, apiUrl); localStorage.setItem(LS_DO_TOKEN_KEY, token); } else { setError("❌ Unexpected response during configuration."); } } catch (err) { setError("❌ Configuration failed. Check your server and token."); } }; const handleConfigChange = (key, value) => { if (key === "apiUrl") { setApiUrl(value); setIsConfigured(false); localStorage.removeItem(LS_API_URL_KEY); } if (key === "token") { setToken(value); setIsConfigured(false); localStorage.removeItem(LS_DO_TOKEN_KEY); } }; const sendMessage = async () => { setError(""); if (!input.trim()) return; if (!apiUrl || !token || !isConfigured) { setError("Please configure the client before using chat."); return; } const newMessages = [...chatHistory, { role: "user", content: input }]; setChatHistory(newMessages); setInput(""); setLoading(true); try { const recentHistory = newMessages.slice(-5); const res = await axios.post(apiUrl, { message: input, chat_history: recentHistory, digitalocean_token: token, }); const botMessage = { role: "ai", content: res.data.response || "No response", }; setChatHistory([...newMessages, botMessage]); } catch (err) { setChatHistory([ ...newMessages, { role: "ai", content: "⚠️ Error fetching response." }, ]); } finally { setLoading(false); } }; const clearLocalConfig = () => { localStorage.removeItem(LS_API_URL_KEY); localStorage.removeItem(LS_DO_TOKEN_KEY); setApiUrl(""); setToken(""); setIsConfigured(false); setShowConfig(true); }; const positionClass = { "bottom-right": "position-bottom-right", "bottom-left": "position-bottom-left", "top-right": "position-top-right", "top-left": "position-top-left", }[position]; return ( <div className={`mcpchat-container ${positionClass} ${ darkMode ? "dark-mode" : "" }`} > {!open ? ( <button className="chat-toggle" onClick={() => setOpen(true)}> {toggleButtonLabel} </button> ) : ( <div className="chatbox"> <div className="chatbox-header"> <img src="https://companieslogo.com/img/orig/DOCN-6eec72eb.png?t=1720244491" alt="DO" className="avatar" /> <span>{title}</span> <div> <button onClick={() => setShowConfig(!showConfig)} title="Settings" className="icon-button" > ⚙️ </button> <button onClick={() => setOpen(false)} title="Close" className="icon-button" > × </button> </div> </div> {showConfig && ( <div className="chatbox-settings"> <label> API URL: <input type="text" value={apiUrl} onChange={(e) => handleConfigChange("apiUrl", e.target.value)} title="Your chatbot API endpoint" /> </label> <label> DigitalOcean Token: <input type="password" value={token} onChange={(e) => handleConfigChange("token", e.target.value)} title="Generate from https://cloud.digitalocean.com/account/api/tokens" /> </label> <button onClick={configureClient}>Configure</button> {(apiUrl || token) && ( <button onClick={clearLocalConfig} className="clear-btn"> Clear Configuration </button> )} </div> )} <div className="chatbox-body"> {chatHistory.map((msg, idx) => ( <div key={idx} className={`chat-msg ${msg.role}`}> {msg.role === "ai" && ( <img src="https://companieslogo.com/img/orig/DOCN-6eec72eb.png?t=1720244491" alt="DO" className="avatar" /> )} <div className="bubble"> <strong> {msg.role === "user" ? "You" : "DigitalOcean"} </strong> <ReactMarkdown children={msg.content} remarkPlugins={[remarkGfm]} /> </div> </div> ))} {loading && ( <div className="chat-msg ai loading"> <img src="https://companieslogo.com/img/orig/DOCN-6eec72eb.png?t=1720244491" alt="DO" className="avatar" /> <div className="bot-typing-bubble"> <span className="dot"></span> <span className="dot"></span> <span className="dot"></span> </div> </div> )} {error && <div className="chat-msg error">{error}</div>} {!isConfigured && ( <div className="chat-msg setup-warning"> 🔧 Please configure your token to start chatting. </div> )} </div> <div className="chatbox-input"> <input value={input} onChange={(e) => setInput(e.target.value)} placeholder={placeholder} disabled={!apiUrl || !isConfigured || loading} onKeyDown={(e) => e.key === "Enter" && sendMessage()} style={{ resize: "vertical" }} /> <button onClick={sendMessage} disabled={!apiUrl || !isConfigured || loading} > Send </button> </div> </div> )} </div> ); }; MCPChatbot.propTypes = { defaultApiUrl: PropTypes.string, position: PropTypes.oneOf([ "bottom-left", "bottom-right", "top-left", "top-right", ]), title: PropTypes.string, placeholder: PropTypes.string, toggleButtonLabel: PropTypes.string, darkMode: PropTypes.bool, }; export default MCPChatbot;