Actions & Events

Handle user interactions in AI-generated components with Melony's built-in action system.

Overview

Melony provides a simple action system that lets you handle button clicks, form submissions, and other user interactions from AI-generated components. Actions are passed as JSON strings and dispatched through the onAction prop.

Setting Up Action Handler

Define an action handler function and pass it to MelonyProvider:

import { MelonyProvider, type Action } from "melony";

function Chat() {
  const handleAction = (action: Action) => {
    console.log("Action received:", action);

    // Handle different action types
    switch (action.type) {
      case "refresh-weather":
        handleWeatherRefresh(action.payload);
        break;
      case "submit-form":
        handleFormSubmit(action.payload);
        break;
      default:
        console.log("Unknown action type:", action.type);
    }
  };

  return (
    <MelonyProvider onAction={handleAction}>
      {/* Your components */}
    </MelonyProvider>
  );
}

Button Actions

The AI can attach actions to buttons using the action prop:

<button 
  label="Refresh Weather" 
  variant="primary"
  action='{"type":"refresh-weather","location":"SF"}' 
/>

<button 
  label="Delete Item" 
  variant="destructive"
  action='{"type":"delete-item","id":"123"}' 
/>

When clicked, these buttons will trigger your onAction handler with the specified payload.

Action Type

Actions follow this TypeScript interface:

interface Action {
  type: string;           // Action identifier
  payload?: any;          // Optional action data
}

Real-World Example

Here's a complete example with multiple action types:

"use client";
import { MelonyProvider, MelonyMarkdown } from "melony";
import { useChat } from "ai/react";
import { useState } from "react";

export default function Chat() {
  const { messages, append } = useChat({ api: "/api/chat" });
  const [loading, setLoading] = useState(false);

  const handleAction = async (action) => {
    switch (action.type) {
      case "refresh-weather":
        setLoading(true);
        await append({
          role: "user",
          content: `Get weather for ${action.payload.location}`,
        });
        setLoading(false);
        break;

      case "send-email":
        // Handle email sending
        console.log("Sending email to:", action.payload.to);
        break;

      case "navigate":
        // Handle navigation
        window.location.href = action.payload.url;
        break;

      default:
        console.log("Unknown action:", action);
    }
  };

  return (
    <MelonyProvider onAction={handleAction}>
      <div className="space-y-4">
        {messages.map((m) => (
          <MelonyMarkdown key={m.id}>{m.content}</MelonyMarkdown>
        ))}
        {loading && <p>Loading...</p>}
      </div>
    </MelonyProvider>
  );
}

Best Practices

  • Type Safety: Use TypeScript unions for action types to ensure type safety
  • Validation: Always validate action payloads before processing
  • Error Handling: Wrap action handlers in try-catch blocks
  • Loading States: Show loading indicators during async operations
  • Feedback: Provide user feedback after action completion

TypeScript Example

Define action types for better type safety:

type WeatherAction = {
  type: "refresh-weather";
  payload: { location: string };
};

type EmailAction = {
  type: "send-email";
  payload: { to: string; subject: string };
};

type AppAction = WeatherAction | EmailAction;

const handleAction = (action: AppAction) => {
  switch (action.type) {
    case "refresh-weather":
      // TypeScript knows payload has { location: string }
      console.log(action.payload.location);
      break;
    case "send-email":
      // TypeScript knows payload has { to: string; subject: string }
      console.log(action.payload.to);
      break;
  }
};