GenCN UI

Summarize Block

GencnUI-powered summarize block component with automatic text rewriting and tone customization.

Try out the component below to see how it summarizes text blocks with automatic text rewriting and tone customization.

Loading preview...
"use client";

import { GencnUISummarizeBlock } from "@/registry/new-york/gencn-ui/items/summarize-block/gencn-ui-summarize-block";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { useState } from "react";

export function GencnUISummarizeBlockExample() {
  const [activeTab, setActiveTab] = useState<Array<string>>(["summary", "summary", "summary"]);

  const handleTabChange = (index: number, tab: string) => {
    setActiveTab((prev) => {
      const newTabs = [...prev];
      newTabs[index] = tab;
      return newTabs;
    });
  };

  // Three mixed product reviews
  const content1 = `⭐⭐⭐⭐⭐ Excellent product! I've been using these wireless earbuds for three months now and I'm genuinely impressed. The sound quality is fantastic - crystal clear highs and deep, rich bass. The battery life exceeds expectations, easily lasting through my entire workday and gym session. The noise cancellation works beautifully even in noisy coffee shops. The only minor issue I had was the case charging, but customer service resolved it quickly. Highly recommend for anyone looking for premium audio at a reasonable price point.`;

  const content2 = `⭐⭐ Disappointed with this purchase. The earbuds arrived with noticeable connectivity issues right out of the box. They frequently disconnect during phone calls, which is frustrating during important work conversations. The battery life is nowhere near what's advertised - I'm lucky to get 4 hours instead of the promised 8. The touch controls are also inconsistent and sometimes unresponsive. For the price, I expected much better quality. The build feels cheap and plastic, definitely not worth the investment. Returned after a week.`;

  const content3 = `⭐⭐⭐⭐ Good overall, but has some drawbacks. The sound quality is decent for the price range, though not exceptional. They fit comfortably and the design is sleek. Battery life is acceptable - lasts about 5-6 hours which works for my daily commute. However, the noise cancellation could be better - it struggles with loud environments like subway trains. The companion app is basic but functional. Overall, it's a solid mid-range option if you're not looking for premium features. Worth considering if you're on a budget.`;

  return (
    <>
      {/* Overall summary provider - summarizes all content together */}
      <Card className="w-full border-none shadow-none">
        <CardHeader>
          <CardTitle>Product Reviews Summary</CardTitle>
          <CardDescription>
            Overall summary of all reviews at the top. Each review has its own
            summary and original content tabs.
          </CardDescription>
        </CardHeader>
        <CardContent className="space-y-6">
          {/* Overall summary at the top */}
          <div>
            <h3 className="mb-3 text-lg font-semibold">Overall Summary</h3>
            <GencnUISummarizeBlock
              text={[content1, content2, content3].join("\n\n")}
              loading={<Skeleton className="h-4 w-3/4" />}
              fallback={<p className="text-muted-foreground leading-relaxed">No summary available</p>}
              error={<p className="text-muted-foreground leading-relaxed">Error summarizing content</p>}
              format="plain-text"
            />
          </div>
          <Separator />

          {/* Content sections - always visible, each with its own summary and original content tabs */}
          <div className="space-y-6">
            {/* Content 1 */}
            <div>
              <h3 className="mb-3 text-lg font-semibold">Review 1: Positive</h3>
              <Tabs defaultValue="summary" className="w-full" value={activeTab[0]} onValueChange={(tab) => handleTabChange(0, tab)}>
                <TabsList className="grid w-full grid-cols-2">
                  <TabsTrigger value="summary">Summary</TabsTrigger>
                  <TabsTrigger value="original">Original Content</TabsTrigger>
                </TabsList>
                <TabsContent value="summary" className="mt-4" forceMount hidden={activeTab[0] !== "summary"}>
                  <GencnUISummarizeBlock
                    text={content1}
                    format="plain-text"
                    loading={<Skeleton className="h-4 w-3/4" />}
                    fallback={<p className="text-muted-foreground leading-relaxed">No summary available</p>}
                    error={<p className="text-muted-foreground leading-relaxed">Error summarizing content</p>}
                  />
                </TabsContent>
                <TabsContent value="original" className="mt-4">
                  <p className="text-muted-foreground leading-relaxed">
                    {content1}
                  </p>
                </TabsContent>
              </Tabs>
            </div>

            <Separator />

            {/* Content 2 */}
            <div>
              <h3 className="mb-3 text-lg font-semibold">Review 2: Negative</h3>
              <Tabs defaultValue="summary" className="w-full" value={activeTab[1]} onValueChange={(tab) => handleTabChange(1, tab)}>
                <TabsList className="grid w-full grid-cols-2">
                  <TabsTrigger value="summary">Summary</TabsTrigger>
                  <TabsTrigger value="original">Original Content</TabsTrigger>
                </TabsList>
                <TabsContent value="summary" className="mt-4" forceMount hidden={activeTab[1] !== "summary"}>
                  <GencnUISummarizeBlock
                    text={content2}
                    format="plain-text"
                    loading={<Skeleton className="h-4 w-3/4" />}
                    fallback={<p className="text-muted-foreground leading-relaxed">No summary available</p>}
                    error={<p className="text-muted-foreground leading-relaxed">Error summarizing content</p>}
                  />
                </TabsContent>
                <TabsContent value="original" className="mt-4">
                  <p className="text-muted-foreground leading-relaxed">
                    {content2}
                  </p>
                </TabsContent>
              </Tabs>
            </div>

            <Separator />

            {/* Content 3 */}
            <div>
              <h3 className="mb-3 text-lg font-semibold">Review 3: Mixed</h3>
              <Tabs defaultValue="summary" className="w-full" value={activeTab[2]} onValueChange={(tab) => handleTabChange(2, tab)}>
                <TabsList className="grid w-full grid-cols-2">
                  <TabsTrigger value="summary">Summary</TabsTrigger>
                  <TabsTrigger value="original">Original Content</TabsTrigger>
                </TabsList>
                <TabsContent value="summary" className="mt-4" forceMount hidden={activeTab[2] !== "summary"}>
                  <GencnUISummarizeBlock
                    text={content3}
                    format="plain-text"
                    loading={<Skeleton className="h-4 w-3/4" />}
                    fallback={<p className="text-muted-foreground leading-relaxed">No summary available</p>}
                    error={<p className="text-muted-foreground leading-relaxed">Error summarizing content</p>}
                  />
                </TabsContent>
                <TabsContent value="original" className="mt-4">
                  <p className="text-muted-foreground leading-relaxed">
                    {content3}
                  </p>
                </TabsContent>
              </Tabs>
            </div>
          </div>
        </CardContent>
      </Card>
    </>
  );
}

