Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created July 20, 2025 16:24
Show Gist options
  • Select an option

  • Save shricodev/d8b980224127ef211c2e1f4ad1dd1bac to your computer and use it in GitHub Desktop.

Select an option

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
"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>
);
}
"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>
);
}
"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>
);
}
export interface Message {
id: string;
username: string;
type: "message" | "notification" | "tool_response";
content: string;
timestamp: string;
isOwn?: boolean;
}
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>
);
}
"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>
);
}
"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>
);
}
"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
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" },
});
}
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));
});
}
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 {};
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;
"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>
);
}
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,
};
}
@alpha55681
Copy link
Copy Markdown

Make this code work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment