Advanced Usage
Explore advanced features and hook combinations for building sophisticated chat interfaces.
Individual Hooks
For more control, use individual hooks to build custom UIs with fine-grained state management:
"use client";
import {
useMelonyMessages,
useMelonySend,
useMelonyStatus,
useMelonyPart,
} from "melony";
export function AdvancedChat() {
const messages = useMelonyMessages({
filter: (part) => part.type === "text",
joinTextDeltas: true,
limit: 50,
});
const send = useMelonySend();
const status = useMelonyStatus();
// Listen to individual parts as they stream
useMelonyPart((part) => {
console.log("New part received:", part);
});
return (
<div>
<div className="messages">
{messages.map((message) => (
<div key={message.id} className={`message ${message.role}`}>
{message.parts.map((part, i) => (
<div key={i}>{part.type === "text" && part.text}</div>
))}
</div>
))}
</div>
<div className="input">
<button
onClick={() => send("Hello!")}
disabled={status === "streaming"}
>
{status === "streaming" ? "Sending..." : "Send"}
</button>
{status === "error" && <p>Error occurred. Please try again.</p>}
</div>
</div>
);
}
Message Filtering
Filter messages and parts to show only what you need:
Filter by part type
const textMessages = useMelonyMessages({
filter: (part) => part.type === "text",
});
Filter by role
const userMessages = useMelonyMessages({
filter: (message) => message.role === "user",
});
Complex filtering
const recentTextMessages = useMelonyMessages({
filter: (part) =>
part.type === "text" &&
part.text &&
part.text.length > 10,
limit: 20,
});
Message Grouping
Group messages by different criteria for better organization:
Group by role
const groupedMessages = useMelonyMessages({
groupBy: (message) => message.role,
});
Group by timestamp
const timeGroupedMessages = useMelonyMessages({
groupBy: (message) => {
const date = new Date(message.createdAt);
return date.toDateString();
},
});
Message Sorting
Sort messages in different orders:
Sort by timestamp (newest first)
const sortedMessages = useMelonyMessages({
sortBy: (a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
});
Sort by message length
const lengthSortedMessages = useMelonyMessages({
sortBy: (a, b) => {
const aLength = a.parts.reduce((sum, part) => sum + (part.text?.length || 0), 0);
const bLength = b.parts.reduce((sum, part) => sum + (part.text?.length || 0), 0);
return bLength - aLength;
},
});
Part Listening
Listen to individual parts as they stream in for real-time updates:
"use client";
import { useMelonyPart } from "melony";
function RealTimeChat() {
const [typingIndicator, setTypingIndicator] = useState(false);
const [lastPart, setLastPart] = useState(null);
// Listen to all parts as they arrive
useMelonyPart((part) => {
setLastPart(part);
if (part.type === "text" && part.text) {
setTypingIndicator(true);
// Hide typing indicator after a delay
setTimeout(() => setTypingIndicator(false), 1000);
}
});
return (
<div>
{lastPart && (
<div className="last-part">
Last part: {lastPart.type} - {lastPart.text}
</div>
)}
{typingIndicator && (
<div className="typing-indicator">
Assistant is typing...
</div>
)}
</div>
);
}
Custom Send Function
Create custom send functions with additional logic:
function CustomChat() {
const send = useMelonySend();
const status = useMelonyStatus();
const sendWithRetry = async (message: string, maxRetries = 3) => {
let attempts = 0;
while (attempts < maxRetries) {
try {
await send(message);
break;
} catch (error) {
attempts++;
if (attempts >= maxRetries) {
console.error("Failed to send message after", maxRetries, "attempts");
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
}
}
};
const sendWithMetadata = async (message: string) => {
const messageWithMeta = {
text: message,
metadata: {
timestamp: Date.now(),
userId: "user123",
sessionId: "session456",
},
};
await send(JSON.stringify(messageWithMeta));
};
return (
<div>
<button onClick={() => sendWithRetry("Hello!")}>
Send with Retry
</button>
<button onClick={() => sendWithMetadata("Hello with metadata!")}>
Send with Metadata
</button>
</div>
);
}
Error Handling
Implement robust error handling for better user experience:
function ErrorHandlingChat() {
const messages = useMelonyMessages();
const send = useMelonySend();
const status = useMelonyStatus();
const [error, setError] = useState(null);
const handleSend = async (message: string) => {
try {
setError(null);
await send(message);
} catch (err) {
setError(err.message || "Failed to send message");
}
};
const retryLastMessage = () => {
const lastUserMessage = messages
.filter(m => m.role === "user")
.pop();
if (lastUserMessage) {
const lastText = lastUserMessage.parts
.find(p => p.type === "text")?.text;
if (lastText) {
handleSend(lastText);
}
}
};
return (
<div>
{messages.map((message) => (
<div key={message.id} className={`message ${message.role}`}>
{message.parts.map((part, i) => (
<div key={i}>
{part.type === "text" && part.text}
{part.type === "error" && (
<div className="error-part text-red-500">
Error: {part.errorMessage}
</div>
)}
</div>
))}
</div>
))}
{status === "error" && (
<div className="error-banner">
<p>Something went wrong. Please try again.</p>
<button onClick={retryLastMessage}>Retry Last Message</button>
</div>
)}
{error && (
<div className="error-message">
{error}
</div>
)}
</div>
);
}
Performance Optimization
Optimize your chat interface for better performance:
Limit message count
const recentMessages = useMelonyMessages({
limit: 50, // Only keep last 50 messages
});
Memoize expensive operations
const processedMessages = useMemo(() => {
return messages.map(message => ({
...message,
wordCount: message.parts.reduce((count, part) =>
count + (part.text?.split(' ').length || 0), 0
),
}));
}, [messages]);
Virtual scrolling for large lists
import { FixedSizeList as List } from 'react-window';
function VirtualizedChat() {
const messages = useMelonyMessages();
const MessageItem = ({ index, style }) => (
<div style={style}>
{renderMessage(messages[index])}
</div>
);
return (
<List
height={600}
itemCount={messages.length}
itemSize={100}
>
{MessageItem}
</List>
);
}
Complete Advanced Example
Here's a complete example showcasing advanced features:
"use client";
import { useState, useMemo, useCallback } from "react";
import {
MelonyProvider,
useMelonyMessages,
useMelonySend,
useMelonyStatus,
useMelonyPart,
} from "melony";
function AdvancedChatInterface() {
const [searchTerm, setSearchTerm] = useState("");
const [showOnlyUserMessages, setShowOnlyUserMessages] = useState(false);
const messages = useMelonyMessages({
filter: (message) => {
if (showOnlyUserMessages && message.role !== "user") return false;
if (searchTerm && !message.parts.some(part =>
part.text?.toLowerCase().includes(searchTerm.toLowerCase())
)) return false;
return true;
},
sortBy: (a, b) => a.createdAt.getTime() - b.createdAt.getTime(),
limit: 100,
});
const send = useMelonySend();
const status = useMelonyStatus();
const [typingIndicator, setTypingIndicator] = useState(false);
useMelonyPart((part) => {
if (part.type === "text" && part.text) {
setTypingIndicator(true);
setTimeout(() => setTypingIndicator(false), 1000);
}
});
const handleSend = useCallback(async (message: string) => {
if (!message.trim()) return;
try {
await send(message);
} catch (error) {
console.error("Failed to send message:", error);
}
}, [send]);
const messageStats = useMemo(() => {
const userCount = messages.filter(m => m.role === "user").length;
const assistantCount = messages.filter(m => m.role === "assistant").length;
const totalWords = messages.reduce((count, message) =>
count + message.parts.reduce((partCount, part) =>
partCount + (part.text?.split(' ').length || 0), 0
), 0
);
return { userCount, assistantCount, totalWords };
}, [messages]);
return (
<div className="advanced-chat">
<div className="chat-header">
<h2>Advanced Chat</h2>
<div className="stats">
Messages: {messages.length} |
Words: {messageStats.totalWords} |
Status: {status}
</div>
</div>
<div className="chat-controls">
<input
type="text"
placeholder="Search messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={showOnlyUserMessages}
onChange={(e) => setShowOnlyUserMessages(e.target.checked)}
/>
Show only user messages
</label>
</div>
<div className="messages-container">
{messages.map((message) => (
<div key={message.id} className={`message ${message.role}`}>
<div className="message-header">
<span className="role">{message.role}</span>
<span className="timestamp">
{message.createdAt.toLocaleTimeString()}
</span>
</div>
<div className="message-content">
{message.parts.map((part, i) => (
<div key={i} className="part">
{part.type === "text" && (
<p>{part.text}</p>
)}
</div>
))}
</div>
</div>
))}
{typingIndicator && (
<div className="typing-indicator">
Assistant is typing...
</div>
)}
</div>
<div className="input-area">
<input
type="text"
placeholder="Type your message..."
onKeyPress={(e) => {
if (e.key === "Enter") {
handleSend(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
disabled={status === "streaming"}
/>
<button
onClick={() => {
const input = document.querySelector('input[placeholder="Type your message..."]');
if (input) {
handleSend(input.value);
input.value = "";
}
}}
disabled={status === "streaming"}
>
{status === "streaming" ? "Sending..." : "Send"}
</button>
</div>
</div>
);
}
export default function AdvancedChat() {
return (
<MelonyProvider endpoint="/api/chat">
<AdvancedChatInterface />
</MelonyProvider>
);
}