Server API

Path:
/api/summarize
Source:
import { google, createGoogleGenerativeAI } from "@ai-sdk/google";
import { streamText } from "ai";

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

interface SummarizeRequest {
  prompt?: string;
  text?: string;
  context?: string;
  options?: {
    tone?: string;
    format?: string;
    length?: string;
  };
  streaming?: boolean;
  LLM_API_KEY?: string;
}

function buildSystemPrompt(options?: SummarizeRequest["options"]): string {
  const summaryType = options?.tone || "key-points"; // Using tone field for summary type
  const summaryFormat = options?.format || "plain-text";
  const summaryLength = options?.length || "medium";
  let typeDescription = "Key points";
  if (summaryType === "tldr") typeDescription = "TL;DR";
  else if (summaryType === "teaser") typeDescription = "Teaser";
  else if (summaryType === "headline") typeDescription = "Headline";
  else if (summaryType === "key-points") typeDescription = "Key points";
  return `You are a helpful summarization assistant. Summarize the given text effectively.
- Type: ${typeDescription}
- Format: ${summaryFormat}
- Length: ${summaryLength}
Generate a concise and accurate summary that captures the essential information from the text.`;
}

export async function POST(req: Request) {
  try {
    const request: SummarizeRequest = await req.json();
    const {
      context,
      options,
      streaming = true,
      LLM_API_KEY,
    } = request;

    const text = request.text || "";
    const systemPrompt = buildSystemPrompt(options);

    // Add context if provided
    const fullUserPrompt = context ? `${context}\n\n${text}` : text;

    if (!fullUserPrompt.trim()) {
      return new Response(
        JSON.stringify({ error: "Text is required" }),
        {
          status: 400,
          headers: { "Content-Type": "application/json" },
        }
      );
    }

    // Create provider instance with manual API key if provided, otherwise use default
    const googleProvider = LLM_API_KEY
      ? createGoogleGenerativeAI({ apiKey: LLM_API_KEY })
      : google;

    const result = streamText({
      model: googleProvider("gemini-2.5-flash-lite"),
      system: systemPrompt,
      prompt: fullUserPrompt,
      maxOutputTokens: 2000,
    });

    if (streaming) {
      // Use toTextStreamResponse for streaming text
      return result.toTextStreamResponse();
    } else {
      // For non-streaming, collect all chunks and return
      const text = await result.text;
      return new Response(JSON.stringify({ text }), {
        headers: { "Content-Type": "application/json" },
      });
    }
  } catch (error) {
    console.error("Summarize API error:", error);
    return new Response(
      JSON.stringify({
        error: "Internal server error",
        message: (error as Error).message,
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" },
      }
    );
  }
}

Installation

Setup Required: Make sure you have configured components.json first. See the installation guide for setup instructions.

npx shadcn@latest add @gencn-ui/gencn-ui-summarize-block
"use client";

import * as React from "react";
import { useSummarizer, type UseSummarizerOptions } from "@/registry/new-york/gencn-ui/items/shared/hooks/use-gencn-ui-summarizer";
import type { SummarizeOptions } from "@/registry/new-york/gencn-ui/items/shared/lib/gencn-ui-summarizer";
import { cn } from "@/lib/utils";

export interface GencnUISummarizeBlockProps extends UseSummarizerOptions {
  text: string;
  className?: string;
  debounceMs?: number;
  summarizeOptions?: SummarizeOptions;
  loading?: React.ReactNode;
  fallback?: React.ReactNode;
  error?: React.ReactNode;
}

export function GencnUISummarizeBlock({
  text,
  className,
  debounceMs = 5000,
  summarizeOptions,
  loading,
  fallback,
  error,
  ...summarizerOptions
}: GencnUISummarizeBlockProps) {
  // Refs are necessary for debouncing - they persist across renders without causing re-renders
  const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const lastSummarizedTextRef = React.useRef<string>("");

  const { isLoading, error: errorState, data, generate, reset } = useSummarizer(summarizerOptions);
  const result = data || "";
  const isResultOriginalText = result && result.trim() === text.trim();
  const trimmedText = text.trim();
  const hasContent = trimmedText.length > 0;

  // Debounced auto-summarize: only trigger if text actually changed
  React.useEffect(() => {
    if (!hasContent) {
      reset();
      return;
    }

    // Skip if text hasn't changed (optimization to avoid unnecessary debounce timers)
    if (trimmedText === lastSummarizedTextRef.current.trim()) {
      return;
    }

    // Clear any existing timer
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }

    // Set new debounced timer
    timerRef.current = setTimeout(async () => {
      await generate({
        text: text,
        context: summarizeOptions?.context,
        streaming: true,
      });
      lastSummarizedTextRef.current = text;
      timerRef.current = null;
    }, debounceMs);

    // Cleanup: clear timer on unmount or when dependencies change
    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [text, hasContent, generate, summarizeOptions?.context, reset, debounceMs]);

  if (errorState && error) {
    return <>{error}</>;
  }

  if (isLoading && loading) {
    return <>{loading}</>;
  }

  // Show fallback if no result, or if result is the same as original text (summarization didn't happen)
  if ((!result || isResultOriginalText) && !isLoading && fallback) {
    return <>{fallback}</>;
  }

  if (!result || isResultOriginalText) return null;

  return (
    <div className={cn("whitespace-pre-wrap", className)}>
      {result}
    </div>
  );
}

Component API