AI Sonner / Toast
AI-powered toast notifications using Sonner with automatic text rewriting and tone customization.
AI Sonner provides AI-powered toast notifications using Sonner with automatic text rewriting and tone customization capabilities.
Try out the component below to see how AI-powered toast notifications work with automatic text rewriting and tone customization.
Loading preview...
"use client"; import * as React from "react"; import { GencnUISonner, useGencnUIToast, Toaster } from "@/registry/new-york/gencn-ui/items/sonner/gencn-ui-sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import type { AITone } from "@/registry/new-york/gencn-ui/items/shared/gencn-ui-types"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; // Array of AITone values for runtime use const AITONE_OPTIONS: AITone[] = [ "friendly", "funny", "sarcastic", "rude", "dramatic", "professional", "cheerful", "empathetic", "concise", "motivational", ]; function GencnUISonnerExampleContent() { const [message, setMessage] = React.useState( "Your form is saved successfully!" ); const [tone, setTone] = React.useState<AITone | undefined>(undefined); const [toastType, setToastType] = React.useState< "success" | "error" | "info" | "warning" >("success"); const gencnUIToast = useGencnUIToast(); const handleToast = async () => { if (!message.trim()) return; const options = tone ? { tone } : undefined; switch (toastType) { case "success": await gencnUIToast.success(message, options); break; case "error": await gencnUIToast.error(message, options); break; case "info": await gencnUIToast.info(message, options); break; case "warning": await gencnUIToast.warning(message, options); break; } }; return ( <> <Toaster richColors duration={10000} /> <Card className="m-0 w-full gap-0 border-none p-0 shadow-none"> <CardHeader className="pb-3"> <CardTitle className="text-lg">GencnUI Sonner Example</CardTitle> </CardHeader> <CardContent className="space-y-3"> <div className="space-y-1.5"> <Label htmlFor="toast-message" className="text-xs"> Edit Message </Label> <Input id="toast-message" value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Enter your message..." className="h-9 w-full" /> </div> <div className="space-y-3"> <div className="space-y-1.5"> <Label htmlFor="toast-tone" className="text-xs"> Tone </Label> <Select value={tone} onValueChange={(value) => setTone(value as AITone)} > <SelectTrigger id="toast-tone" className="h-9 w-full"> <SelectValue placeholder="Default" /> </SelectTrigger> <SelectContent> {AITONE_OPTIONS.map((toneOption) => ( <SelectItem key={toneOption} value={toneOption}> {toneOption.charAt(0).toUpperCase() + toneOption.slice(1)} </SelectItem> ))} </SelectContent> </Select> </div> <div className="space-y-1.5"> <Label htmlFor="toast-type" className="text-xs"> Type </Label> <Select value={toastType} onValueChange={(value) => setToastType(value as typeof toastType) } > <SelectTrigger id="toast-type" className="h-9 w-full"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="success">Success</SelectItem> <SelectItem value="error">Error</SelectItem> <SelectItem value="info">Info</SelectItem> <SelectItem value="warning">Warning</SelectItem> </SelectContent> </Select> </div> <Button onClick={handleToast} disabled={!message.trim()} variant={toastType === "error" ? "destructive" : "default"} className="h-9 w-full px-4 text-xs" > Show Toast </Button> </div> {tone && ( <Button variant="ghost" size="sm" onClick={() => setTone(undefined)} className="-mt-2 h-6 text-xs" > Clear tone </Button> )} </CardContent> </Card> </> ); } export function GencnUISonnerExample() { return ( <GencnUISonner> <GencnUISonnerExampleContent /> </GencnUISonner> ); }
Server API
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" }, } ); } }
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-sonner"use client"; import * as React from "react"; import { toast, type ExternalToast } from "sonner"; import { useRewriter } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-rewriter"; import type { AITone } from "@/registry/new-york/gencn-ui/items/shared/gencn-ui-types"; export type { AITone }; export type GencnUISONNEROptions = ExternalToast & { tone?: AITone; maxLength?: number; retryOnFail?: boolean; }; interface GencnUISonnerContextValue { rewriteMessage: ( message: React.ReactNode, tone?: AITone, maxLength?: number, retryOnFail?: boolean ) => Promise<React.ReactNode>; } const GencnUISonnerContext = React.createContext< GencnUISonnerContextValue | undefined >(undefined); export interface GencnUISonnerProps { children: React.ReactNode; defaultRewriterOptions?: { sharedContext?: string; }; } export function GencnUISonner({ children, defaultRewriterOptions, }: GencnUISonnerProps) { // Call useRewriter with options, similar to how useChat is called const rewriter = useRewriter(); const rewriteMessage = React.useCallback( async ( message: React.ReactNode, tone?: AITone, maxLength?: number, retryOnFail: boolean = true ): Promise<React.ReactNode> => { if (typeof message !== "string") return message; const text = message as string; const toneInstruction = tone ? `Rephrase the following text in a ${tone} tone while preserving intent.` : "Rephrase the following text to improve clarity while preserving intent."; const lengthInstruction = typeof maxLength === "number" && maxLength > 0 ? ` Keep it under ${maxLength} characters.` : ""; const context = `${toneInstruction}${lengthInstruction} Respond with only the rewritten text, no quotes.`; try { // Use rewriter hook's generate function which handles downloadable state and server fallback automatically // If rewriter is downloadable, it will register the download and return the same text // If rewriter is unavailable, it will use server fallback if enabled const rewritten = await rewriter.generate({ text, tone, context: defaultRewriterOptions?.sharedContext ? `${defaultRewriterOptions.sharedContext}\n\n${context}` : context, streaming: false, }); return rewritten || text; } catch (err) { if (retryOnFail) return text; throw err; } }, [rewriter, defaultRewriterOptions] ); const value: GencnUISonnerContextValue = React.useMemo( () => ({ rewriteMessage, }), [rewriteMessage] ); return ( <GencnUISonnerContext.Provider value={value}> {children} </GencnUISonnerContext.Provider> ); } export function useGencnUISonner(): GencnUISonnerContextValue { const context = React.useContext(GencnUISonnerContext); if (context === undefined) { throw new Error( "useGencnUISonner must be used within a GencnUISonner component" ); } return context; } async function rewriteIfString( input: React.ReactNode, rewriteMessage: GencnUISonnerContextValue["rewriteMessage"], tone?: AITone, maxLength?: number, retryOnFail: boolean = true ): Promise<React.ReactNode> { if (typeof input !== "string") return input; return rewriteMessage(input, tone, maxLength, retryOnFail); } export function createGencnUIToast( rewriteMessage: GencnUISonnerContextValue["rewriteMessage"] ) { const gencnUIToast = async function ( message: React.ReactNode, options?: GencnUISONNEROptions ) { const rewritten = await rewriteIfString( message, rewriteMessage, options?.tone, options?.maxLength, options?.retryOnFail !== false ); return toast(rewritten, options); }; gencnUIToast.success = async function ( message: React.ReactNode, options?: GencnUISONNEROptions ) { const rewritten = await rewriteIfString( message, rewriteMessage, options?.tone, options?.maxLength, options?.retryOnFail !== false ); return toast.success(rewritten, options); }; gencnUIToast.error = async function ( message: React.ReactNode, options?: GencnUISONNEROptions ) { const rewritten = await rewriteIfString( message, rewriteMessage, options?.tone, options?.maxLength, options?.retryOnFail !== false ); return toast.error(rewritten, options); }; gencnUIToast.info = async function ( message: React.ReactNode, options?: GencnUISONNEROptions ) { const rewritten = await rewriteIfString( message, rewriteMessage, options?.tone, options?.maxLength, options?.retryOnFail !== false ); return toast.info(rewritten, options); }; gencnUIToast.warning = async function ( message: React.ReactNode, options?: GencnUISONNEROptions ) { const rewritten = await rewriteIfString( message, rewriteMessage, options?.tone, options?.maxLength, options?.retryOnFail !== false ); return toast.warning(rewritten, options); }; gencnUIToast.message = async function ( message: React.ReactNode, options?: GencnUISONNEROptions ) { const rewritten = await rewriteIfString( message, rewriteMessage, options?.tone, options?.maxLength, options?.retryOnFail !== false ); return toast.message(rewritten as any, options as any); }; gencnUIToast.loading = function ( message: React.ReactNode, options?: GencnUISONNEROptions ) { // For loading, show immediately; do not wait for rewriting to avoid UX delay. return toast.loading(message, options); }; gencnUIToast.custom = function ( renderer: Parameters<typeof toast.custom>[0], options?: ExternalToast ) { return toast.custom(renderer, options); }; gencnUIToast.dismiss = function (toastId?: number | string) { return toast.dismiss(toastId as any); }; gencnUIToast.promise = async function <T>( promise: Promise<T>, cfg: { loading: React.ReactNode; success: React.ReactNode | ((value: T) => React.ReactNode); error: React.ReactNode | ((error: any) => React.ReactNode); tone?: AITone; maxLength?: number; retryOnFail?: boolean; }, options?: GencnUISONNEROptions ) { // Show loading immediately (without waiting for rewrite) const id = toast.loading(cfg.loading, options); try { const value = await promise; const successContent = typeof cfg.success === "function" ? cfg.success(value) : cfg.success; const rewritten = await rewriteIfString( successContent, rewriteMessage, cfg.tone ?? options?.tone, cfg.maxLength ?? options?.maxLength, (cfg.retryOnFail ?? options?.retryOnFail) !== false ); return toast.success(rewritten, { ...options, id }); } catch (error) { const errorContent = typeof cfg.error === "function" ? cfg.error(error) : cfg.error; const rewritten = await rewriteIfString( errorContent, rewriteMessage, cfg.tone ?? options?.tone, cfg.maxLength ?? options?.maxLength, (cfg.retryOnFail ?? options?.retryOnFail) !== false ); return toast.error(rewritten, { ...options, id }); } }; return gencnUIToast; } // Hook to get toast functions export function useGencnUIToast() { const { rewriteMessage } = useGencnUISonner(); return React.useMemo( () => createGencnUIToast(rewriteMessage), [rewriteMessage] ); } // Re-export Toaster for convenience export { Toaster } from "sonner"; export type GencnUIToastType = ReturnType<typeof createGencnUIToast>;
Usage
Wrap your application with GencnUISonner component at a high level, then use the useGencnUIToast hook to access toast functions:
import {
GencnUISonner,
useGencnUIToast,
Toaster,
} from "@/registry/new-york/gencn-ui/items/sonner/gencn-ui-sonner";
function App() {
return (
<GencnUISonner>
<Toaster />
<YourAppContent />
</GencnUISonner>
);
}
function YourAppContent() {
const gencnUIToast = useGencnUIToast();
const handleClick = async () => {
await gencnUIToast.success("Form saved successfully!", {
tone: "friendly",
});
};
return <button onClick={handleClick}>Save</button>;
}
