-
-
Save shricodev/d8b980224127ef211c2e1f4ad1dd1bac to your computer and use it in GitHub Desktop.
Real-time Chat Application with MCP Support (Developed by Kimi K2) - Blog Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { Wifi, WifiOff } from "lucide-react"; | |
| interface ChatHeaderProps { | |
| isConnected: boolean; | |
| username: string; | |
| } | |
| export function ChatHeader({ isConnected, username }: ChatHeaderProps) { | |
| return ( | |
| <div className="flex items-center justify-between px-4 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white"> | |
| <div> | |
| <h1 className="text-xl font-bold">Global Chat</h1> | |
| <p className="text-sm opacity-90">Welcome, {username}</p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm">Status:</span> | |
| {isConnected ? ( | |
| <Wifi className="w-5 h-5" /> | |
| ) : ( | |
| <WifiOff className="w-5 h-5" /> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { MessageSquare } from "lucide-react"; | |
| interface ChatJoinProps { | |
| onJoin: (username: string) => void; | |
| } | |
| export function ChatJoin({ onJoin }: ChatJoinProps) { | |
| const [input, setInput] = useState(""); | |
| const handleJoin = () => { | |
| const trimmed = input.trim(); | |
| if (trimmed) onJoin(trimmed); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50 flex items-center justify-center p-4"> | |
| <div className="w-full max-w-sm bg-white rounded-2xl shadow-xl p-8 space-y-6"> | |
| <div className="text-center"> | |
| <div className="w-16 h-16 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center mx-auto mb-4"> | |
| <MessageSquare className="w-8 h-8 text-white" /> | |
| </div> | |
| <h1 className="text-2xl font-bold">Global Chat</h1> | |
| <p className="text-gray-600 mt-2">Enter your username to join</p> | |
| </div> | |
| <div className="space-y-4"> | |
| <Input | |
| maxLength={20} | |
| placeholder="Username" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyDown={(e) => e.key === "Enter" && handleJoin()} | |
| /> | |
| <Button | |
| onClick={handleJoin} | |
| disabled={!input.trim()} | |
| className="w-full" | |
| > | |
| Join | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useWebSocket } from "@/hooks/use-websocket"; | |
| import { ChatHeader } from "./chat-header"; | |
| import { MessageList } from "./message-list"; | |
| import MessageInput from "./message-input"; | |
| import { TypingIndicator } from "./typing-indicator"; | |
| interface ChatRoomProps { | |
| username: string; | |
| } | |
| export default function ChatRoom({ username }: ChatRoomProps) { | |
| const socketUrl = "ws://localhost:3000/chat"; | |
| const { messages, sendMessage, isConnected, typingUsers, setTyping } = | |
| useWebSocket(socketUrl, username); | |
| return ( | |
| <div className="flex flex-col h-screen max-w-4xl mx-auto bg-white shadow-2xl"> | |
| <ChatHeader isConnected={isConnected} username={username} /> | |
| <MessageList messages={messages} /> | |
| <TypingIndicator typingUsers={typingUsers} /> | |
| <MessageInput | |
| sendMessage={sendMessage} | |
| setTyping={setTyping} | |
| isConnected={isConnected} | |
| /> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export interface Message { | |
| id: string; | |
| username: string; | |
| type: "message" | "notification" | "tool_response"; | |
| content: string; | |
| timestamp: string; | |
| isOwn?: boolean; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { Metadata } from "next"; | |
| import { Inter } from "next/font/google"; | |
| import "./globals.css"; | |
| import { cn } from "@/lib/utils"; | |
| const inter = Inter({ subsets: ["latin"] }); | |
| export const metadata: Metadata = { | |
| title: "Real-time Chat", | |
| description: "Beautiful real-time chat with Next.js and WebSockets", | |
| }; | |
| export default function RootLayout({ | |
| children, | |
| }: { | |
| children: React.ReactNode; | |
| }) { | |
| return ( | |
| <html lang="en"> | |
| <body className={cn(inter.className, "antialiased")}>{children}</body> | |
| </html> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState, useRef, useEffect, useCallback } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { Mic, Send, StopCircle } from "lucide-react"; | |
| interface MessageInputProps { | |
| sendMessage: (content: string) => void; | |
| setTyping: (typing: boolean) => void; | |
| isConnected: boolean; | |
| } | |
| export default function MessageInput({ | |
| sendMessage, | |
| setTyping, | |
| isConnected, | |
| }: MessageInputProps) { | |
| const [text, setText] = useState(""); | |
| const [isListening, setIsListening] = useState(false); | |
| const [browserSupported, setBrowserSupported] = useState(true); | |
| const recognitionRef = useRef<SpeechRecognition | null>(null); | |
| const timeoutRef = useRef<NodeJS.Timeout | null>(null); | |
| const handleChange = useCallback( | |
| (str: string) => { | |
| setText(str); | |
| setTyping(true); | |
| if (timeoutRef.current) clearTimeout(timeoutRef.current); | |
| timeoutRef.current = setTimeout(() => setTyping(false), 1000); | |
| }, | |
| [setTyping], | |
| ); | |
| const handleSend = () => { | |
| const trimmed = text.trim(); | |
| if (!trimmed || !isConnected) return; | |
| sendMessage(trimmed); | |
| setText(""); | |
| setTyping(false); | |
| }; | |
| useEffect(() => { | |
| const SpeechRecognitionClass = | |
| window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SpeechRecognitionClass) { | |
| setBrowserSupported(false); | |
| return; | |
| } | |
| const recognition = new SpeechRecognitionClass(); | |
| recognition.continuous = true; | |
| recognition.interimResults = true; | |
| recognition.lang = "en-US"; | |
| recognition.onresult = (event: SpeechRecognitionEvent) => { | |
| let transcript = ""; | |
| for (let i = event.resultIndex; i < event.results.length; i++) { | |
| transcript += event.results[i][0].transcript; | |
| } | |
| setText(transcript); | |
| handleChange(transcript); | |
| }; | |
| recognition.onerror = (event: SpeechRecognitionErrorEvent) => { | |
| console.error("Speech recognition error:", event.error); | |
| setIsListening(false); | |
| }; | |
| recognition.onend = () => { | |
| setIsListening(false); | |
| }; | |
| recognitionRef.current = recognition; | |
| }, [handleChange]); | |
| const startListening = () => { | |
| if (!recognitionRef.current || !browserSupported) return; | |
| setText(""); | |
| recognitionRef.current.start(); | |
| setIsListening(true); | |
| }; | |
| const stopListening = () => { | |
| if (!recognitionRef.current) return; | |
| recognitionRef.current.stop(); | |
| setIsListening(false); | |
| }; | |
| return ( | |
| <div className="p-3 border-t bg-gray-50"> | |
| {!browserSupported && ( | |
| <div className="text-sm text-red-600 mb-2"> | |
| Browser not supported for speech recognition | |
| </div> | |
| )} | |
| <div className="flex items-end gap-2"> | |
| <Textarea | |
| rows={1} | |
| disabled={!isConnected} | |
| className="min-h-0 resize-none" | |
| placeholder="Type or speak..." | |
| value={text} | |
| onChange={(e) => handleChange(e.target.value)} | |
| onKeyDown={(e) => | |
| e.key === "Enter" && | |
| !e.shiftKey && | |
| (e.preventDefault(), handleSend()) | |
| } | |
| /> | |
| {browserSupported && !isListening && ( | |
| <Button | |
| size="icon" | |
| type="button" | |
| onClick={startListening} | |
| className="rounded-full" | |
| disabled={!isConnected} | |
| > | |
| <Mic className="w-4 h-4" /> | |
| </Button> | |
| )} | |
| {browserSupported && isListening && ( | |
| <Button | |
| size="icon" | |
| type="button" | |
| variant="destructive" | |
| className="rounded-full" | |
| onClick={stopListening} | |
| > | |
| <StopCircle className="w-4 h-4" /> | |
| </Button> | |
| )} | |
| <Button | |
| size="icon" | |
| onClick={handleSend} | |
| disabled={!text.trim() || !isConnected} | |
| className="rounded-full" | |
| > | |
| <Send className="w-4 h-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useEffect, useRef } from "react"; | |
| import type { Message } from "@/types/chat"; | |
| import { Avatar, AvatarFallback } from "@/components/ui/avatar"; | |
| import { cn } from "@/lib/utils"; | |
| import ToolResponse from "./tool-response"; | |
| interface MessageListProps { | |
| messages: Message[]; | |
| } | |
| export function MessageList({ messages }: MessageListProps) { | |
| const bottomRef = useRef<HTMLDivElement>(null); | |
| useEffect( | |
| () => bottomRef.current?.scrollIntoView({ behavior: "smooth" }), | |
| [messages], | |
| ); | |
| return ( | |
| <div className="flex-1 overflow-y-auto px-4 py-3"> | |
| <div className="space-y-3"> | |
| {messages.map((msg) => | |
| msg.type === "tool_response" ? ( | |
| <div key={msg.id}> | |
| <ToolResponse response={msg.content} /> | |
| </div> | |
| ) : msg.username === "system" ? ( | |
| <div key={msg.id} className="text-center"> | |
| <span className="text-xs px-2 py-1 bg-gray-200 rounded"> | |
| {msg.content} | |
| </span> | |
| </div> | |
| ) : ( | |
| <div | |
| key={msg.id} | |
| className={cn( | |
| "flex items-start gap-2", | |
| msg.isOwn && "justify-end", | |
| )} | |
| > | |
| {!msg.isOwn && ( | |
| <Avatar className="w-8 h-8"> | |
| <AvatarFallback> | |
| {(msg.username || "?")[0].toUpperCase()} | |
| </AvatarFallback> | |
| </Avatar> | |
| )} | |
| <div | |
| className={cn( | |
| "max-w-xs rounded-lg px-3 py-2", | |
| msg.isOwn ? "bg-[#2563eb] text-white" : "bg-gray-100", | |
| )} | |
| > | |
| {!msg.isOwn && ( | |
| <p className="text-sm font-bold">{msg.username}</p> | |
| )} | |
| <p className="text-sm">{msg.content}</p> | |
| <p className="text-xs opacity-70 text-right mt-0.5"> | |
| {new Date(msg.timestamp).toLocaleTimeString([], { | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| })} | |
| </p> | |
| </div> | |
| </div> | |
| ), | |
| )} | |
| <div ref={bottomRef} /> | |
| </div> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { useState } from "react"; | |
| import ChatRoom from "@/components/chat/chat-room"; | |
| import { ChatJoin } from "@/components/chat/chat-join"; | |
| export default function Home() { | |
| const [username, setUsername] = useState<string | null>(null); | |
| if (!username) { | |
| return <ChatJoin onJoin={setUsername} />; | |
| } | |
| return <ChatRoom username={username} />; | |
| } | |
| Real-time Chat Application with MCP Support (Developed by Claude Sonnet 4) - Blog Demo |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { ChatOpenAI } from "@langchain/openai"; | |
| import { createOpenAIFunctionsAgent, AgentExecutor } from "langchain/agents"; | |
| import { LangchainToolSet } from "composio-core"; | |
| import { pull } from "langchain/hub"; | |
| export async function POST(req: Request) { | |
| const { message } = await req.json(); | |
| if (message.startsWith("#ai")) { | |
| const command = message.substring(4); | |
| const llm = new ChatOpenAI({ apiKey: process.env.OPENAI_API_KEY }); | |
| const toolset = new LangchainToolSet({ | |
| apiKey: process.env.COMPOSIO_API_KEY, | |
| }); | |
| const tools = await toolset.getTools({ | |
| apps: ["GMAIL"], | |
| }); | |
| const agent = await createOpenAIFunctionsAgent({ | |
| llm, | |
| tools, | |
| prompt: await pull("hwchase17/openai-functions-agent"), | |
| }); | |
| const agentExecutor = new AgentExecutor({ agent, tools }); | |
| const response = await agentExecutor.invoke({ input: command }); | |
| return new Response( | |
| JSON.stringify({ type: "tool_response", content: response }), | |
| { | |
| headers: { "Content-Type": "application/json" }, | |
| }, | |
| ); | |
| } | |
| return new Response(JSON.stringify({ type: "chat", content: message }), { | |
| headers: { "Content-Type": "application/json" }, | |
| }); | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const { createServer } = require("http"); | |
| const { parse } = require("url"); | |
| const next = require("next"); | |
| const { WebSocketServer } = require("ws"); | |
| const dev = process.env.NODE_ENV !== "production"; | |
| const hostname = "localhost"; | |
| const port = 3000; | |
| const app = next({ dev, hostname, port }); | |
| const handle = app.getRequestHandler(); | |
| app.prepare().then(() => { | |
| const server = createServer(async (req, res) => { | |
| const parsed = parse(req.url, true); | |
| handle(req, res, parsed); | |
| }); | |
| const wss = new WebSocketServer({ noServer: true }); | |
| server.on("upgrade", (req, socket, head) => { | |
| const { pathname } = parse(req.url, true); | |
| if (pathname !== "/chat") { | |
| socket.destroy(); | |
| return; | |
| } | |
| wss.handleUpgrade(req, socket, head, (ws) => { | |
| wss.emit("connection", ws, req); | |
| }); | |
| }); | |
| /* Map<ws, username> */ | |
| const clients = new Map(); | |
| wss.on("connection", (ws) => { | |
| let username = null; | |
| ws.on("message", (data) => { | |
| const msg = JSON.parse(data.toString()); | |
| switch (msg.type) { | |
| case "join": | |
| username = msg.username; | |
| clients.set(ws, username); | |
| broadcast(wss, { | |
| type: "notification", | |
| username: "system", | |
| content: `${username} joined the chat`, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| break; | |
| case "message": | |
| case "voice": | |
| case "image": | |
| if (!username) return; | |
| broadcast(wss, { | |
| id: Date.now().toString(), | |
| type: msg.type, | |
| username, | |
| content: msg.content, | |
| timestamp: new Date().toISOString(), | |
| fileName: msg.fileName, | |
| mimeType: msg.mimeType, | |
| }); | |
| break; | |
| case "tool_response": | |
| if (!username) return; | |
| broadcast(wss, { | |
| id: Date.now().toString(), | |
| type: msg.type, | |
| username, | |
| content: msg.content, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| break; | |
| case "typing": | |
| if (!username) return; | |
| wss.clients.forEach((c) => { | |
| if (c !== ws && c.readyState === 1) | |
| c.send( | |
| JSON.stringify({ | |
| type: "typing", | |
| username, | |
| isTyping: msg.isTyping, | |
| }), | |
| ); | |
| }); | |
| break; | |
| } | |
| }); | |
| ws.on("close", () => { | |
| if (!username) return; | |
| clients.delete(ws); | |
| broadcast(wss, { | |
| type: "notification", | |
| username: "system", | |
| content: `${username} left the chat`, | |
| timestamp: new Date().toISOString(), | |
| }); | |
| }); | |
| ws.on("error", console.error); | |
| }); | |
| server.listen(port, () => { | |
| console.log(`> Ready on http://${hostname}:${port}`); | |
| }); | |
| }); | |
| function broadcast(wss, obj) { | |
| wss.clients.forEach((c) => { | |
| if (c.readyState === 1) c.send(JSON.stringify(obj)); | |
| }); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| declare global { | |
| interface Window { | |
| SpeechRecognition?: typeof SpeechRecognition; | |
| webkitSpeechRecognition?: typeof SpeechRecognition; | |
| } | |
| class SpeechRecognition extends EventTarget { | |
| continuous: boolean; | |
| interimResults: boolean; | |
| lang: string; | |
| onresult: ((event: SpeechRecognitionEvent) => void) | null; | |
| onerror: ((event: SpeechRecognitionErrorEvent) => void) | null; | |
| onend: (() => void) | null; | |
| start(): void; | |
| stop(): void; | |
| } | |
| interface SpeechRecognitionEvent extends Event { | |
| resultIndex: number; | |
| results: SpeechRecognitionResultList; | |
| } | |
| interface SpeechRecognitionResultList { | |
| readonly length: number; | |
| item(index: number): SpeechRecognitionResult; | |
| [index: number]: SpeechRecognitionResult; | |
| } | |
| interface SpeechRecognitionResult { | |
| readonly length: number; | |
| item(index: number): SpeechRecognitionAlternative; | |
| [index: number]: SpeechRecognitionAlternative; | |
| } | |
| interface SpeechRecognitionAlternative { | |
| readonly transcript: string; | |
| readonly confidence: number; | |
| } | |
| interface SpeechRecognitionErrorEvent extends Event { | |
| readonly error: string; | |
| readonly message?: string; | |
| } | |
| } | |
| export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from 'react'; | |
| interface ToolResponseProps { | |
| response: any; | |
| } | |
| const ToolResponse: React.FC<ToolResponseProps> = ({ response }) => { | |
| return ( | |
| <div className="bg-gray-100 p-4 rounded-lg"> | |
| <h3 className="font-bold mb-2">Tool Response</h3> | |
| <pre className="whitespace-pre-wrap">{JSON.stringify(response, null, 2)}</pre> | |
| </div> | |
| ); | |
| }; | |
| export default ToolResponse; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { Users } from "lucide-react"; | |
| interface TypingIndicatorProps { | |
| typingUsers: string[]; | |
| } | |
| export function TypingIndicator({ typingUsers }: TypingIndicatorProps) { | |
| if (typingUsers.length === 0) return null; | |
| return ( | |
| <div className="flex items-center gap-2 px-3 py-1 text-sm text-gray-500"> | |
| <Users className="w-4 h-4" /> | |
| <span> | |
| {typingUsers.slice(0, 2).join(", ")} | |
| {typingUsers.length > 2 && ` and ${typingUsers.length - 2} more`}{" "} | |
| typing... | |
| </span> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useEffect, useRef, useState } from "react"; | |
| import type { Message } from "@/types/chat"; | |
| export function useWebSocket(url: string, username: string) { | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [connected, setConnected] = useState(false); | |
| const [typingUsers, setTypingUsers] = useState<string[]>([]); | |
| const wsRef = useRef<WebSocket | null>(null); | |
| useEffect(() => { | |
| if (typeof window === "undefined") return; | |
| const socket = new WebSocket(url); | |
| wsRef.current = socket; | |
| socket.onopen = () => { | |
| setConnected(true); | |
| socket.send(JSON.stringify({ type: "join", username })); | |
| }; | |
| socket.onmessage = (e) => { | |
| const data = JSON.parse(e.data); | |
| switch (data.type) { | |
| case "notification": | |
| setMessages((p) => [...p, { ...data, isOwn: false }]); | |
| break; | |
| case "message": | |
| setMessages((p) => [ | |
| ...p, | |
| { ...data, isOwn: data.username === username }, | |
| ]); | |
| break; | |
| case "tool_response": | |
| setMessages((p) => [ | |
| ...p, | |
| { ...data, isOwn: data.username === username }, | |
| ]); | |
| break; | |
| case "typing": | |
| setTypingUsers((p) => | |
| data.isTyping | |
| ? [...p.filter((u) => u !== data.username), data.username] | |
| : p.filter((u) => u !== data.username), | |
| ); | |
| break; | |
| } | |
| }; | |
| socket.onclose = () => setConnected(false); | |
| socket.onerror = console.error; | |
| return () => socket.close(); | |
| }, [url, username]); | |
| const send = (content: string) => { | |
| if (content.startsWith("#ai")) { | |
| fetch("/api/chat", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ message: content }), | |
| }) | |
| .then((res) => res.json()) | |
| .then((data) => { | |
| if (wsRef.current?.readyState !== 1) return; | |
| wsRef.current.send(JSON.stringify(data)); | |
| }); | |
| } else { | |
| if (wsRef.current?.readyState !== 1) return; | |
| wsRef.current.send(JSON.stringify({ type: "message", content })); | |
| } | |
| }; | |
| const sendTyping = (isTyping: boolean) => { | |
| if (wsRef.current?.readyState !== 1) return; | |
| wsRef.current.send(JSON.stringify({ type: "typing", isTyping })); | |
| }; | |
| return { | |
| messages, | |
| sendMessage: send, | |
| isConnected: connected, | |
| typingUsers, | |
| setTyping: sendTyping, | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Make this code work