GenCN UI
Shadcn RegistryAI Elements

Suggestions

GencnUI-powered suggestion component helps you create variants of a sentence based on the provided context.

ai-elements suggestion component is powered by the use-gencn-ui-suggestions hook.

Loading preview...
"use client";

import * as React from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/spinner";
import { Suggestions, Suggestion } from "@/components/ai-elements/suggestion";
import { useGencnUISuggestions } from "@/registry/new-york/gencn-ui/items/shared/hooks/use-gencn-ui-suggestions";

const defaultSuggestPrompt =
  "Generate complete, independent sentence suggestions based on the user's input. Each suggestion should start with the user sentence and be a full, grammatically correct sentence that relates to or expands on the user's text. complete the user's text - and provide alternative complete sentences that the user might want to use. Make each suggestion unique and meaningful.";

export function SuggestionExample() {
  const [inputValue, setInputValue] = React.useState("");
  const suggestTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);

  // Use the new useGencnUISuggestions hook
  const {
    suggestions,
    isLoading,
    generate,
    reset,
    error,
  } = useGencnUISuggestions({
    instructions: defaultSuggestPrompt,
    minSuggestions: 1,
    maxSuggestions: 3,
    available: "enabled",
    maxWordsPerSuggestion: 5,
  });

  // Generate suggestions with debouncing
  const generateSuggestions = React.useCallback(
    async (text: string) => {
      const autoSuggestMinChars = 3;
      const autoSuggestDebounceMs = 500;

      if (!text.trim() || text.trim().length < autoSuggestMinChars) {
        reset();
        return;
      }

      // Cancel any existing timeout
      if (suggestTimeoutRef.current) {
        clearTimeout(suggestTimeoutRef.current);
      }

      // Debounce the suggestion generation
      const timeoutId = setTimeout(async () => {
        try {
          // Pass the current text as context for generating related sentence suggestions
          await generate(`User's current text: "${text}"`);
        } catch (err) {
          console.error("Error generating suggestions:", err);
        }
      }, autoSuggestDebounceMs);

      suggestTimeoutRef.current = timeoutId;
    },
    [generate, reset]
  );

  // Handle input change
  const handleInputChange = React.useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const value = e.target.value;
      setInputValue(value);
      generateSuggestions(value);
    },
    [generateSuggestions]
  );

  // Handle suggestion click
  const handleSuggestionClick = React.useCallback(
    (suggestion: string) => {
      setInputValue(suggestion);
      reset();
    },
    [reset]
  );

  // Cleanup on unmount
  React.useEffect(() => {
    return () => {
      if (suggestTimeoutRef.current) {
        clearTimeout(suggestTimeoutRef.current);
      }
      reset();
    };
  }, [reset]);

  return (
    <div className="space-y-4">
      <div className="space-y-2">
        <Label htmlFor="suggestion-input" className="text-sm font-medium">
          Type to get AI suggestions
        </Label>
        <Input
          id="suggestion-input"
          placeholder="Type something (at least 3 characters)..."
          value={inputValue}
          onChange={handleInputChange}
          className="w-full"
        />
      </div>

      {(isLoading || suggestions.length > 0) && (
        <div className="space-y-2">
          <Label className="text-sm font-medium">Suggestions</Label>
          {isLoading && suggestions.length === 0 ? (
            <div className="flex items-center gap-2 py-2">
              <Spinner className="size-4" />
              <span className="text-muted-foreground text-sm">
                Generating suggestions...
              </span>
            </div>
          ) : (
            <Suggestions>
              {suggestions.map((suggestion, index) => (
                <Suggestion
                  key={`${suggestion.text}-${index}`}
                  onClick={handleSuggestionClick}
                  suggestion={suggestion.text}
                />
              ))}
            </Suggestions>
          )}
        </div>
      )}
      {error && <p className="text-destructive text-xs">Error: {error}</p>}
    </div>
  );
}

Server API

Path:
/api/suggestions
Source:
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" },
      }
    );
  }
}