Textarea
GencnUI-powered textarea component with automatic text rewriting and tone customization.
Try out the component below to see how it works in practice. You can interact with the textarea and explore its AI-powered features.
Loading preview...
"use client"; import { useState } from "react"; import { GencnUITextarea } from "@/registry/new-york/gencn-ui/items/textarea/gencn-ui-textarea"; export function GencnUITextareaExample() { const [textareaValue, setTextareaValue] = useState("This is a test textarea"); return ( <> <div className="space-y-4"> <div className="space-y-2"> <label htmlFor="example-textarea" className="text-sm font-medium"> AI-Powered Textarea </label> <GencnUITextarea id="example-textarea" placeholder="Type or paste text here and use AI to summarize, improve, or translate it..." rows={6} features={[ "compose", "improve", "fix-grammar", "translate", "inline-suggest", ]} translateTargets={["en", "fr", "es", "de", "hi", "ja", "zh-CN"]} translateLanguageMap={{ en: "English", fr: "French", es: "Spanish", de: "German", hi: "Hindi", ja: "Japanese", "zh-CN": "Chinese (Simplified)", }} value={textareaValue} onAccept={(text) => setTextareaValue(text)} onChange={(e) => setTextareaValue(e.target.value)} className="w-full" /> </div> <p className="text-muted-foreground text-sm"> Click the AI button to access writing assistance features. </p> </div> </> ); }
Server API
Path:
/api/composeSource:
import { google, createGoogleGenerativeAI } from "@ai-sdk/google"; import { streamText } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; interface ComposeRequest { prompt?: string; text?: string; context?: string; options?: { tone?: string; format?: string; length?: string; }; streaming?: boolean; LLM_API_KEY?: string; } function buildSystemPrompt(options?: ComposeRequest["options"]): string { const tone = options?.tone || "neutral"; const format = options?.format || "plain-text"; const length = options?.length || "medium"; return `You are a helpful writing assistant. Generate content based on the user's prompt. - Tone: ${tone} - Format: ${format} - Length: ${length} Generate high-quality, appropriate content that matches the user's requirements.`; } export async function POST(req: Request) { try { const request: ComposeRequest = await req.json(); const { context, options, streaming = true, LLM_API_KEY, } = request; const prompt = request.prompt || ""; const systemPrompt = buildSystemPrompt(options); // Add context if provided const fullUserPrompt = context ? `${context}\n\n${prompt}` : prompt; if (!fullUserPrompt.trim()) { return new Response( JSON.stringify({ error: "Prompt 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("Compose API error:", error); return new Response( JSON.stringify({ error: "Internal server error", message: (error as Error).message, }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } }
Path:
/api/improveSource:
import { google, createGoogleGenerativeAI } from "@ai-sdk/google"; import { streamText } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; interface ImproveRequest { prompt?: string; text?: string; context?: string; options?: { tone?: string; format?: string; length?: string; }; streaming?: boolean; LLM_API_KEY?: string; } function buildSystemPrompt(): string { return `You are a helpful writing assistant. Improve the given text for clarity and readability while preserving its original meaning and intent.`; } export async function POST(req: Request) { try { const request: ImproveRequest = await req.json(); const { context, streaming = true, LLM_API_KEY, } = request; const text = request.text || ""; const systemPrompt = buildSystemPrompt(); // 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("Improve API error:", error); return new Response( JSON.stringify({ error: "Internal server error", message: (error as Error).message, }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } }
Path:
/api/fix-grammarSource:
import { google, createGoogleGenerativeAI } from "@ai-sdk/google"; import { streamText } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; interface FixGrammarRequest { prompt?: string; text?: string; context?: string; options?: { tone?: string; format?: string; length?: string; }; streaming?: boolean; LLM_API_KEY?: string; } function buildSystemPrompt(): string { return `You are a grammar and proofreading assistant. Fix any grammatical errors, spelling mistakes, and improve the clarity of the given text while preserving the original meaning and style. if there is no change and grammar is correct then return only the text without any changes or correct an return the text only`; } export async function POST(req: Request) { try { const request: FixGrammarRequest = await req.json(); const { context, streaming = true, LLM_API_KEY, } = request; const text = request.text || ""; const systemPrompt = buildSystemPrompt(); // 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("Fix Grammar API error:", error); return new Response( JSON.stringify({ error: "Internal server error", message: (error as Error).message, }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } }
Path:
/api/translateSource:
import { google, createGoogleGenerativeAI } from "@ai-sdk/google"; import { streamText } from "ai"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; interface TranslateRequest { prompt?: string; text?: string; context?: string; options?: { tone?: string; format?: string; length?: string; sourceLanguage?: string; targetLanguage?: string; }; streaming?: boolean; LLM_API_KEY?: string; } function buildSystemPrompt(): string { return `You are a translation assistant. Translate the given text accurately, preserving the meaning, tone, and style of the original text. The target language may be specified in the text itself.`; } export async function POST(req: Request) { try { const request: TranslateRequest = await req.json(); const { context, streaming = true, LLM_API_KEY, } = request; const text = request.text || ""; const systemPrompt = buildSystemPrompt(); // 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("Translate API error:", error); return new Response( JSON.stringify({ error: "Internal server error", message: (error as Error).message, }), { status: 500, headers: { "Content-Type": "application/json" }, } ); } }
Path:
/api/suggestionsSource:
import { google, createGoogleGenerativeAI } from "@ai-sdk/google"; import { generateObject } from "ai"; import { z } from "zod"; // Allow streaming responses up to 30 seconds export const maxDuration = 30; interface SuggestionsRequest { prompt: string; schema: Record<string, unknown>; omitResponseConstraintInput?: boolean; LLM_API_KEY?: string; } // Convert JSON Schema to Zod schema function jsonSchemaToZod(schema: Record<string, unknown>): z.ZodTypeAny { if (!schema || typeof schema !== "object") { return z.any(); } if (schema.type === "object") { const shape: Record<string, z.ZodTypeAny> = {}; if (schema.properties) { for (const [key, prop] of Object.entries( schema.properties as Record<string, Record<string, unknown>> )) { const fieldSchema = jsonSchemaToZod(prop); // Make optional if not in required array if ( schema.required && Array.isArray(schema.required) && schema.required.includes(key) ) { shape[key] = fieldSchema; } else { shape[key] = fieldSchema.optional(); } } } return z.object(shape); } else if (schema.type === "array") { const itemSchema = schema.items && typeof schema.items === "object" && !Array.isArray(schema.items) ? jsonSchemaToZod(schema.items as Record<string, unknown>) : z.any(); let arraySchema = z.array(itemSchema); // Handle minItems and maxItems if (typeof schema.minItems === "number") { arraySchema = arraySchema.min(schema.minItems) as z.ZodArray<z.ZodTypeAny>; } if (typeof schema.maxItems === "number") { arraySchema = arraySchema.max(schema.maxItems) as z.ZodArray<z.ZodTypeAny>; } return arraySchema; } else if (schema.type === "string") { let stringSchema = z.string(); if (typeof schema.minLength === "number") { stringSchema = stringSchema.min(schema.minLength); } if (typeof schema.maxLength === "number") { stringSchema = stringSchema.max(schema.maxLength); } if (schema.enum && Array.isArray(schema.enum)) { return z.enum(schema.enum as [string, ...string[]]); } return stringSchema; } else if (schema.type === "number") { let numberSchema = z.number(); if (typeof schema.minimum === "number") { numberSchema = numberSchema.min(schema.minimum); } if (typeof schema.maximum === "number") { numberSchema = numberSchema.max(schema.maximum); } return numberSchema; } else if (schema.type === "boolean") { return z.boolean(); } else if (schema.type === "integer") { let intSchema = z.number().int(); if (typeof schema.minimum === "number") { intSchema = intSchema.min(schema.minimum); } if (typeof schema.maximum === "number") { intSchema = intSchema.max(schema.maximum); } return intSchema; } return z.any(); } export async function POST(req: Request) { try { const request: SuggestionsRequest = await req.json(); const { prompt, schema, LLM_API_KEY } = request; if (!prompt) { return new Response(JSON.stringify({ error: "Prompt is required" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } if (!schema) { return new Response(JSON.stringify({ error: "Schema 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; // Convert JSON Schema to Zod schema const zodSchema = jsonSchemaToZod(schema); const result = await generateObject({ model: googleProvider("gemini-2.0-flash-exp"), schema: zodSchema, prompt, }); return new Response(JSON.stringify(result.object), { headers: { "Content-Type": "application/json" }, }); } catch (error) { console.error("Suggestions 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-textarea"use client"; import { forwardRef, useState, useRef, useMemo, useImperativeHandle, useCallback, type ComponentProps, type FocusEvent, type FormEvent, type KeyboardEvent, } from "react"; import { Wand2, Languages, Repeat2, SpellCheck } from "lucide-react"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverAnchor, } from "@/components/ui/popover"; import { useRewriter } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-rewriter"; import { useWriter } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-writer"; import { useProofreader } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-proofreader"; import { useGencnUI } from "@/registry/new-york/gencn-ui/items/shared/hooks/use-gencn-ui"; import { TranslateFeature } from "@/registry/new-york/gencn-ui/items/shared/components/gencn-ui-translate-feature"; import { ImproveFeature } from "@/registry/new-york/gencn-ui/items/shared/components/gencn-ui-improve-feature"; import { FixGrammarFeature } from "@/registry/new-york/gencn-ui/items/shared/components/gencn-ui-fix-grammar-feature"; import { ComposeFeature } from "@/registry/new-york/gencn-ui/items/shared/components/gencn-ui-compose-feature"; import { InlineSuggest } from "@/registry/new-york/gencn-ui/items/shared/components/gencn-ui-inline-suggest"; export type ButtonVisibility = "ALWAYS" | "ON_FOCUS"; export interface GencnUITextareaProps extends ComponentProps<"textarea"> { buttonVisibility?: ButtonVisibility; containerClassName?: string; buttonClassName?: string; features?: Array< | "compose" | "improve" | "fix-grammar" | "translate" | "inline-suggest" >; translateTargets?: string[]; translateLanguageMap?: Record<string, string>; placeholderPrompt?: string; writerOptions?: { tone?: "formal" | "neutral" | "casual"; format?: "markdown" | "plain-text"; length?: "short" | "medium" | "long"; sharedContext?: string; expectedInputLanguages?: string[]; expectedContextLanguages?: string[]; outputLanguage?: string; }; autoSuggestDebounceMs?: number; autoSuggestMinChars?: number; autoSuggestMaxChars?: number; autoSuggestPrompt?: string; onAccept?: (text: string) => void; onAIError?: (error: Error) => void; } export const GencnUITextarea = forwardRef<HTMLTextAreaElement, GencnUITextareaProps>( ( { buttonVisibility = "ALWAYS", containerClassName, buttonClassName, className, onFocus, onBlur, features, translateTargets, translateLanguageMap, placeholderPrompt, writerOptions, autoSuggestDebounceMs = 500, autoSuggestMinChars = 3, autoSuggestMaxChars = 48, autoSuggestPrompt, onAccept, onAIError, ...props }, ref ) => { const [isFocused, setIsFocused] = useState(false); const textareaRef = useRef<HTMLTextAreaElement>(null); const containerRef = useRef<HTMLDivElement>(null); const [activeFeature, setActiveFeature] = useState< "compose" | "improve" | "fix-grammar" | "translate" | null >(null); const [suggestionsOpen, setSuggestionsOpen] = useState(false); const [suggestMode, setSuggestMode] = useState<"inline" | null>(null); const lastKeyPressedRef = useRef<string | null>(null); const rewriter = useRewriter(); const writer = useWriter(); const proofreader = useProofreader(); const { enableServerLLM, } = useGencnUI(); const inlineSuggestActive = useMemo(() => { return ( (features?.includes("inline-suggest") ?? false) && (rewriter.isSupported === true || enableServerLLM) ); }, [features, rewriter.isSupported, enableServerLLM]); useImperativeHandle(ref, () => textareaRef.current as HTMLTextAreaElement, []); const handleFocus = useCallback( (e: FocusEvent<HTMLTextAreaElement>) => { setIsFocused(true); onFocus?.(e); }, [onFocus] ); const handleBlur = useCallback( (e: FocusEvent<HTMLTextAreaElement>) => { setIsFocused(false); onBlur?.(e); }, [onBlur] ); const handleKeyDown = useCallback( (e: KeyboardEvent<HTMLTextAreaElement>) => { // Track the last key pressed for suggestion trigger checks lastKeyPressedRef.current = e.key; props.onKeyDown?.(e); }, [props] ); const handleInput = useCallback( (e: FormEvent<HTMLTextAreaElement>) => { if (inlineSuggestActive && !suggestionsOpen) { const value = e.currentTarget.value; // Don't trigger if backspace was pressed if (lastKeyPressedRef.current === "Backspace") { props.onInput?.(e); return; } // Don't trigger if last character is not a space if (value.length > 0 && value[value.length - 1] !== " ") { props.onInput?.(e); return; } // Don't trigger if trimmed text ends with a dot const trimmedValue = value.trim(); if (trimmedValue.length > 0 && trimmedValue[trimmedValue.length - 1] === ".") { props.onInput?.(e); return; } if (value.trim().length >= autoSuggestMinChars) { setSuggestMode("inline"); setSuggestionsOpen(true); } } props.onInput?.(e); }, [inlineSuggestActive, autoSuggestMinChars, suggestionsOpen, props] ); const openCompose = useCallback(() => { // If writer is downloadable, don't open compose - user can download from widget // The hook will automatically register download when generate() is called if (writer.isSupported && writer.availability === "downloadable") { return; } setActiveFeature("compose"); }, [writer.isSupported, writer.availability]); const openImprove = useCallback(() => { // If rewriter is downloadable, don't open improve - user can download from widget // The hook will automatically register download when generate() is called if (rewriter.isSupported && rewriter.availability === "downloadable") { return; } setActiveFeature("improve"); }, [rewriter.isSupported, rewriter.availability]); const openFixGrammar = useCallback(() => { // If proofreader is downloadable, don't open fix-grammar - user can download from widget // The hook will automatically register download when generate() is called if ( proofreader.isSupported && proofreader.availability === "downloadable" ) { return; } setActiveFeature("fix-grammar"); }, [ proofreader.isSupported, proofreader.availability, ]); const openTranslate = useCallback(() => { setActiveFeature("translate"); }, []); const handleAcceptResult = useCallback( (text: string) => { if (textareaRef.current) { textareaRef.current.value = text; const event = new Event("input", { bubbles: true }); textareaRef.current.dispatchEvent(event); } onAccept?.(text); setActiveFeature(null); }, [onAccept] ); const hasAnySupportedFeature = useMemo(() => { if (!features || features.length === 0) return false; return features.some((feature) => { switch (feature) { case "compose": case "improve": case "fix-grammar": case "translate": return enableServerLLM; case "inline-suggest": return rewriter.isSupported === true || enableServerLLM; default: return false; } }); }, [features, rewriter.isSupported, enableServerLLM]); const featureIcon = useMemo(() => { if (!activeFeature && (features?.length ?? 0) === 1) { const f = features![0]; if (f === "compose") return <Wand2 className="size-4" />; if (f === "translate") return <Languages className="size-4" />; if (f === "improve" || f === "fix-grammar") return <Repeat2 className="size-4" />; } return <Wand2 className="size-4" />; }, [activeFeature, features]); const shouldShowButton = buttonVisibility === "ALWAYS" || (buttonVisibility === "ON_FOCUS" && isFocused); const hasFeatureUI = Array.isArray(features) && features.length > 0; const shouldShowMagicIcon = hasFeatureUI && hasAnySupportedFeature; return ( <div ref={containerRef} className={cn("relative", containerClassName)}> <Popover open={suggestionsOpen} onOpenChange={(open) => { setSuggestionsOpen(open); if (!open) setSuggestMode(null); }} > <PopoverAnchor asChild> <div className="relative"> <Textarea ref={textareaRef} className={cn( className, shouldShowButton && shouldShowMagicIcon && "pr-12" )} onFocus={handleFocus} onBlur={handleBlur} onKeyDown={handleKeyDown} onInput={handleInput} {...props} /> </div> </PopoverAnchor> <InlineSuggest inputRef={textareaRef} active={inlineSuggestActive} debounceMs={autoSuggestDebounceMs} minChars={autoSuggestMinChars} maxChars={autoSuggestMaxChars} prompt={autoSuggestPrompt} open={suggestionsOpen && suggestMode === "inline"} onOpenChange={setSuggestionsOpen} onModeChange={setSuggestMode} rewriter={rewriter} onChange={props.onChange ? (e) => { if (e.target instanceof HTMLTextAreaElement) { props.onChange?.(e as React.ChangeEvent<HTMLTextAreaElement>); } } : undefined} onInput={props.onInput ? (e) => { if (e.currentTarget instanceof HTMLTextAreaElement) { props.onInput?.(e as React.FormEvent<HTMLTextAreaElement>); } } : undefined} /> </Popover> {shouldShowButton && shouldShowMagicIcon && (features!.length === 1 ? ( <Button type="button" size="icon" variant="ghost" onClick={() => { if (features![0] === "compose") { openCompose(); } else if (features![0] === "improve") { openImprove(); } else if (features![0] === "fix-grammar") { openFixGrammar(); } else if (features![0] === "translate") { openTranslate(); } }} className={cn( "hover:bg-accent/80 absolute top-2.5 right-2.5 z-10 h-7 w-7 rounded-md shadow-sm transition-opacity", buttonClassName )} aria-label="AI actions" > {featureIcon} </Button> ) : ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button type="button" size="icon" variant="ghost" className={cn( "hover:bg-accent/80 absolute top-2.5 right-2.5 z-10 h-7 w-7 rounded-md shadow-sm transition-opacity", buttonClassName )} aria-label="Choose AI action" > <Wand2 className="size-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="min-w-44"> {features!.includes("compose") && ( <DropdownMenuItem onClick={openCompose}> <Wand2 className="mr-2 h-4 w-4" /> Compose </DropdownMenuItem> )} {features!.includes("improve") && ( <DropdownMenuItem onClick={openImprove}> <Repeat2 className="mr-2 h-4 w-4" /> Improve </DropdownMenuItem> )} {features!.includes("fix-grammar") && ( <DropdownMenuItem onClick={openFixGrammar}> <SpellCheck className="mr-2 h-4 w-4" /> Fix grammar </DropdownMenuItem> )} {features!.includes("translate") && ( <DropdownMenuItem onClick={openTranslate}> <Languages className="mr-2 h-4 w-4" /> Translate </DropdownMenuItem> )} </DropdownMenuContent> </DropdownMenu> ))} {features?.includes("compose") && ( <ComposeFeature open={activeFeature === "compose"} onOpenChange={(open) => { if (!open) setActiveFeature(null); }} onAccept={handleAcceptResult} onError={onAIError} writerOptions={writerOptions} placeholderPrompt={placeholderPrompt} /> )} {features?.includes("improve") && ( <ImproveFeature open={activeFeature === "improve"} onOpenChange={(open) => { if (!open) setActiveFeature(null); }} onAccept={handleAcceptResult} onError={onAIError} getCurrentValue={() => textareaRef.current?.value || ""} /> )} {features?.includes("fix-grammar") && ( <FixGrammarFeature open={activeFeature === "fix-grammar"} onOpenChange={(open) => { if (!open) setActiveFeature(null); }} onAccept={handleAcceptResult} onError={onAIError} getCurrentValue={() => textareaRef.current?.value || ""} /> )} {features?.includes("translate") && ( <TranslateFeature open={activeFeature === "translate"} onOpenChange={(open) => { if (!open) setActiveFeature(null); }} onAccept={handleAcceptResult} onError={onAIError} translateTargets={translateTargets} translateLanguageMap={translateLanguageMap} getCurrentValue={() => textareaRef.current?.value || ""} /> )} </div> ); } ); GencnUITextarea.displayName = "GencnUITextarea";

