Melony
HomeGitHub

Examples

Real-world examples showing how to use melony in different scenarios.

Basic Chat Interface

A simple chat interface with streaming support using melony hooks.

"use client";
import {
  MelonyProvider,
  useMelonyMessages,
  useMelonySend,
  useMelonyStatus,
} from "melony";

function ChatMessages() {
  const messages = useMelonyMessages();
  const send = useMelonySend();
  const status = useMelonyStatus();

  return (
    <div className="chat-container">
      <div className="messages">
        {messages.map((message) => (
          <div key={message.id} className={`message ${message.role}`}>
            <div className="message-header">
              <strong>{message.role}</strong>
            </div>
            <div className="message-content">
              {message.parts.map((part, i) => (
                <div key={i}>
                  {part.type === "text" && <p>{part.text}</p>}
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
      
      <div className="input-area">
        <button 
          onClick={() => send("Hello!")} 
          disabled={status === "streaming"}
        >
          {status === "streaming" ? "Sending..." : "Send"}
        </button>
        {status === "error" && <p>Error occurred. Please try again.</p>}
      </div>
    </div>
  );
}

export default function BasicChat() {
  return (
    <MelonyProvider endpoint="/api/chat">
      <ChatMessages />
    </MelonyProvider>
  );
}

Custom Message Types

Chat interface with custom message types for rich content.

"use client";
import { MelonyProvider, useMelonyMessages, useMelonySend } from "melony";

// Define custom part types
type RichPart = {
  melonyId: string;
  type: "text" | "image" | "code" | "tool_call";
  role: "user" | "assistant" | "system";
  text?: string;
  imageUrl?: string;
  code?: string;
  language?: string;
  toolName?: string;
  toolArgs?: Record<string, any>;
};

function RichChat() {
  const messages = useMelonyMessages<RichPart>();
  const send = useMelonySend();

  const renderPart = (part: RichPart) => {
    switch (part.type) {
      case "text":
        return <p className="text-content">{part.text}</p>;
      
      case "image":
        return (
          <div className="image-container">
            <img src={part.imageUrl} alt="Chat image" className="max-w-full rounded" />
          </div>
        );
      
      case "code":
        return (
          <pre className="code-block">
            <code className={`language-${part.language}`}>
              {part.code}
            </code>
          </pre>
        );
      
      case "tool_call":
        return (
          <div className="tool-call">
            <strong>🔧 {part.toolName}</strong>
            <pre>{JSON.stringify(part.toolArgs, null, 2)}</pre>
          </div>
        );
      
      default:
        return null;
    }
  };

  return (
    <div className="rich-chat">
      {messages.map((message) => (
        <div key={message.id} className={`message ${message.role}`}>
          <div className="message-header">
            <strong>{message.role}</strong>
          </div>
          <div className="message-content">
            {message.parts.map((part, i) => (
              <div key={i} className="part">
                {renderPart(part)}
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

export default function CustomTypesChat() {
  return (
    <MelonyProvider<RichPart> endpoint="/api/chat">
      <RichChat />
    </MelonyProvider>
  );
}

Advanced Features

Chat interface with advanced features like filtering, search, and real-time updates.

"use client";
import { useState, useMemo, useCallback } from "react";
import {
  MelonyProvider,
  useMelonyMessages,
  useMelonySend,
  useMelonyStatus,
  useMelonyPart,
} from "melony";

function AdvancedChat() {
  const [searchTerm, setSearchTerm] = useState("");
  const [showOnlyUserMessages, setShowOnlyUserMessages] = useState(false);
  const [typingIndicator, setTypingIndicator] = 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();
  
  // Listen to parts for typing indicator
  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 AdvancedChatExample() {
  return (
    <MelonyProvider endpoint="/api/chat">
      <AdvancedChat />
    </MelonyProvider>
  );
}

Server Integration

Complete example with server-side integration using AI SDK.

Client Component

"use client";
import { MelonyProvider, useMelonyMessages, useMelonySend } from "melony";

function ChatInterface() {
  const messages = useMelonyMessages();
  const send = useMelonySend();

  return (
    <div className="chat">
      <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" && <p>{part.text}</p>}
              </div>
            ))}
          </div>
        ))}
      </div>
      
      <div className="input">
        <button onClick={() => send("Hello!")}>
          Send Message
        </button>
      </div>
    </div>
  );
}

export default function Chat() {
  return (
    <MelonyProvider endpoint="/api/chat">
      <ChatInterface />
    </MelonyProvider>
  );
}

Server API Route

// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";

export async function POST(req: Request) {
  const { message } = await req.json();

  const result = await streamText({
    model: openai("gpt-4o"),
    messages: [
      {
        role: "user",
        content: message,
      },
    ],
  });

  return result.toUIMessageStream();
}

Error Handling

Robust error handling with retry functionality and user feedback.

"use client";
import { useState, useCallback } from "react";
import { MelonyProvider, useMelonyMessages, useMelonySend, useMelonyStatus } from "melony";

function ErrorHandlingChat() {
  const messages = useMelonyMessages();
  const send = useMelonySend();
  const status = useMelonyStatus();
  const [error, setError] = useState<string | null>(null);

  const handleSend = useCallback(async (message: string) => {
    try {
      setError(null);
      await send(message);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to send message");
    }
  }, [send]);

  const retryLastMessage = useCallback(() => {
    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);
      }
    }
  }, [messages, handleSend]);

  return (
    <div className="error-handling-chat">
      {messages.map((message) => (
        <div key={message.id} className={`message ${message.role}`}>
          {message.parts.map((part, i) => (
            <div key={i}>
              {part.type === "text" && <p>{part.text}</p>}
              {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 className="input">
        <button 
          onClick={() => handleSend("Hello!")}
          disabled={status === "streaming"}
        >
          {status === "streaming" ? "Sending..." : "Send"}
        </button>
      </div>
    </div>
  );
}

export default function ErrorHandlingExample() {
  return (
    <MelonyProvider endpoint="/api/chat">
      <ErrorHandlingChat />
    </MelonyProvider>
  );
}

Custom Hooks

Building custom hooks that combine melony functionality for reusable logic.

"use client";
import { useMemo, useCallback } from "react";
import { useMelonyMessages, useMelonySend, useMelonyStatus } from "melony";

// Custom hook for chat statistics
function useChatStats() {
  const messages = useMelonyMessages();
  
  return 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]);
}

// Custom hook for message search
function useMessageSearch(query: string) {
  const messages = useMelonyMessages();
  
  return useMemo(() => {
    if (!query) return messages;
    
    return messages.filter(message =>
      message.parts.some(part =>
        part.text?.toLowerCase().includes(query.toLowerCase())
      )
    );
  }, [messages, query]);
}

// Custom hook for chat controls
function useChatControls() {
  const send = useMelonySend();
  const status = useMelonyStatus();
  
  const sendWithRetry = useCallback(async (message: string, maxRetries = 3) => {
    let attempts = 0;
    
    while (attempts < maxRetries) {
      try {
        await send(message);
        break;
      } catch (error) {
        attempts++;
        if (attempts >= maxRetries) throw error;
        await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
      }
    }
  }, [send]);
  
  return {
    send,
    sendWithRetry,
    status,
    isIdle: status === "idle",
    isStreaming: status === "streaming",
    hasError: status === "error",
  };
}

// Usage in component
function MyChat() {
  const stats = useChatStats();
  const searchResults = useMessageSearch("hello");
  const { send, isStreaming, hasError } = useChatControls();
  
  return (
    <div>
      <div>Stats: {stats.userCount} user, {stats.assistantCount} assistant</div>
      <div>Search results: {searchResults.length}</div>
      <button onClick={() => send("Hello")} disabled={isStreaming}>
        Send
      </button>
      {hasError && <div>Error occurred</div>}
    </div>
  );
}

export default function CustomHooksExample() {
  return (
    <MelonyProvider endpoint="/api/chat">
      <MyChat />
    </MelonyProvider>
  );
}