Skip to main content
Glama
WebSocketChatGPT.tsx14.8 kB
import React, { useEffect, useRef, useState } from "react"; import { render as amisRender } from "amis"; import { formatFinalGetUrl } from "@/utils/utils"; import { Button, Flex, Space, Typography } from "antd"; import { BulbOutlined, InfoCircleOutlined, PlusOutlined, RocketOutlined, SmileOutlined, UserOutlined } from "@ant-design/icons"; import { Bubble, BubbleProps, Prompts, PromptsProps, Sender, Welcome } from "@ant-design/x"; import { Modal } from "antd"; interface WebSocketChatGPTProps { url: string; params: Record<string, string>; data: Record<string, any>; } const WebSocketChatGPT = React.forwardRef<HTMLDivElement, WebSocketChatGPTProps>( ({ url, data, params }, _) => { url = formatFinalGetUrl({ url, data, params }); const token = localStorage.getItem('token'); url = url + (url.includes('?') ? '&' : '?') + `token=${token}`; let historyUrl = '/ai/chat/ws_chatgpt/history' historyUrl = historyUrl + (historyUrl.includes('?') ? '&' : '?') + `token=${token}`; let historyResetUrl = '/ai/chat/ws_chatgpt/history/reset' historyResetUrl = historyResetUrl + (historyResetUrl.includes('?') ? '&' : '?') + `token=${token}`; const [messages, setMessages] = useState<{ role: "user" | "ai"; content: string }[]>([]); const [status, setStatus] = useState<string>("Disconnected"); const [inputMessage, setInputMessage] = useState<string>(""); // 用户输入的消息 const wsRef = useRef<WebSocket | null>(null); const messageContainerRef = useRef<HTMLDivElement | null>(null); // 滚动到底部 const [loading, setLoading] = useState<boolean>(false); console.log(status) useEffect(() => { if (wsRef.current) { wsRef.current.close(); } let finalUrl = url; if (!finalUrl.startsWith("ws")) { const protocol = window.location.protocol === "https:" ? "wss://" : "ws://"; finalUrl = protocol + location.host + finalUrl; } const ws = new WebSocket(finalUrl); wsRef.current = ws; ws.onopen = () => setStatus("Connected"); const formatToolCallResult = (message: any) => { try { const data = JSON.parse(message); if (data.tool_name && data.parameters && data.result) { return `🛠️ **工具调用**: ${data.tool_name}\n\n📝 **参数**:\n\`\`\`json\n${JSON.stringify(data.parameters, null, 2)}\n\`\`\`\n\n🎯 **结果**:\n${data.result}\n`; } return message; } catch { return message; } }; ws.onmessage = (event) => { try { const rawMessage = event.data || ""; if (rawMessage) { setMessages((prev) => { // 找到最后一个 AI 占位符并替换为实际消息 const aiPlaceholderIndex = prev.findIndex( (msg) => msg.role === "ai" && msg.content === "thinking" ); const formattedMessage = formatToolCallResult(rawMessage); if (aiPlaceholderIndex !== -1) { return prev.map((msg, index) => index === aiPlaceholderIndex ? { ...msg, content: formattedMessage } : msg ); } // 如果没有找到占位符,默认行为 if (prev.length === 0 || prev[prev.length - 1].role !== "ai") { return [...prev, { role: "ai", content: formattedMessage }]; } else { return prev.map((msg, index) => index === prev.length - 1 ? { ...msg, content: msg.content + formattedMessage } : msg ); } }); } } catch (error) { console.error("Failed to parse WebSocket message:", error); setMessages((prev) => [...prev, event.data]); } }; ws.onclose = () => setStatus("Disconnected"); ws.onerror = () => setStatus("Error"); return () => { wsRef.current?.close(); wsRef.current = null; }; }, [url]); // 发送消息 const handleSendMessage = () => { setLoading(true); if (!inputMessage.trim()) return; if (wsRef.current) { wsRef.current.send(inputMessage); } // 立即显示用户消息,并准备新的 AI 回复条目 setMessages((prev) => [ ...prev, { role: "user", content: `${inputMessage}` }, { role: "ai", content: "thinking" } // 插入AI思考中的占位符 ]); setInputMessage(""); // 清空输入框 setLoading(false); }; // 滚动到底部 const scrollToBottom = () => { if (messageContainerRef.current) { messageContainerRef.current.scrollTop = messageContainerRef.current.scrollHeight; } }; useEffect(() => { scrollToBottom(); }, [messages]); const renderMarkdown: BubbleProps['messageRender'] = (content: string) => { return amisRender({ type: "markdown", value: content }) }; const items: PromptsProps['items'] = [ { key: '1', icon: <BulbOutlined style={{ color: '#FFD700' }} />, label: 'yaml编写', description: '请给我一个基本的nginx 部署yaml', }, { key: '2', icon: <InfoCircleOutlined style={{ color: '#1890FF' }} />, label: '网络', description: '请解释下Deploy中的HostNetwork如何配置?', }, { key: '3', icon: <RocketOutlined style={{ color: '#722ED1' }} />, label: '自动应用', description: '请给我一个基本的nginx 部署yaml,并部署到集群中', }, { key: '4', icon: <SmileOutlined style={{ color: '#52C41A' }} />, label: 'Yaml模板', description: '请给我一个基本的nginx 部署yaml,并保存为模板', }, ]; const fooAvatar: React.CSSProperties = { color: '#f56a00', backgroundColor: '#fde3cf', }; const barAvatar: React.CSSProperties = { color: '#fff', backgroundColor: '#87d068', }; return ( <> <div style={{ width: "100%", height: "100%", minHeight: "600px" }}> { messages.length == 0 && <> <Welcome title="ChatGPT" description="我是k8m的AI小助手,你可以问我任何关于kubernetes的问题,我尽量给你提供最准确的答案。" style={{ backgroundImage: 'linear-gradient(97deg, #f2f9fe 0%, #f7f3ff 100%)', borderStartStartRadius: 4, }} /> <Prompts title="✨ 奇思妙想和创新的火花" items={items} wrap styles={{ item: { flex: 'none', width: 'calc(50% - 6px)', }, }} onItemClick={(info) => { setInputMessage(`${info.data.description}`); }} /> </> } <Flex gap="middle" vertical> {messages.map((msg) => ( <> <Bubble placement={msg.role === "user" ? "end" : "start"} content={msg.content} avatar={{ icon: msg.role === "user" ? <UserOutlined /> : <RocketOutlined />, style: msg.role === "user" ? barAvatar : fooAvatar, }} messageRender={renderMarkdown} loading={msg.role === 'ai' && msg.content === 'thinking'} /> </> ))} </Flex> <Flex vertical gap="middle" className="mt-20 mb-20"> { messages.length > 0 && <> <Space size="small"> <Button onClick={() => { setMessages([]); }} icon={<PlusOutlined />} style={{ width: '100px', backgroundImage: 'linear-gradient(97deg, #f2f9fe 0%, #f7f3ff 100%)', borderStartStartRadius: 4, borderStartEndRadius: 4, }} > 新会话 </Button> <Button onClick={() => { fetch(historyUrl) .then(response => response.json()) .then(data => { const itemCount = data.data ? data.data.length : 0; Modal.success({ content: `对话历史包含 ${itemCount} 条记录。`, }); }); }} icon={<InfoCircleOutlined />} style={{ width: '100px', backgroundImage: 'linear-gradient(97deg, #f2f9fe 0%, #f7f3ff 100%)', borderStartStartRadius: 4, borderStartEndRadius: 4, }} > 对话历史 </Button> <Button onClick={() => { fetch(historyResetUrl) .then(response => response.json()) .then(_ => { Modal.success({ content: '对话历史已清空。', }); }); }} icon={<InfoCircleOutlined />} style={{ width: '100px', backgroundImage: 'linear-gradient(97deg, #f2f9fe 0%, #f7f3ff 100%)', borderStartStartRadius: 4, borderStartEndRadius: 4, }} > 清空历史 </Button> </Space> </> } <Sender loading={loading} value={inputMessage} onChange={(v) => { setInputMessage(v); }} onSubmit={() => { setInputMessage(''); handleSendMessage(); }} onCancel={() => { setLoading(false); }} actions={(_, info) => { const { SendButton, ClearButton } = info.components; return ( <Space size="small"> <Typography.Text type="secondary"> <small>`Shift + Enter` 换行</small> </Typography.Text> <ClearButton /> <SendButton type="primary" disabled={false} /> </Space> ); }} /> </Flex> </div> </> ); } ); export default WebSocketChatGPT;

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/weibaohui/k8m'

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