GenCN UI

Summarize Selection

GencnUI-powered text selection summarization component that appears when users select text.

Try out the component below to see how it provides text summarization when you select text on the page.

Loading preview...
"use client";

import * as React from "react";
import { GencnUISummarizeSelection } from "@/registry/new-york/gencn-ui/items/summarize-selection/gencn-ui-summarize-selection";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

export function GencnUISummarizeSelectionExample() {
  return (
    <>
      <Card className="w-full border-none shadow-none">
        <CardHeader>
          <CardTitle>Text Selection Summarization</CardTitle>
          <CardDescription>
            Select any text below and a summarize button will appear. Click it
            to generate a summary of the selected text.
          </CardDescription>
        </CardHeader>
        <CardContent className="space-y-6">
          <div className="prose prose-sm max-w-none">
            <h3 className="mb-3 text-lg font-semibold">
              Artificial Intelligence
            </h3>
            <p className="mb-4 text-sm leading-relaxed">
              Artificial intelligence (AI) has revolutionized the way we
              interact with technology. From voice assistants to recommendation
              systems, AI is embedded in many aspects of our daily lives.
              Machine learning algorithms can process vast amounts of data and
              identify patterns that would be impossible for humans to detect
              manually. This capability enables AI systems to make predictions,
              automate tasks, and provide insights across various industries.
            </p>
            <p className="mb-4 text-sm leading-relaxed">
              The field of natural language processing has seen remarkable
              advances in recent years. Large language models can now understand
              context, generate human-like text, and perform complex language
              tasks with impressive accuracy. These developments have opened up
              new possibilities for content creation, translation, and automated
              customer service.
            </p>
            <p className="mb-4 text-sm leading-relaxed">
              As AI technology continues to evolve, we can expect to see even
              more sophisticated applications that enhance productivity and
              creativity. However, it's also important to consider the ethical
              implications and ensure that AI systems are developed and deployed
              responsibly.
            </p>

            <h3 className="mt-6 mb-3 text-lg font-semibold">
              Machine Learning
            </h3>
            <p className="mb-4 text-sm leading-relaxed">
              Machine learning is a subset of artificial intelligence that
              focuses on enabling machines to learn from data without being
              explicitly programmed. It involves training algorithms on large
              datasets to recognize patterns and make predictions. There are
              three main types of machine learning: supervised learning, where
              models learn from labeled data; unsupervised learning, which finds
              patterns in unlabeled data; and reinforcement learning, where
              agents learn through trial and error with rewards and penalties.
            </p>
            <p className="text-sm leading-relaxed">
              Deep learning, a subset of machine learning, uses neural networks
              with multiple layers to process complex data. These networks have
              been particularly successful in image recognition, natural
              language processing, and speech recognition tasks. The
              availability of large datasets and powerful computing resources
              has accelerated the development of deep learning models in recent
              years.
            </p>
          </div>
        </CardContent>
      </Card>

      <GencnUISummarizeSelection
        defaultOptions={{ format: "plain-text", length: "medium" }}
      />
    </>
  );
}

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.

The GencnUISummarizeSelection component is a standalone component that works with GencnUIProvider. You need both components installed.

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

import * as React from "react";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Sparkles } from "lucide-react";
import {
  ButtonPosition,
  SummarizeSelectionProps,
} from "@/registry/new-york/gencn-ui/items/shared/gencn-ui-types";
import { GencnUISummarizeBlock } from "@/registry/new-york/gencn-ui/items/summarize-block/gencn-ui-summarize-block";
import {
  SummarizerClient,
} from "@/registry/new-york/gencn-ui/items/shared/lib/gencn-ui-summarizer";

export function GencnUISummarizeSelection({
  className,
  defaultOptions,
  dialogTitle = "Summary",
}: SummarizeSelectionProps) {
  const [selectionText, setSelectionText] = React.useState<string>("");
  const [position, setPosition] = React.useState<ButtonPosition>({
    top: 0,
    left: 0,
    visible: false,
  });
  const [open, setOpen] = React.useState(false);
  const debounceRef = React.useRef<number | null>(null);
  const rangeRef = React.useRef<Range | null>(null);
  const isSelectionReversedRef = React.useRef<boolean>(false);

  // Use lib functions directly for summarizer support/availability
  const [isSummarizerSupported, setIsSummarizerSupported] = React.useState<
    boolean | null
  >(null);
  const [summarizerAvailability, setSummarizerAvailability] = React.useState<
    "available" | "downloadable" | "unavailable" | null
  >(null);

  React.useEffect(() => {
    const supported = SummarizerClient.isSupported();
    setIsSummarizerSupported(supported);
    if (supported) {
      SummarizerClient.checkAvailability()
        .then((avail) => {
          setSummarizerAvailability(avail);
        })
        .catch(() => {
          setSummarizerAvailability(null);
        });
    }
  }, []);

  const clearPosition = React.useCallback(() => {
    setPosition((p) => ({ ...p, visible: false }));
  }, []);

  const getTargetRect = React.useCallback((range: Range): DOMRect | null => {
    const rects = range.getClientRects();
    if (!rects || rects.length === 0) return range.getBoundingClientRect();
    // For reversed selections (bottom to top), use the first rect
    // For normal selections (top to bottom), use the last rect
    if (isSelectionReversedRef.current) {
      for (let i = 0; i < rects.length; i++) {
        const r = rects[i];
        if (r && r.width > 0 && r.height > 0) return r as DOMRect;
      }
    } else {
      // Prefer the last non-empty rect for multi-line selections
      for (let i = rects.length - 1; i >= 0; i--) {
        const r = rects[i];
        if (r && r.width > 0 && r.height > 0) return r as DOMRect;
      }
    }
    return range.getBoundingClientRect();
  }, []);

  const updateFromSelection = React.useCallback(() => {
    if (open) {
      clearPosition();
      return;
    }
    const sel = window.getSelection?.();
    if (!sel || sel.isCollapsed || !sel.rangeCount) {
      setSelectionText("");
      clearPosition();
      rangeRef.current = null;
      return;
    }
    const range = sel.getRangeAt(0);
    const text = sel.toString().trim();
    if (!text) {
      setSelectionText("");
      clearPosition();
      rangeRef.current = null;
      return;
    }

    // Detect selection direction: if anchor comes after focus, selection was made bottom to top
    const anchorNode = sel.anchorNode;
    const focusNode = sel.focusNode;
    let isReversed = false;

    if (anchorNode && focusNode) {
      if (anchorNode === focusNode) {
        // Same node, compare offsets
        isReversed = sel.anchorOffset > sel.focusOffset;
      } else {
        // Different nodes: create ranges at anchor and focus points and compare them
        const anchorRange = document.createRange();
        anchorRange.setStart(anchorNode, sel.anchorOffset);
        anchorRange.collapse(true);

        const focusRange = document.createRange();
        focusRange.setStart(focusNode, sel.focusOffset);
        focusRange.collapse(true);

        // Compare which range comes first in document order
        // If anchor range comes after focus range, selection was reversed (bottom to top)
        const comparison = anchorRange.compareBoundaryPoints(
          Range.START_TO_START,
          focusRange
        );
        isReversed = comparison > 0;
      }
    }

    isSelectionReversedRef.current = isReversed;

    rangeRef.current = range.cloneRange();
    const rect = getTargetRect(rangeRef.current);

    // If selection is reversed (bottom to top), show button above the selection
    // If selection is normal (top to bottom), show button below the selection
    const BUTTON_HEIGHT = 32; // Approximate button height (h-8 = 32px)
    const SPACING = 6;
    const top = isReversed
      ? (rect?.top ?? 0) - BUTTON_HEIGHT - SPACING
      : (rect?.bottom ?? 0) + SPACING;
    const left = (rect?.right ?? 0) + 6;
    setSelectionText(text);
    setPosition({ top, left, visible: true });
  }, [clearPosition, getTargetRect, open]);

  const debouncedUpdate = React.useCallback(() => {
    if (debounceRef.current) window.clearTimeout(debounceRef.current);
    debounceRef.current = window.setTimeout(updateFromSelection, 100);
  }, [updateFromSelection]);

  React.useEffect(() => {
    const onSelectionChange = () => debouncedUpdate();
    document.addEventListener("selectionchange", onSelectionChange);
    const onScrollOrResize = () => {
      if (open) return;
      if (rangeRef.current) {
        const rect = getTargetRect(rangeRef.current);
        const BUTTON_HEIGHT = 32; // Approximate button height (h-8 = 32px)
        const SPACING = 6;
        const isReversed = isSelectionReversedRef.current;
        const top = isReversed
          ? (rect?.top ?? 0) - BUTTON_HEIGHT - SPACING
          : (rect?.bottom ?? 0) + SPACING;
        const left = (rect?.right ?? 0) + 6;
        setPosition((p) => ({ ...p, top, left }));
      }
    };
    window.addEventListener("scroll", onScrollOrResize, true);
    window.addEventListener("resize", onScrollOrResize, true);
    return () => {
      document.removeEventListener("selectionchange", onSelectionChange);
      window.removeEventListener("scroll", onScrollOrResize, true);
      window.removeEventListener("resize", onScrollOrResize, true);
      if (debounceRef.current) window.clearTimeout(debounceRef.current);
    };
  }, [debouncedUpdate, getTargetRect]);

  const handleOpen = React.useCallback(async () => {
    if (!selectionText) return;

    // Check if summarizer is downloadable - if so, don't open dialog
    // The hook will automatically register download when generate() is called
    if (isSummarizerSupported && summarizerAvailability === "downloadable") {
      // Don't open dialog - user can download from widget
      // Registration will happen automatically when generate() is called in the dialog
      return;
    }

    // If summarizer is not supported or unavailable, and server LLM is enabled, open dialog (will use server fallback)
    // If summarizer is available, open dialog
    setOpen(true);
  }, [
    selectionText,
    isSummarizerSupported,
    summarizerAvailability,
  ]);

  // Hide the button when dialog opens to avoid overlap
  React.useEffect(() => {
    if (open) clearPosition();
  }, [open, clearPosition]);

  return (
    <>
      {!open && position.visible && (
        <div
          className={cn("fixed z-50", className)}
          style={{ top: position.top, left: position.left }}
        >
          <Tooltip>
            <TooltipTrigger asChild>
              <Button
                type="button"
                size="icon"
                variant="outline"
                className="h-8 w-8 rounded-lg shadow"
                onMouseDown={(e: React.MouseEvent<HTMLButtonElement>) =>
                  e.preventDefault()
                }
                onClick={() => void handleOpen()}
              >
                <Sparkles className="h-4 w-4" />
              </Button>
            </TooltipTrigger>
            <TooltipContent>Summarize selection</TooltipContent>
          </Tooltip>
        </div>
      )}

      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="sm:max-w-xl">
          <DialogHeader>
            <DialogTitle>{dialogTitle}</DialogTitle>
          </DialogHeader>
          {selectionText && (
            <GencnUISummarizeBlock
              text={selectionText}
              {...defaultOptions}
            />
          )}
        </DialogContent>
      </Dialog>
    </>
  );
}

Component API