DEV Community

Cover image for Build an AI Summarizer Agent in TypeScript Using Mastra (Part 2): Connect to Frontend
Timmy Dahunsi
Timmy Dahunsi

Posted on

Build an AI Summarizer Agent in TypeScript Using Mastra (Part 2): Connect to Frontend

Series Navigation: Part 1: Building the Agent | Part 2: Connect to Frontend (You are here)

In Part 1, we built a fully functional AI summarizer agent that can fetch content from URLs and generate intelligent summaries.
In this follow-up, you’ll learn how to connect that agent to a Next.js frontend using the Mastra Client, bringing your summarizer to life with a simple web interface.
Here’s what the final result looks like Ai-summarizer-agent.com:

AI Summarizer

Prerequisites

  • Completed Part 1 of this series
  • Basic knowledge of React and Next.js
  • The AI Summarizer Agent from Part 1 (code available on GitHub)

Setting Up Next.js with Mastra

Let's start by setting up a Next.js project that integrates with our Mastra agent.

Step 1: Create a Next.js Project

If you're starting fresh, create a new Next.js app:

npx create-next-app@latest ai-summarizer-app
cd ai-summarizer-app
Enter fullscreen mode Exit fullscreen mode

When prompted, select:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: Yes
  • App Router: Yes
  • Import alias: No (or default)

Step 2: Install Mastra Client

The Mastra Client allows your frontend to communicate with your agents:

npm install @mastra/core
Enter fullscreen mode Exit fullscreen mode

Step 3: Copy Your Agent Code

Copy your agent code from Part 1:

  1. Copy the entire src/mastra folder from Part 1
  2. Place it in your new Next.js project at src/mastra
  3. Copy your .env file with the Gemini API key

Your project structure should now look like:

ai-summarizer-app/
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   └── summarize/
│   │   │       └── route.ts
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components/
│   │   └── SummarizerUI.tsx
│   ├── mastra/
│   │   ├── agents/
│   │   │   └── summarizer.ts
│   │   ├── tools/
│   │   │   └── web-scraper.ts
│   │   └── index.ts
├── .env
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Creating the API Route

Next.js API routes will act as the bridge between your frontend and the Mastra agent.

Step 1: Create the Summarize API Route

Create a new file at src/app/api/summarize/route.ts:

