digitalocean-mcp-chatbot-sdk
Version:
React SDK for Digitalocean MCP Chatbot integration
278 lines (261 loc) • 8.43 kB
JSX
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;