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/summarizeSource:
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> </> ); }