import { summarizerAgent } from '@/src/mastra/agents/summarizer';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  try {
    const { input } = await request.json();

    if (!input || typeof input !== 'string') {
      return NextResponse.json(
        { error: 'Input is required', success: false },
        { status: 400 }
      );
    }

    const result = await summarizerAgent.generate(input, {
      maxSteps: 5,
    });

    return NextResponse.json({
      text: result.text,
      success: true,
    });
  } catch (error) {
    return NextResponse.json(
      {
        error: error instanceof Error ? error.message : 'Failed',
        success: false,
      },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  • We accept a POST request with an input field
  • We validate that input exists and is a string.
  • We call the summarizerAgent with the input and an optional parameter called maxSteps (maxSteps defines how many “reasoning steps” the agent can take while generating the summary)
  • We return the summarized text as a JSON response for real-time updates.
  • We handle errors gracefully

Part 3: Building the Frontend UI

Now let's create an interactive UI for our summarizer.

Step 1: Create the Summarizer Component

Create a new file at src/components/SummarizerUI.tsx:

'use client';

import { useState } from 'react';

type InputMode = 'url' | 'text';

export default function SummarizerUI() {
  const [input, setInput] = useState('');
  const [summary, setSummary] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [mode, setMode] = useState<InputMode>('url');

  const handleSummarize = async () => {
    if (!input.trim()) {
      setError('Please enter a URL or text to summarize');
      return;
    }

    setLoading(true);
    setError('');
    setSummary('');

    try {
      const response = await fetch('/api/summarize', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ input }),
      });

      if (!response.ok) {
        throw new Error('Failed to generate summary');
      }

      const data = await response.json();

      if (data.success) {
        setSummary(data.text);
      } else {
        setError(data.error || 'Failed to generate summary');
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Something went wrong');
    } finally {
      setLoading(false);
    }
  };

  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey && mode === 'url') {
      e.preventDefault();
      handleSummarize();
    }
  };

  return (
    <div className="max-w-4xl mx-auto p-6 space-y-6">
      {/* Header */}
      <div className="text-center space-y-2">
        <h1 className="text-4xl font-bold text-gray-900">AI Summarizer Agent</h1>
      </div>

      {/* Mode Selector */}
      <div className="flex gap-2 justify-center">
        <button
          onClick={() => setMode('url')}
          className={`px-4 py-2 rounded-lg font-medium transition-colors ${
            mode === 'url'
              ? 'bg-blue-600 text-white'
              : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
          }`}
        >
          URL
        </button>
        <button
          onClick={() => setMode('text')}
          className={`px-4 py-2 rounded-lg font-medium transition-colors ${
            mode === 'text'
              ? 'bg-blue-600 text-white'
              : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
          }`}
        >
          Text
        </button>
      </div>

      {/* Input Area */}
      <div className="space-y-2">
        <label className="block text-sm font-medium text-gray-700">
          {mode === 'url' ? 'Enter URL' : 'Enter Text'}
        </label>
        {mode === 'url' ? (
          <input
            type="text"
            value={input}
            onChange={(e) => setInput(e.target.value)}
            onKeyPress={handleKeyPress}
            placeholder="https://example.com/article"
            className="w-full px-4 py-3 border text-black border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            disabled={loading}
          />
        ) : (
          <textarea
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="Paste your text here..."
            rows={6}
            className="w-full px-4 py-3 border text-black border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
            disabled={loading}
          />
        )}
      </div>

      {/* Submit Button */}
      <button
        onClick={handleSummarize}
        disabled={loading || !input.trim()}
        className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
      >
        {loading ? (
          <span className="flex items-center justify-center gap-2">
            <svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
              <circle
                className="opacity-25"
                cx="12"
                cy="12"
                r="10"
                stroke="currentColor"
                strokeWidth="4"
                fill="none"
              />
              <path
                className="opacity-75"
                fill="currentColor"
                d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
              />
            </svg>
            Generating Summary...
          </span>
        ) : (
          'Summarize'
        )}
      </button>

      {/* Error Message */}
      {error && (
        <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
          <p className="text-red-800 text-sm">
            <span className="font-medium">Error:</span> {error}
          </p>
        </div>
      )}

      {/* Summary Output */}
      {summary && (
        <div className="space-y-3">
          <h2 className="text-xl font-semibold text-gray-900">Summary</h2>
          <div className="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 border border-blue-200 rounded-lg">
            <div
              className="prose prose-sm max-w-none text-gray-800"
              dangerouslySetInnerHTML={{
                __html: summary.replace(
                  /\*\*(.*?)\*\*/g,
                  '<strong>$1</strong>'
                ),
              }}
            />
          </div>
        </div>
      )}

      {summary && (
        <button
          onClick={() => {
            setSummary('');
            setInput('');
            setError('');
          }}
          className="text-sm text-gray-600 hover:text-gray-900 underline"
        >
          Clear and start over
        </button>
      )}

    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

What's happening here:

  • We create a React component with state management for input, summary, loading, error, and mode (which toggles between 'url' and 'text' input types)
  • We build a handleSummarize function that sends a POST request to /api/summarize with the user's input and updates the UI based on the response
  • We render a clean interface with mode selector buttons, dynamic input fields (text input for URLs, textarea for text), and a submit button with loading states
  • We display the generated summary in a styled container with basic Markdown formatting (converting bold syntax to HTML)
  • We handle errors gracefully by showing error messages in a red alert box and include a "Clear and start over" button to reset the interface

AI Summarizer agent

Step 2: Update the Home Page

Update src/app/page.tsx to use our new component:

import SummarizerUI from '@/components/SummarizerUI';

export default function Home() {
  return (
    <main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 py-12">
      <SummarizerUI />
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Update the Layout (Optional)

Update src/app/layout.tsx to add metadata:

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: 'AI Summarizer | Powered by Mastra',
  description: 'Intelligent content summarization using AI agents',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

Enter fullscreen mode Exit fullscreen mode

Testing Your Application

Let's test everything works correctly.

Run the Development Server

npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 in your browser.

Test URL Summarization

  1. Click the "URL" button
  2. Enter: https://blog.google/technology/ai/google-gemini-ai/
  3. Click "Summarize"
  4. Watch the agent fetch and summarize the content

URL Summarization

Test Text Summarization

  1. Click the " Text" button
  2. Paste a long article or text
  3. Click " Summarize"
  4. See the immediate summary generation

Text Summarization

Test Error Handling

  1. Try an invalid URL
  2. Try empty input
  3. Verify error messages display correctly

Error Handling

Conclusion

You've successfully connected your Mastra AI agent to a beautiful Next.js frontend. You now have a production-ready application that:

What You've Learned

  • Mastra Client Integration: How to connect frontend to an agents
  • Next.js API Routes: Building backend endpoints for AI agents
  • Modern UI/UX: Creating intuitive interfaces for AI applications

Complete Code Repository

The full working code for both Part 1 and Part 2 is available on GitHub:

Repository: github.com/yourusername/ai-summarizer-mastra


Additional Resources

Documentation

Happy building!

If you found this tutorial helpful, consider sharing it with other developers learning AI agent development!

Top comments (0)