Melony
HomeGitHub

Custom Message Types

Extend melony with your own message structures and types using TypeScript generics.

Overview

melony supports custom message structures through TypeScript generics and mappers. This allows you to define your own part types and message formats while maintaining full type safety.

Defining Custom Types

Start by defining your custom part type that extends the base melony structure:

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

Required Fields

melonyId - Unique identifier for the part
type - The type of content (text, image, tool_call, etc.)
role - Who sent the message (user, assistant, system)

Using Custom Types

Use your custom types with the MelonyProvider and hooks:

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

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

function ChatWithCustomTypes() {
  const messages = useMelonyMessages<CustomPart>();
  const send = useMelonySend();
  const status = useMelonyStatus();

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          <strong>{message.role}:</strong>
          {message.parts.map((part, i) => (
            <div key={i}>
              {part.type === "text" && <p>{part.text}</p>}
              {part.type === "image" && (
                <img src={part.imageUrl} alt="User uploaded image" />
              )}
              {part.type === "tool_call" && (
                <div>
                  <strong>Tool:</strong> {part.toolName}
                  <pre>{JSON.stringify(part.toolArgs, null, 2)}</pre>
                </div>
              )}
            </div>
          ))}
        </div>
      ))}
      <button onClick={() => send("Hello!")} disabled={status === "streaming"}>
        {status === "streaming" ? "Sending..." : "Send"}
      </button>
    </div>
  );
}

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

Advanced Custom Types

You can create more complex custom types with additional metadata and validation:

// Advanced custom part with metadata
type AdvancedPart = {
  melonyId: string;
  type: "text" | "image" | "video" | "audio" | "file" | "code_block" | "tool_call";
  role: "user" | "assistant" | "system" | "function";
  
  // Text content
  text?: string;
  language?: string; // For code blocks
  
  // Media content
  imageUrl?: string;
  videoUrl?: string;
  audioUrl?: string;
  fileUrl?: string;
  fileName?: string;
  fileSize?: number;
  
  // Tool calls
  toolName?: string;
  toolArgs?: Record<string, any>;
  toolResult?: any;
  
  // Metadata
  timestamp?: number;
  metadata?: Record<string, any>;
  
  // UI hints
  isStreaming?: boolean;
  isError?: boolean;
  errorMessage?: string;
};

Type Mappers

You can create custom mappers to transform server responses into your custom types:

// Custom mapper function
function mapServerResponseToCustomPart(serverData: any): CustomPart {
  return {
    melonyId: serverData.id || crypto.randomUUID(),
    type: serverData.type,
    role: serverData.role,
    text: serverData.content,
    imageUrl: serverData.image_url,
    toolName: serverData.function_name,
    toolArgs: serverData.function_arguments,
  };
}

// Use with custom mapper
const messages = useMelonyMessages<CustomPart>({
  mapper: mapServerResponseToCustomPart,
});

Type Validation

Add runtime validation to ensure your custom types are properly structured:

// Validation function
function validateCustomPart(part: any): part is CustomPart {
  return (
    typeof part === 'object' &&
    typeof part.melonyId === 'string' &&
    typeof part.type === 'string' &&
    typeof part.role === 'string' &&
    ['text', 'image', 'tool_call'].includes(part.type) &&
    ['user', 'assistant', 'system'].includes(part.role)
  );
}

// Use with validation
const messages = useMelonyMessages<CustomPart>({
  validator: validateCustomPart,
});

Real-World Example

Here's a complete example of a chat interface with custom message types:

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

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

function RichChatInterface() {
  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>
        );
      
      case "error":
        return (
          <div className="error-message text-red-500">
            ❌ {part.errorMessage}
          </div>
        );
      
      default:
        return null;
    }
  };

  return (
    <div className="chat-container">
      {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 RichChat() {
  return (
    <MelonyProvider<RichPart> endpoint="/api/chat">
      <RichChatInterface />
    </MelonyProvider>
  );
}

Best Practices

Keep types focused

Define types that match your specific use case rather than trying to support every possible message type.

Use TypeScript strictly

Enable strict TypeScript settings to catch type errors at compile time.

Add runtime validation

Include validation functions to catch runtime errors and provide better error messages.

Document your types

Add JSDoc comments to explain the purpose and structure of your custom types.