Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save shricodev/20b7fd93456b1f71bc85a953e894b1ef to your computer and use it in GitHub Desktop.
Real-time Chat Application with MCP Support (Developed by Claude Sonnet 4) - Blog Demo
import { intentDetector, DetectedIntent } from './intent-detector'
import { composioClient, ComposioResult } from './composio-client'
export interface AgenticMessage {
id: string
text: string
sender: string
timestamp: number
type: 'user' | 'agent' | 'system' | 'tool_result'
toolCall?: {
tool: string
action: string
parameters: Record<string, unknown>
}
toolResult?: {
success: boolean
data?: unknown
error?: string
}
isProcessing?: boolean
}
export interface AgenticResponse {
responseMessage: AgenticMessage
toolExecuted: boolean
toolResult?: ComposioResult
}
export class AgenticHandler {
private isProcessing = false
async processMessage(userMessage: string): Promise<AgenticResponse> {
if (this.isProcessing) {
return {
responseMessage: {
id: Date.now().toString(),
text: 'I am currently processing another request. Please wait...',
sender: 'agent',
timestamp: Date.now(),
type: 'agent'
},
toolExecuted: false
}
}
this.isProcessing = true
try {
// Detect intent from user message
const intent = intentDetector.detectIntent(userMessage)
// If no tool intent detected, return a regular chat response
if (intent.tool === 'none' || intent.confidence < 0.5) {
return {
responseMessage: {
id: Date.now().toString(),
text: 'I understand your message, but I didn\'t detect any specific tool actions to perform. You can ask me to send emails, create calendar events, post to Slack, or create GitHub issues.',
sender: 'agent',
timestamp: Date.now(),
type: 'agent'
},
toolExecuted: false
}
}
// Create processing message
const processingMessage: AgenticMessage = {
id: Date.now().toString(),
text: `🤖 Executing ${intent.tool} action: ${intent.action}...`,
sender: 'agent',
timestamp: Date.now(),
type: 'agent',
isProcessing: true,
toolCall: {
tool: intent.tool,
action: intent.action,
parameters: intent.parameters
}
}
// Execute the tool call
const toolResult = await this.executeToolCall(intent)
// Generate response message based on result
const responseMessage = await this.generateResponseMessage(intent, toolResult)
return {
responseMessage: {
...responseMessage,
toolCall: processingMessage.toolCall,
toolResult: {
success: toolResult.success,
data: toolResult.data,
error: toolResult.error
}
},
toolExecuted: true,
toolResult
}
} catch (error: unknown) {
console.error('Error processing agentic message:', error)
return {
responseMessage: {
id: Date.now().toString(),
text: `❌ Sorry, I encountered an error while processing your request: ${error instanceof Error ? error.message : 'Unknown error'}`,
sender: 'agent',
timestamp: Date.now(),
type: 'agent'
},
toolExecuted: false
}
} finally {
this.isProcessing = false
}
}
private async executeToolCall(intent: DetectedIntent): Promise<ComposioResult> {
// Validate required parameters
const missingParams = this.validateParameters(intent)
if (missingParams.length > 0) {
return {
success: false,
error: `Missing required parameters: ${missingParams.join(', ')}`
}
}
// Initialize Composio if not already done
if (!composioClient.isClientInitialized()) {
try {
await composioClient.initialize()
} catch {
return {
success: false,
error: 'Failed to initialize Composio client. Please check your API key.'
}
}
}
// Execute the specific tool action
switch (intent.tool) {
case 'gmail':
return await this.executeGmailAction(intent)
case 'slack':
return await this.executeSlackAction(intent)
case 'github':
return await this.executeGithubAction(intent)
case 'calendar':
return await this.executeCalendarAction(intent)
default:
return {
success: false,
error: `Unsupported tool: ${intent.tool}`
}
}
}
private async executeGmailAction(intent: DetectedIntent): Promise<ComposioResult> {
const { to, subject, body } = intent.parameters
if (!to || typeof to !== 'string') {
return {
success: false,
error: 'Email address is required'
}
}
return await composioClient.sendEmail(
to,
(subject as string) || 'Message from Agentic Chat',
(body as string) || intent.originalMessage
)
}
private async executeSlackAction(intent: DetectedIntent): Promise<ComposioResult> {
const { channel, message } = intent.parameters
return await composioClient.sendSlackMessage(
(channel as string) || '#general',
(message as string) || intent.originalMessage
)
}
private async executeGithubAction(intent: DetectedIntent): Promise<ComposioResult> {
const { repo, title, body } = intent.parameters
if (!repo || typeof repo !== 'string') {
return {
success: false,
error: 'Repository name is required (format: owner/repo)'
}
}
return await composioClient.createGithubIssue(
repo,
(title as string) || 'Issue from Agentic Chat',
(body as string) || intent.originalMessage
)
}
private async executeCalendarAction(intent: DetectedIntent): Promise<ComposioResult> {
const { summary, start, end, description } = intent.parameters
if (!start || typeof start !== 'string') {
return {
success: false,
error: 'Start time is required for calendar events'
}
}
return await composioClient.createCalendarEvent(
(summary as string) || 'Event from Agentic Chat',
start,
(end as string) || this.calculateEndTime(start),
(description as string) || intent.originalMessage
)
}
private validateParameters(intent: DetectedIntent): string[] {
const missing: string[] = []
switch (intent.tool) {
case 'gmail':
if (!intent.parameters.to) missing.push('email address')
break
case 'github':
if (!intent.parameters.repo) missing.push('repository')
break
case 'calendar':
if (!intent.parameters.start) missing.push('start time')
break
}
return missing
}
private async generateResponseMessage(intent: DetectedIntent, result: ComposioResult): Promise<AgenticMessage> {
const baseMessage: AgenticMessage = {
id: Date.now().toString(),
text: '', // Will be set below
sender: 'agent',
timestamp: Date.now(),
type: 'tool_result'
}
if (result.success) {
const successMessages: Record<string, string> = {
gmail: '✅ Email sent successfully!',
slack: '✅ Slack message posted successfully!',
github: '✅ GitHub issue created successfully!',
calendar: '✅ Calendar event created successfully!'
}
return {
...baseMessage,
text: successMessages[intent.tool] || '✅ Action completed successfully!'
}
} else {
return {
...baseMessage,
text: `❌ Failed to execute ${intent.tool} action: ${result.error}`
}
}
}
private calculateEndTime(startTime: string): string {
const start = new Date(startTime)
const end = new Date(start.getTime() + 60 * 60 * 1000) // Add 1 hour
return end.toISOString()
}
isCurrentlyProcessing(): boolean {
return this.isProcessing
}
}
export const agenticHandler = new AgenticHandler()
"use client";
import { useState, useEffect, useRef } from "react";
import { io, Socket } from "socket.io-client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Send,
Users,
Mic,
MicOff,
Bot,
Loader2,
CheckCircle,
AlertCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useVoiceRecognition } from "@/hooks/use-voice-recognition";
import { agenticHandler } from "@/lib/agentic-handler";
interface Message {
id: string;
text: string;
sender: string;
timestamp: number;
type?: "user" | "agent" | "system" | "tool_result";
toolCall?: {
tool: string;
action: string;
parameters: Record<string, unknown>;
};
toolResult?: {
success: boolean;
data?: unknown;
error?: string;
};
isProcessing?: boolean;
}
interface ChatInterfaceProps {
username: string;
}
export function ChatInterface({ username }: ChatInterfaceProps) {
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [messageText, setMessageText] = useState("");
const [userCount, setUserCount] = useState(0);
const [isConnected, setIsConnected] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const {
isListening,
transcript,
isSupported,
startListening,
stopListening,
resetTranscript,
} = useVoiceRecognition();
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (transcript) {
setMessageText(transcript);
}
}, [transcript]);
useEffect(() => {
const newSocket = io(
process.env.NODE_ENV === "production"
? undefined
: "http://localhost:3000",
);
newSocket.on("connect", () => {
console.log("Connected to server");
setIsConnected(true);
newSocket.emit("join-chat", username);
});
newSocket.on("disconnect", () => {
console.log("Disconnected from server");
setIsConnected(false);
});
newSocket.on("user-count", (count: number) => {
setUserCount(count);
});
newSocket.on("receive-message", (message: Message) => {
setMessages((prev) => [...prev, message]);
});
newSocket.on("user-joined", (joinedUsername: string) => {
const joinMessage: Message = {
id: Date.now().toString(),
text: `${joinedUsername} joined the chat`,
sender: "system",
timestamp: Date.now(),
};
setMessages((prev) => [...prev, joinMessage]);
});
newSocket.on("user-left", (leftUsername: string) => {
const leaveMessage: Message = {
id: Date.now().toString(),
text: `${leftUsername} left the chat`,
sender: "system",
timestamp: Date.now(),
};
setMessages((prev) => [...prev, leaveMessage]);
});
setSocket(newSocket);
return () => {
newSocket.close();
};
}, [username]);
const sendMessage = async () => {
if (!messageText.trim() || !socket) return;
const userMessage: Message = {
id: Date.now().toString(),
text: messageText,
sender: username,
timestamp: Date.now(),
type: "user",
};
// Add user message immediately
setMessages((prev) => [...prev, userMessage]);
socket.emit("send-message", userMessage);
const messageToProcess = messageText;
setMessageText("");
resetTranscript();
try {
// Process message for agentic workflow
const agenticResponse =
await agenticHandler.processMessage(messageToProcess);
// Add agent response
const agentMessage = {
...agenticResponse.responseMessage,
id: Date.now().toString(),
};
setMessages((prev) => [...prev, agentMessage]);
socket.emit("send-message", agentMessage);
} catch (error) {
console.error("Error processing agentic message:", error);
const errorMessage: Message = {
id: Date.now().toString(),
text: "❌ Sorry, I encountered an error processing your request.",
sender: "agent",
timestamp: Date.now(),
type: "agent",
};
setMessages((prev) => [...prev, errorMessage]);
socket.emit("send-message", errorMessage);
}
};
const toggleVoiceRecording = () => {
if (isListening) {
stopListening();
} else {
resetTranscript();
setMessageText("");
startListening();
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
return (
<div className="max-w-4xl mx-auto p-4 h-screen flex flex-col">
<Card className="flex-1 flex flex-col">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-2xl font-bold">Real-time Chat</CardTitle>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Users className="w-4 h-4" />
<span>{userCount} online</span>
<div
className={cn(
"w-2 h-2 rounded-full",
isConnected ? "bg-green-500" : "bg-red-500",
)}
/>
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col space-y-4">
<ScrollArea className="flex-1 pr-4">
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={cn(
"flex gap-3",
message.sender === username && "flex-row-reverse",
message.sender === "system" && "justify-center",
)}
>
{message.sender !== "system" && (
<Avatar className="w-8 h-8">
<AvatarFallback
className={cn(
message.type === "agent" ||
message.type === "tool_result"
? "bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300"
: "",
)}
>
{message.type === "agent" ||
message.type === "tool_result" ? (
<Bot className="w-4 h-4" />
) : (
message.sender[0].toUpperCase()
)}
</AvatarFallback>
</Avatar>
)}
<div
className={cn(
"max-w-xs lg:max-w-md xl:max-w-lg",
message.sender === "system" && "max-w-none",
)}
>
{message.sender !== "system" && (
<div
className={cn(
"text-xs text-muted-foreground mb-1 flex items-center gap-1",
message.sender === username &&
"text-right justify-end",
)}
>
{message.type === "agent" ||
message.type === "tool_result" ? (
<>
<Bot className="w-3 h-3" />
Agent
</>
) : message.sender === username ? (
"You"
) : (
message.sender
)}
{message.isProcessing && (
<Loader2 className="w-3 h-3 animate-spin" />
)}
</div>
)}
<div
className={cn(
"rounded-lg px-3 py-2 text-sm",
message.sender === username &&
"bg-primary text-primary-foreground ml-auto",
(message.type === "agent" ||
message.type === "tool_result") &&
"bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-100",
message.sender !== username &&
message.sender !== "system" &&
message.type !== "agent" &&
message.type !== "tool_result" &&
"bg-muted",
message.sender === "system" &&
"bg-muted/50 text-muted-foreground text-center italic",
)}
>
<div className="flex items-center gap-2">
{message.toolResult?.success === true && (
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0" />
)}
{message.toolResult?.success === false && (
<AlertCircle className="w-4 h-4 text-red-500 flex-shrink-0" />
)}
<span>{message.text}</span>
</div>
{message.toolCall && (
<div className="mt-2 p-2 bg-black/5 dark:bg-white/5 rounded text-xs">
<div className="font-medium">
Tool: {message.toolCall.tool}
</div>
<div className="text-muted-foreground">
Action: {message.toolCall.action}
</div>
{Object.keys(message.toolCall.parameters).length >
0 && (
<div className="mt-1">
<div className="text-muted-foreground">
Parameters:
</div>
<pre className="text-xs mt-1 whitespace-pre-wrap">
{JSON.stringify(
message.toolCall.parameters,
null,
2,
)}
</pre>
</div>
)}
</div>
)}
</div>
<div
className={cn(
"text-xs text-muted-foreground mt-1",
message.sender === username && "text-right",
message.sender === "system" && "text-center",
)}
>
{new Date(message.timestamp).toLocaleTimeString()}
</div>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
</ScrollArea>
<div className="space-y-2">
{isListening && (
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 rounded-lg px-3 py-2">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
<span>Listening... Speak now</span>
</div>
</div>
)}
<div className="flex gap-2">
<Input
placeholder={
isListening
? "Listening..."
: "Ask me to send emails, create calendar events, post to Slack, or create GitHub issues..."
}
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
onKeyPress={handleKeyPress}
disabled={!isConnected || isListening}
className={cn(
"flex-1",
isListening &&
"border-red-300 bg-red-50/50 dark:bg-red-950/20 dark:border-red-800",
)}
/>
{isSupported && (
<Button
onClick={toggleVoiceRecording}
disabled={!isConnected}
size="icon"
variant={isListening ? "destructive" : "outline"}
className={cn(
"transition-all duration-200",
isListening && "animate-pulse",
)}
>
{isListening ? (
<MicOff className="w-4 h-4" />
) : (
<Mic className="w-4 h-4" />
)}
</Button>
)}
<Button
onClick={sendMessage}
disabled={!messageText.trim() || !isConnected}
size="icon"
>
<Send className="w-4 h-4" />
</Button>
</div>
{!isSupported && (
<div className="text-xs text-muted-foreground">
Voice recording not supported in this browser
</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}
import { ComposioToolSet } from "composio-core";
export interface ComposioAction {
name: string;
description: string;
parameters: Record<string, unknown>;
}
export interface ComposioToolCall {
tool: string;
action: string;
parameters: Record<string, unknown>;
}
export interface ComposioResult {
success: boolean;
data?: unknown;
error?: string;
}
export class ComposioClient {
private toolset: ComposioToolSet | null = null;
private isInitialized = false;
async initialize(apiKey?: string): Promise<void> {
try {
// Initialize Composio with API key from environment or parameter
const composioApiKey = apiKey || process.env.COMPOSIO_API_KEY;
if (!composioApiKey) {
throw new Error("Composio API key not provided");
}
this.toolset = new ComposioToolSet({
apiKey: composioApiKey,
baseUrl: process.env.COMPOSIO_BASE_URL,
});
this.isInitialized = true;
console.log("Composio client initialized successfully");
} catch (error) {
console.error("Failed to initialize Composio client:", error);
throw error;
}
}
async getAvailableActions(_apps?: string[]): Promise<ComposioAction[]> {
if (!this.toolset || !this.isInitialized) {
throw new Error("Composio client not initialized");
}
try {
const availableActions: ComposioAction[] = [
{
name: "gmail_send_email",
description: "Send an email via Gmail",
parameters: { to: "string", subject: "string", body: "string" },
},
{
name: "slack_send_message",
description: "Send a message to Slack",
parameters: { channel: "string", message: "string" },
},
{
name: "github_create_issue",
description: "Create a GitHub issue",
parameters: { repo: "string", title: "string", body: "string" },
},
{
name: "calendar_create_event",
description: "Create a calendar event",
parameters: {
summary: "string",
start: "string",
end: "string",
description: "string",
},
},
];
return availableActions;
} catch (error) {
console.error("Failed to get available actions:", error);
return [];
}
}
async executeAction(toolCall: ComposioToolCall): Promise<ComposioResult> {
if (!this.toolset || !this.isInitialized) {
throw new Error("Composio client not initialized");
}
try {
console.log(
`Simulating execution of ${toolCall.tool}_${toolCall.action} with params:`,
toolCall.parameters,
);
return {
success: true,
data: {
message: `Successfully executed ${toolCall.tool}_${toolCall.action}`,
parameters: toolCall.parameters,
},
};
} catch (error) {
console.error("Failed to execute action:", error);
return {
success: false,
error:
error instanceof Error ? error.message : "Unknown error occurred",
};
}
}
// Specific methods for common tools
async sendEmail(
to: string,
subject: string,
body: string,
): Promise<ComposioResult> {
return this.executeAction({
tool: "gmail",
action: "send_email",
parameters: {
to,
subject,
body,
},
});
}
async sendSlackMessage(
channel: string,
message: string,
): Promise<ComposioResult> {
return this.executeAction({
tool: "slack",
action: "send_message",
parameters: {
channel,
message,
},
});
}
async createGithubIssue(
repo: string,
title: string,
description: string,
): Promise<ComposioResult> {
return this.executeAction({
tool: "github",
action: "create_issue",
parameters: {
repo,
title,
body: description,
},
});
}
async createCalendarEvent(
title: string,
startTime: string,
endTime: string,
description?: string,
): Promise<ComposioResult> {
return this.executeAction({
tool: "calendar",
action: "create_event",
parameters: {
summary: title,
start: { dateTime: startTime },
end: { dateTime: endTime },
description,
},
});
}
isClientInitialized(): boolean {
return this.isInitialized;
}
}
// Singleton instance for the application
export const composioClient = new ComposioClient();
export interface DetectedIntent {
tool: 'gmail' | 'slack' | 'github' | 'calendar' | 'drive' | 'none'
action: string
parameters: Record<string, unknown>
confidence: number
originalMessage: string
}
export interface IntentPattern {
keywords: string[]
tool: string
action: string
parameterExtractors: Record<string, (text: string) => unknown>
}
export class IntentDetector {
private patterns: IntentPattern[] = [
// Gmail patterns
{
keywords: ['send email', 'email', 'mail', 'send message'],
tool: 'gmail',
action: 'send_email',
parameterExtractors: {
to: (text: string) => this.extractEmail(text),
subject: (text: string) => this.extractSubject(text),
body: (text: string) => this.extractEmailBody(text)
}
},
// Slack patterns
{
keywords: ['slack', 'send slack', 'slack message', 'post to slack'],
tool: 'slack',
action: 'send_message',
parameterExtractors: {
channel: (text: string) => this.extractSlackChannel(text),
message: (text: string) => this.extractSlackMessage(text)
}
},
// GitHub patterns
{
keywords: ['create issue', 'github issue', 'open issue', 'new issue'],
tool: 'github',
action: 'create_issue',
parameterExtractors: {
repo: (text: string) => this.extractGithubRepo(text),
title: (text: string) => this.extractIssueTitle(text),
body: (text: string) => this.extractIssueBody(text)
}
},
// Calendar patterns
{
keywords: ['schedule', 'calendar', 'meeting', 'appointment', 'event'],
tool: 'calendar',
action: 'create_event',
parameterExtractors: {
summary: (text: string) => this.extractEventTitle(text),
start: (text: string) => this.extractStartTime(text),
end: (text: string) => this.extractEndTime(text),
description: (text: string) => this.extractEventDescription(text)
}
}
]
detectIntent(message: string): DetectedIntent {
const normalizedMessage = message.toLowerCase()
for (const pattern of this.patterns) {
const matchedKeywords = pattern.keywords.filter(keyword =>
normalizedMessage.includes(keyword.toLowerCase())
)
if (matchedKeywords.length > 0) {
const confidence = matchedKeywords.length / pattern.keywords.length
const parameters: Record<string, unknown> = {}
// Extract parameters using the pattern's extractors
for (const [paramName, extractor] of Object.entries(pattern.parameterExtractors)) {
try {
const value = extractor(message)
if (value) {
parameters[paramName] = value as string
}
} catch (error) {
console.warn(`Failed to extract parameter ${paramName}:`, error)
}
}
return {
tool: pattern.tool as 'gmail' | 'slack' | 'github' | 'calendar' | 'drive',
action: pattern.action,
parameters,
confidence,
originalMessage: message
}
}
}
return {
tool: 'none',
action: '',
parameters: {},
confidence: 0,
originalMessage: message
}
}
// Email extractors
private extractEmail(text: string): string | null {
const emailRegex = /[\w\.-]+@[\w\.-]+\.\w+/g
const matches = text.match(emailRegex)
return matches ? matches[0] : null
}
private extractSubject(text: string): string | null {
// Look for patterns like "subject: ...", "title: ...", or quoted strings
const subjectPatterns = [
/subject[:\s]+([^,\n]+)/i,
/title[:\s]+([^,\n]+)/i,
/"([^"]+)"/,
/'([^']+)'/
]
for (const pattern of subjectPatterns) {
const match = text.match(pattern)
if (match) {
return match[1].trim()
}
}
// Fallback: extract text after "send email to X about Y"
const aboutMatch = text.match(/about\s+(.+?)(?:\s+with|$)/i)
if (aboutMatch) {
return aboutMatch[1].trim()
}
return null
}
private extractEmailBody(text: string): string {
// Remove the command part and extract the body
const cleanedText = text
.replace(/send\s+email\s+to\s+[\w\.-]+@[\w\.-]+\.\w+/i, '')
.replace(/subject[:\s]+[^,\n]+/i, '')
.replace(/about\s+[^,\n]+/i, '')
.trim()
return cleanedText || 'Message sent from agentic chat'
}
// Slack extractors
private extractSlackChannel(text: string): string {
const channelMatch = text.match(/#([a-zA-Z0-9-_]+)/g)
if (channelMatch) {
return channelMatch[0]
}
const toMatch = text.match(/to\s+([a-zA-Z0-9-_]+)/i)
if (toMatch) {
return `#${toMatch[1]}`
}
return '#general'
}
private extractSlackMessage(text: string): string {
return text
.replace(/send\s+slack\s+message/i, '')
.replace(/to\s+#?[a-zA-Z0-9-_]+/i, '')
.replace(/post\s+to\s+slack/i, '')
.trim()
}
// GitHub extractors
private extractGithubRepo(text: string): string | null {
const repoMatch = text.match(/repo[:\s]+([a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)/i)
if (repoMatch) {
return repoMatch[1]
}
const inMatch = text.match(/in\s+([a-zA-Z0-9-_]+\/[a-zA-Z0-9-_]+)/i)
if (inMatch) {
return inMatch[1]
}
return null
}
private extractIssueTitle(text: string): string {
const titleMatch = text.match(/title[:\s]+"([^"]+)"/i) || text.match(/title[:\s]+([^,\n]+)/i)
if (titleMatch) {
return titleMatch[1].trim()
}
// Fallback: extract from the main text
return text.replace(/create\s+issue/i, '').replace(/github\s+issue/i, '').trim()
}
private extractIssueBody(text: string): string {
const bodyMatch = text.match(/description[:\s]+(.+)/i) || text.match(/body[:\s]+(.+)/i)
if (bodyMatch) {
return bodyMatch[1].trim()
}
return 'Issue created from agentic chat'
}
// Calendar extractors
private extractEventTitle(text: string): string {
const titleMatch = text.match(/schedule\s+(.+?)\s+(?:at|on|for)/i)
if (titleMatch) {
return titleMatch[1].trim()
}
return text.replace(/schedule|meeting|appointment|event/gi, '').trim()
}
private extractStartTime(text: string): string | null {
const timePatterns = [
/at\s+(\d{1,2}:\d{2}(?:\s*[ap]m)?)/i,
/(\d{1,2}:\d{2}(?:\s*[ap]m)?)/i,
/on\s+([a-zA-Z]+\s+\d{1,2})/i
]
for (const pattern of timePatterns) {
const match = text.match(pattern)
if (match) {
// This is simplified - in practice you'd want proper date parsing
return new Date().toISOString()
}
}
return null
}
private extractEndTime(text: string): string | null {
// Simplified - would need more sophisticated parsing
const startTime = this.extractStartTime(text)
if (startTime) {
const endTime = new Date(startTime)
endTime.setHours(endTime.getHours() + 1)
return endTime.toISOString()
}
return null
}
private extractEventDescription(text: string): string {
return text.replace(/schedule|meeting|appointment|event/gi, '').trim()
}
}
export const intentDetector = new IntentDetector()
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
export interface MCPToolCall {
name: string
arguments: Record<string, unknown>
}
export interface MCPToolResult {
content: Array<{
type: 'text'
text: string
}>
isError?: boolean
}
export interface MCPServerConfig {
name: string
command: string
args?: string[]
env?: Record<string, string>
}
export class MCPClient {
private client: Client | null = null
private transport: StdioClientTransport | null = null
private isConnected = false
async connect(serverConfig: MCPServerConfig): Promise<void> {
try {
// Create transport for the MCP server
this.transport = new StdioClientTransport({
command: serverConfig.command,
args: serverConfig.args || [],
env: {
...Object.fromEntries(
Object.entries(process.env).filter(([, value]) => value !== undefined)
) as Record<string, string>,
...serverConfig.env
}
})
// Create MCP client
this.client = new Client({
name: 'agentic-chat-client',
version: '1.0.0'
}, {
capabilities: {
tools: {}
}
})
// Connect to server
await this.client.connect(this.transport)
this.isConnected = true
console.log(`Connected to MCP server: ${serverConfig.name}`)
} catch (error) {
console.error('Failed to connect to MCP server:', error)
throw error
}
}
async disconnect(): Promise<void> {
if (this.client) {
await this.client.close()
this.client = null
}
if (this.transport) {
await this.transport.close()
this.transport = null
}
this.isConnected = false
}
async listTools(): Promise<unknown[]> {
if (!this.client || !this.isConnected) {
throw new Error('MCP client not connected')
}
try {
const response = await this.client.listTools()
return response.tools || []
} catch (error) {
console.error('Failed to list tools:', error)
return []
}
}
async callTool(toolCall: MCPToolCall): Promise<MCPToolResult> {
if (!this.client || !this.isConnected) {
throw new Error('MCP client not connected')
}
try {
const response = await this.client.callTool({
name: toolCall.name,
arguments: toolCall.arguments
})
return {
content: response.content as Array<{ type: 'text'; text: string }>,
isError: response.isError as boolean
}
} catch (error) {
console.error('Failed to call tool:', error)
return {
content: [{ type: 'text', text: `Error calling tool: ${error instanceof Error ? error.message : 'Unknown error'}` }],
isError: true
}
}
}
isClientConnected(): boolean {
return this.isConnected
}
}
// Singleton instance for the application
export const mcpClient = new MCPClient()
'use client'
import { useState } from 'react'
import { ChatInterface } from '@/components/chat/chat-interface'
import { UsernameForm } from '@/components/chat/username-form'
export default function Home() {
const [username, setUsername] = useState<string | null>(null)
if (!username) {
return <UsernameForm onUsernameSubmit={setUsername} />
}
return <ChatInterface username={username} />
}
const { createServer } = require("http");
const { parse } = require("url");
const next = require("next");
const { Server } = require("socket.io");
const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = process.env.PORT || 3000;
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const httpServer = createServer(async (req, res) => {
try {
const parsedUrl = parse(req.url, true);
await handle(req, res, parsedUrl);
} catch (err) {
console.error("Error occurred handling", req.url, err);
res.statusCode = 500;
res.end("internal server error");
}
});
const io = new Server(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
let userCount = 0;
const connectedUsers = new Map();
io.on("connection", (socket) => {
userCount++;
console.log(`User connected: ${socket.id}, total users: ${userCount}`);
socket.emit("user-count", userCount);
socket.broadcast.emit("user-count", userCount);
socket.on("join-chat", (username) => {
connectedUsers.set(socket.id, username);
socket.broadcast.emit("user-joined", username);
console.log(`${username} joined the chat`);
});
socket.on("send-message", (message) => {
// Enhanced logging for agentic messages
if (message.type === "tool_result" || message.toolCall) {
console.log(`Tool message from ${message.sender}:`, {
text: message.text,
tool: message.toolCall?.tool,
action: message.toolCall?.action,
success: message.toolResult?.success,
});
} else {
console.log(`Message from ${message.sender}: ${message.text}`);
}
socket.broadcast.emit("receive-message", message);
});
socket.on("disconnect", () => {
userCount--;
const username = connectedUsers.get(socket.id);
if (username) {
connectedUsers.delete(socket.id);
socket.broadcast.emit("user-left", username);
console.log(`${username} left the chat`);
}
console.log(`User disconnected: ${socket.id}, total users: ${userCount}`);
socket.broadcast.emit("user-count", userCount);
});
});
httpServer
.once("error", (err) => {
console.error(err);
process.exit(1);
})
.listen(port, () => {
console.log(`> Ready on http://${hostname}:${port}`);
});
});
export interface Message {
id: string
text: string
sender: string
timestamp: number
type?: 'user' | 'agent' | 'system' | 'tool_result'
toolCall?: {
tool: string
action: string
parameters: Record<string, unknown>
}
toolResult?: {
success: boolean
data?: unknown
error?: string
}
isProcessing?: boolean
}
"use client";
import { useState, useRef, useCallback } from "react";
interface SpeechRecognitionEvent {
results: SpeechRecognitionResultList;
resultIndex: number;
}
interface SpeechRecognition extends EventTarget {
continuous: boolean;
interimResults: boolean;
lang: string;
onstart: ((event: Event) => void) | null;
onend: ((event: Event) => void) | null;
onresult: ((event: SpeechRecognitionEvent) => void) | null;
onerror: ((event: any) => void) | null;
start(): void;
stop(): void;
abort(): void;
}
declare global {
interface Window {
SpeechRecognition: new () => SpeechRecognition;
webkitSpeechRecognition: new () => SpeechRecognition;
}
}
export const useVoiceRecognition = () => {
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState("");
const [isSupported, setIsSupported] = useState(false);
const recognitionRef = useRef<SpeechRecognition | null>(null);
const initializeRecognition = useCallback(() => {
if (typeof window === "undefined") return false;
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
setIsSupported(false);
return false;
}
setIsSupported(true);
recognitionRef.current = new SpeechRecognition();
const recognition = recognitionRef.current;
recognition.continuous = false;
recognition.interimResults = true;
recognition.lang = "en-US";
recognition.onstart = () => {
setIsListening(true);
};
recognition.onend = () => {
setIsListening(false);
};
recognition.onresult = (event: SpeechRecognitionEvent) => {
let finalTranscript = "";
let interimTranscript = "";
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript + " ";
} else {
interimTranscript += transcript;
}
}
setTranscript(finalTranscript || interimTranscript);
};
recognition.onerror = (event: any) => {
console.error("Speech recognition error:", event.error);
setIsListening(false);
};
return true;
}, []);
const startListening = useCallback(() => {
if (!recognitionRef.current && !initializeRecognition()) {
return;
}
if (recognitionRef.current && !isListening) {
setTranscript("");
recognitionRef.current.start();
}
}, [isListening, initializeRecognition]);
const stopListening = useCallback(() => {
if (recognitionRef.current && isListening) {
recognitionRef.current.stop();
}
}, [isListening]);
const resetTranscript = useCallback(() => {
setTranscript("");
}, []);
return {
isListening,
transcript,
isSupported,
startListening,
stopListening,
resetTranscript,
};
};
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { MessageCircle } from 'lucide-react'
interface UsernameFormProps {
onUsernameSubmit: (username: string) => void
}
export function UsernameForm({ onUsernameSubmit }: UsernameFormProps) {
const [username, setUsername] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (username.trim()) {
onUsernameSubmit(username.trim())
}
}
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto mb-4 p-3 bg-primary/10 rounded-full w-fit">
<MessageCircle className="w-6 h-6 text-primary" />
</div>
<CardTitle className="text-2xl font-bold">Join Chat</CardTitle>
<CardDescription>
Enter your username to start chatting with others
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Input
placeholder="Enter your username"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="text-center"
maxLength={20}
required
/>
</div>
<Button type="submit" className="w-full" disabled={!username.trim()}>
Start Chatting
</Button>
</form>
</CardContent>
</Card>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment