Current Section

Overview

0%

← Back to Real Projects
Real Projects · Project 7

Build an AI Meeting Assistant

Build an enterprise productivity tool that turns messy meeting transcripts and audio notes into structured executive summaries, binding decisions, assigned action items, and ready-to-send follow-up emails.

Next.js App RouterTranscript ParsingAction Item ExtractionProduction Recipe
meeting-assistant-preview.tsx
Live UI Preview

Raw Transcript Input

[00:02] Sarah: Let us review the Q3 RAG migration roadmap.
[00:15] Alex: We decided to use pgvector on AWS RDS.
[00:42] Sarah: Great. Alex, can you benchmark HNSW indexing by Friday?
[01:05] David: I will handle the IAM security role policies.
Analyze Transcript ⚡

Extracted Action Items

2 Tasks Found

Benchmark HNSW indexing

Owner: Alex • Due: Friday
Pending

Configure IAM security roles

Owner: David
Pending
AIMates Hands-On Lab

Want to Test Transcript Extraction Live?

Launch our pre-configured sandbox with ready-to-run transcript parser routes and task table UI components.

Launch Sandbox Lab →

The 30-Second Recipe

An AI meeting assistant transforms unstructured conversational transcripts into structured corporate execution.

Instead of forcing human managers to read 50 pages of meeting notes, your backend route accepts raw transcript text, passes it through a Zod-constrained LLM schema parser, and isolates executive decisions, action items with assigned owners, blockers, and ready-to-send follow-up emails into interactive UI cards.

Paste Transcript → POST /api/analyze-meeting → Zod Schema Extraction → Action Item Table & Email

Topology

End-to-End System Architecture

Here is how data flows from raw spoken transcript to structured team follow-up:

01

Transcript Ingestion

User pastes raw meeting notes or VTT/TXT speech-to-text transcript files into the client input box.

02

Payload Chunking

If meeting text exceeds context limits, text is token-chunked across logical dialogue boundaries.

03

Structured Extraction

Model runs under Zod JSON schema validation, extracting executive summaries, decisions, owners, and deadlines.

04

Follow-Up Synthesis

An automated secondary prompt synthesizes a clean, copy-ready email or Slack summary for stakeholders.

05

Action Item Workspace

Frontend renders interactive task tables with assigned owners and 1-click export options.

Step 1 · Backend Infrastructure

The Structured Transcript Analysis API

Create the backend route handler at app/api/analyze-meeting/route.ts. It uses Zod and OpenAI Structured Outputs to extract exact action items, owners, and decisions:

app/api/analyze-meeting/route.ts
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const MeetingAnalysisSchema = z.object({
  executiveSummary: z.string().describe("A 3-sentence executive summary of the meeting goals and outcome"),
  decisions: z.array(z.string()).describe("Key decisions ratified during the discussion"),
  actionItems: z.array(
    z.object({
      task: z.string().describe("The specific task or deliverable"),
      owner: z.string().describe("The person assigned to the task"),
      deadline: z.string().optional().describe("Explicit deadline if mentioned, otherwise 'Unspecified'"),
    })
  ),
  risksAndBlockers: z.array(z.string()).describe("Potential technical or schedule blockers identified"),
  followUpEmail: z.string().describe("A professional follow-up email draft ready to send to participants"),
});

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

    if (!transcript || transcript.trim().length < 10) {
      return new Response(JSON.stringify({ error: "Valid transcript text is required" }), { status: 400 });
    }

    const completion = await openai.beta.chat.completions.parse({
      model: "gpt-4o",
      messages: [
        {
          role: "system",
          content: "You are an expert executive chief of staff. Analyze meeting transcripts with meticulous accuracy, ensuring zero action items or decisions are missed.",
        },
        {
          role: "user",
          content: `Please analyze the following meeting transcript:\n\n${transcript}`,
        },
      ],
      response_format: zodResponseFormat(MeetingAnalysisSchema, "meeting_analysis"),
    });

    const result = completion.choices[0].message.parsed;
    return new Response(JSON.stringify(result), {
      headers: { "Content-Type": "application/json" },
    });
  } catch (error) {
    console.error("Meeting analysis error:", error);
    return new Response(JSON.stringify({ error: "Failed to analyze transcript" }), { status: 500 });
  }
}

Step 2 · Frontend Implementation

The Interactive Workspace UI

Create the client workspace component at components/tools/MeetingAnalyzer.tsx to display action item tables, decisions, and follow-up drafts:

components/tools/MeetingAnalyzer.tsx
"use client";

import { useState } from "react";

interface ActionItem {
  task: string;
  owner: string;
  deadline?: string;
}

interface AnalysisResult {
  executiveSummary: string;
  decisions: string[];
  actionItems: ActionItem[];
  risksAndBlockers: string[];
  followUpEmail: string;
}

export default function MeetingAnalyzer() {
  const [transcript, setTranscript] = useState("");
  const [loading, setLoading] = useState(false);
  const [analysis, setAnalysis] = useState<AnalysisResult | null>(null);

  const handleAnalyze = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!transcript.trim() || loading) return;

    setLoading(true);
    try {
      const res = await fetch("/api/analyze-meeting", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ transcript }),
      });
      const data = await res.json();
      setAnalysis(data);
    } catch (err) {
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="space-y-8">
      <form onSubmit={handleAnalyze} className="space-y-4 p-6 border border-[var(--app-border)] rounded-2xl bg-[var(--app-card)] shadow-sm">
        <h3 className="text-sm font-black text-[var(--app-text)]">Paste Meeting Transcript / Notes</h3>
        <textarea
          value={transcript}
          onChange={(e) => setTranscript(e.target.value)}
          placeholder="Paste VTT, Zoom, or manual meeting notes here..."
          className="w-full bg-[var(--app-bg)] border border-[var(--app-border)] rounded-xl p-4 text-xs text-[var(--app-text)] h-48 focus:ring-1 focus:ring-amber-500 font-mono"
        />
        <button
          type="submit"
          disabled={loading || !transcript.trim()}
          className="w-full bg-amber-500 hover:bg-amber-400 disabled:opacity-50 text-slate-950 font-bold py-3 rounded-xl text-xs transition shadow-sm"
        >
          {loading ? "Analyzing Transcript & Extracting Tasks..." : "Analyze Meeting Notes ⚡"}
        </button>
      </form>

      {analysis && (
        <div className="space-y-6">
          {/* Executive Summary Card */}
          <div className="p-6 border border-[var(--app-border)] rounded-2xl bg-[var(--app-card)] space-y-2 shadow-sm">
            <h4 className="text-xs font-bold text-amber-600 dark:text-amber-400 uppercase tracking-wider">Executive Summary</h4>
            <p className="text-xs leading-relaxed text-[var(--app-text)]">{analysis.executiveSummary}</p>
          </div>

          {/* Action Items Table Grid */}
          <div className="p-6 border border-[var(--app-border)] rounded-2xl bg-[var(--app-card)] space-y-4 shadow-sm">
            <h4 className="text-xs font-bold text-emerald-600 dark:text-emerald-400 uppercase tracking-wider">Action Items &amp; Owners</h4>
            <div className="space-y-2">
              {analysis.actionItems.map((item, idx) => (
                <div key={idx} className="p-3 rounded-xl bg-[var(--app-chip)]/40 border border-[var(--app-border)] flex flex-col sm:flex-row sm:items-center justify-between gap-2 text-xs">
                  <div>
                    <span className="font-bold text-[var(--app-text)]">{item.task}</span>
                  </div>
                  <div className="flex items-center gap-3 shrink-0">
                    <span className="bg-amber-500/10 text-amber-700 dark:text-amber-300 px-2 py-0.5 rounded font-mono text-[11px]">
                      {item.owner}
                    </span>
                    <span className="text-[var(--app-muted)] text-[11px]">
                      Due: {item.deadline || "TBD"}
                    </span>
                  </div>
                </div>
              ))}
            </div>
          </div>

          {/* Follow-up Email Draft */}
          <div className="p-6 border border-[var(--app-border)] rounded-2xl bg-[var(--app-card)] space-y-3 shadow-sm">
            <div className="flex justify-between items-center">
              <h4 className="text-xs font-bold text-sky-600 dark:text-sky-400 uppercase tracking-wider">Ready-to-Send Follow-Up Email</h4>
              <button
                onClick={() => navigator.clipboard.writeText(analysis.followUpEmail)}
                className="text-[11px] bg-sky-500/10 text-sky-600 dark:text-sky-300 px-3 py-1 rounded-lg font-bold"
              >
                Copy Email
              </button>
            </div>
            <pre className="p-4 rounded-xl bg-[var(--app-chip)]/40 border border-[var(--app-border)] text-xs text-[var(--app-text)] whitespace-pre-wrap font-sans">
              {analysis.followUpEmail}
            </pre>
          </div>
        </div>
      )}
    </div>
  );
}

Scaling Architecture

Handling Multi-Hour Meeting Transcripts

Two-hour enterprise meetings can generate over 25,000 words. Passing raw transcripts directly can hit model context limits or dilute extraction accuracy. Production meeting assistants use map-reduce chunking:

Phase 1: Map-Reduce Chunking

Split transcripts into 3,000-word blocks. Pass each block through a fast worker model concurrently via AsyncIO to extract local action items and segment decisions.

Phase 2: Global Synthesis

Aggregate all extracted sub-summaries and pass them to GPT-4o for deduplication, final executive synthesis, and professional follow-up email compilation.

Level Up

Hands-On Build Challenges

Ready to take this assistant to production? Implement these three enhancements:

Challenge 1: Whisper Audio Upload

Integrate OpenAI Whisper API so users can upload MP3 or WAV meeting recordings directly.

Challenge 2: Calendar Integration

Connect Google Calendar API to automatically fetch attendee lists and match participant names to action items.

Challenge 3: Jira/Linear Sync

Add a 1-click "Create Jira Issues" button that turns extracted action items into tracked engineering tickets.

Release Gate

Production Meeting Assistant Checklist

✓Meeting transcripts are validated for length, rejecting inputs exceeding token limits or returning chunking warnings.
✓Output data is structured strictly via Zod JSON schemas, guaranteeing separate arrays for decisions and action items.
✓Action items automatically extract assignee names and explicit deadlines when mentioned in dialogue.
✓UI provides instant 1-click clipboard copy buttons for follow-up emails and Slack synopses.
✓Sensitive corporate transcript data is processed under zero-retention provider API agreements.
✓Error boundaries gracefully handle rate-limit throttles or malformed speech-to-text formatting.
✓API keys are sequestered safely in server environment variables, never exposed to the client browser.

Key Takeaways

Structured extraction converts conversational audio into corporate execution.

Building an AI meeting assistant demonstrates the true power of LLMs as structured data processors. By combining Zod schema parsing with clean workspace UI tables, you eliminate meeting friction and turn lengthy discussions into clear, accountable action items.

Raw Transcript → Zod Schema Extraction → Action Item Table → Stakeholder Follow-Up Email.