UI Dojo

repository·main·Indexed 41 days ago

https://github.com/mastra-ai/ui-dojo

A showcase repository demonstrating the integration of Mastra with AI UI frameworks including Vercel AI SDK, Assistant UI, and CopilotKit. It provides reference implementations for generative UIs, multi-step workflows with Human in the Loop, agent networks, and custom AI components like PromptInput and BackgroundTaskCard.

Tokens
16.3K
Snippets
49
Records
55
Agent score
49%

What's inside UI Dojo

  1. Advanced AI UI patterns in UI Dojo

    main

    The project demonstrates several advanced patterns for building agentic interfaces:

    • Generative UIs: Building custom UI components that render in response to tool calls. See src/pages/ai-sdk/generative-user-interfaces.tsx.
    • Workflows: Implementing multi-step AI workflows with streaming and suspend/resume capabilities (Human in the Loop). See src/pages/ai-sdk/workflow.tsx and src/pages/ai-sdk/workflow-suspend-resume.tsx.
    • Agent Networks: Coordinating multiple AI agents (e.g., via a routing agent). See src/pages/ai-sdk/network.tsx.
    • Custom Events: Handling custom events within Generative UIs, Agent Networks, and Sub-agents/Workflows. See files in src/pages/ai-sdk/ with -custom-events suffix.
    • Client Tools: Using Mastra's Client SDK with different frameworks. See src/pages/client-tools/.
  2. Explore Mastra UI framework integrations

    main

    UI Dojo provides side-by-side implementations of Mastra integrated with three major AI UI frameworks. You can find these examples in the src/pages/ directory:

    • AI SDK: Uses Vercel's AI SDK and @mastra/ai-sdk. Located at src/pages/ai-sdk/index.tsx.
    • Assistant UI: Uses Assistant UI's Thread components and useExternalStoreRuntime() to connect Assistant UI to Mastra's memory. Located at src/pages/assistant-ui/index.tsx.
    • CopilotKit: Uses CopilotKit's Chat component. Located at src/pages/copilot-kit/index.tsx.

    These examples are useful for comparing how different frameworks handle chat interfaces and agentic interactions.

  3. Install and set up UI Dojo

    main

    To run the UI Dojo showcase locally, follow these steps to clone the repository, install dependencies, and configure your environment.

    Prerequisites

    • Node.js 20 or later
    • OpenAI API key

    Installation Steps

    1. Clone and install dependencies:
      git clone git@github.com:mastra-ai/ui-dojo.git
      cd ui-dojo
      pnpm install
    2. Set up environment variables: Create a .env file from the example and add your OpenAI API key:
      cp .env.example .env
      # Edit .env and add your API key
    3. Start the development server:
      pnpm run dev
      This command runs both the Mastra server and the Vite dev server concurrently.
    git clone git@github.com:mastra-ai/ui-dojo.git
    cd ui-dojo
    pnpm install
    cp .env.example .env
    pnpm run dev
  4. Manage PromptInput state with PromptInputProvider

    main

    By default, PromptInput is self-managed. If you need to lift the text input and attachment state outside of the component (e.g., to access it from other parts of your UI), wrap your application or a parent component in PromptInputProvider.

    This provider manages:

    • textInput: The current string value and methods to set or clear it.
    • attachments: The list of files, methods to add, remove, or clear them, and a reference to the hidden file input.

    When using the provider, PromptInput will automatically sync with this global state instead of using its own local state.

    import { PromptInputProvider } from "@/components/ai-elements/prompt-input";
    
    function App() {
      return (
        <PromptInputProvider initialInput="Hello!">
          {/* Your PromptInput and other components will now share this state */}
          <YourChatInterface />
        </PromptInputProvider>
      );
    }
  5. Render AI SDK message parts (Text, Reasoning, Sources)

    main

    When iterating over messages from useChat, you can inspect the parts array of each message to render specific UI elements based on the part.type. Supported types in this implementation include:

    • text: Standard text response. Use the <Response> component for formatting.
    • reasoning: Internal model thought process. Use <Reasoning> components to display this.
    • source-url: URLs provided by the model as citations. Use <Sources> and <Source> to render these.

    You can use getVisibleReasoningText(message.parts) to extract reasoning text for display.

    {message.parts.map((part, i) => {
      switch (part.type) {
        case "text":
          return (
            <Message from={message.role}>
              <MessageContent>
                <Response>{part.text}</Response>
              </MessageContent>
            </Message>
          );
        case "reasoning":
          // Handle reasoning separately or via getVisibleReasoningText
          return null;
        case "source-url":
          // Render using <Sources> component
          return null;
        default:
          return null;
      }
    })} 
  6. Implement Shared State with Working Memory

    main

    You can implement a "Shared State" pattern where an agent manages a structured piece of data (the "working memory") that is synchronized with a UI. This is achieved by configuring workingMemory within the Memory instance.

    Key configuration options:

    • enabled: Boolean to activate working memory.
    • scope: The scope of the memory (e.g., "thread").
    • schema: A Zod object defining the structure of the state.

    When the agent uses tools to update this memory, the changes can be streamed to the frontend as STATE_DELTA events, allowing the UI to reflect the agent's internal state changes in real-time.

    export const ckSharedStateAgent = new Agent({
      id: "ck_shared_state",
      // ... instructions ...
      memory: new Memory({
        storage: getStorage(),
        options: {
          workingMemory: {
            enabled: true,
            scope: "thread",
            schema: z.object({
              recipe: z.object({
                skill_level: z.enum(["Beginner", "Intermediate", "Advanced"]),
                special_preferences: z.array(z.enum(["High Protein", "Low Carb", "Spicy", "Budget-Friendly", "One-Pot Meal", "Vegetarian", "Vegan"])),
                cooking_time: z.enum(["5 min", "15 min", "30 min", "45 min", "60+ min"]),
                ingredients: z.array(z.object({
                  icon: z.string(),
                  name: z.string(),
                  amount: z.string(),
                })),
                instructions: z.array(z.string()),
              }),
            }),
          },
        },
      }),
    });
  7. Implement Human-in-the-loop (HITL) via Tool Interrupts

    main
    You can implement native suspend/resume patterns (Human-in-the-loop) by using tools that are designed to pause execution. When an agent calls a tool like schedule_meeting, the execution can be suspended to show a UI element (like a time picker). Once the user interacts with the UI, the tool resumes, allowing the agent to continue the conversation based on the user's input.
  8. Integrate CopilotKit with Mastra

    main

    To use CopilotKit within a Mastra-powered application, wrap your chat components with the CopilotKit provider. You must provide a runtimeUrl pointing to your Mastra backend endpoint (e.g., ${MASTRA_BASE_URL}/copilotkit) and specify an agent ID to identify the agent being used.

    Note: Ensure you import @copilotkit/react-core/v2/styles.css to include the necessary styles for the CopilotKit components.

    import "@copilotkit/react-core/v2/styles.css";
    import { CopilotKit } from "@copilotkit/react-core";
    import { MASTRA_BASE_URL } from "@/constants";
    
    const AGENT_ID = "ck_agentic_chat";
    
    function CopilotKitDemo() {
      return (
        <CopilotKit
          runtimeUrl={`${MASTRA_BASE_URL}/copilotkit`}
          agent={AGENT_ID}
        >
          <Chat />
        </CopilotKit>
      );
    }
  9. Integrate Assistant UI with Mastra using useExternalStoreRuntime

    main

    To use @assistant-ui/react with Mastra's chat capabilities, you can implement a custom runtime using useExternalStoreRuntime. This allows you to bridge Mastra's useChat hook with the Assistant UI component tree.

    Key steps in the integration pattern:

    1. Use Mastra's useChat hook to manage messages, sending, and running state.
    2. Map Mastra messages to Assistant UI compatible formats using toAssistantUIMessage from @mastra/react.
    3. Implement onNew to call Mastra's sendMessage with mode: "stream".
    4. Implement onCancel to trigger an AbortController and Mastra's cancelRun.
    5. Wrap your chat components in AssistantRuntimeProvider using the runtime created by useExternalStoreRuntime.
    import { 
      useChat, 
      toAssistantUIMessage 
    } from "@mastra/react";
    import { 
      AssistantRuntimeProvider, 
      useExternalStoreRuntime, 
      type AppendMessage 
    } from "@assistant-ui/react";
    
    // Inside your component:
    const { messages, sendMessage, cancelRun, isRunning: isRunningStream, setMessages } = useChat({
      agentId,
      initialMessages: initialMessages || [],
    });
    
    const onNew = async (message: AppendMessage) => {
      const input = message.content[0].text;
      await sendMessage({
        message: input,
        mode: "stream",
        threadId,
      });
    };
    
    const runtime = useExternalStoreRuntime({
      isRunning: isRunningStream,
      messages: messages.map(toAssistantUIMessage),
      convertMessage: (x) => x,
      onNew,
      onCancel: async () => { /* implement abort logic */ cancelRun?.(); },
    });
    
    return (
      <AssistantRuntimeProvider runtime={runtime}>
        {children}
      </AssistantRuntimeProvider>
    );
  10. Integrate Vercel AI SDK with Mastra via useChat

    main

    To build a chat interface using the Vercel AI SDK (@ai-sdk/react) and Mastra, use the useChat hook. You must provide a transport configured with a DefaultChatTransport pointing to your Mastra agent's chat route.

    When calling sendMessage, you can pass an optional body object to include additional parameters (like model or webSearch) that your Mastra agent expects.

    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport } from "ai";
    
    const { messages, sendMessage, status, regenerate } = useChat({
      transport: new DefaultChatTransport({
        // The endpoint defined in your Mastra configuration
        api: `${MASTRA_BASE_URL}/chat/yourAgentName`,
      }),
    });
    
    // Sending a message with custom body parameters
    const handleSubmit = (text: string) => {
      sendMessage(
        { text },
        {
          body: {
            model: "openai/gpt-4o",
            webSearch: true,
          },
        }
      );
    };
  11. Troubleshoot common UI Dojo issues

    main

    OPENAI_API_KEY is not set

    • Ensure you created a .env file from .env.example.
    • Verify the API key is valid and correctly formatted.
    • Restart the dev server after updating .env.

    Port already in use

    • Check if another Mastra or Vite process is running.
    • Kill the existing process or modify the port in vite.config.ts.

    Agent not responding

    • Check the browser console and terminal for errors.
    • Verify your OpenAI API key has sufficient credits.
    • Ensure the Mastra server is running (default: http://localhost:4750).
  12. Configure Bundler Externals

    main

    When building the Mastra server, you can use the bundler.externals option to prevent specific runtime packages from being bundled. This is useful for packages that should be loaded by the environment at runtime, such as @copilotkit/runtime or various @ag-ui packages.

    bundler: {
      externals: [
        "@copilotkit/runtime",
        "@copilotkit/runtime/v2",
        "@ag-ui/mastra",
        "@ag-ui/mastra/copilotkit",
        "@ag-ui/mastra/a2ui",
        "@ag-ui/client",
        "@ag-ui/core",
        "@ag-ui/mcp-apps-middleware",
      ],
    },