Todos os componentes

Team Prompt Library

Team Prompt Library é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

HextaUI MIT other

O que é

Team Prompt Library é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Não precisa de pacotes além do que o projeto já tem

Licença

Este componente vem de HextaUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Team Prompt Library

registry/new-york/blocks/team/team-prompt-library.tsx
"use client";

import {
  BookOpen,
  Heart,
  Loader2,
  MoreVertical,
  Plus,
  Search,
  Sparkles,
  Star,
  Trash2,
} from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/registry/new-york/ui/avatar";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/registry/new-york/ui/dialog";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/registry/new-york/ui/dropdown-menu";
import {
  Empty,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/registry/new-york/ui/empty";
import { Field, FieldContent, FieldLabel } from "@/registry/new-york/ui/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/registry/new-york/ui/input-group";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/registry/new-york/ui/select";
import { Separator } from "@/registry/new-york/ui/separator";
import { Textarea } from "@/registry/new-york/ui/textarea";

export interface TeamPrompt {
  id: string;
  title: string;
  prompt: string;
  description?: string;
  category?: string;
  tags?: string[];
  author: {
    id: string;
    name: string;
    avatar?: string;
  };
  isFavorite?: boolean;
  rating?: number;
  usageCount?: number;
  bestModel?: string;
  tone?: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface TeamPromptLibraryProps {
  prompts?: TeamPrompt[];
  currentUserId?: string;
  onCreate?: (data: {
    title: string;
    prompt: string;
    description?: string;
    category?: string;
    tags?: string[];
    bestModel?: string;
    tone?: string;
  }) => Promise<TeamPrompt>;
  onUpdate?: (promptId: string, data: Partial<TeamPrompt>) => Promise<void>;
  onDelete?: (promptId: string) => Promise<void>;
  onFavorite?: (promptId: string, isFavorite: boolean) => Promise<void>;
  onUse?: (prompt: TeamPrompt) => void;
  className?: string;
  showSearch?: boolean;
  showCategories?: boolean;
}

const defaultCategories = [
  "All",
  "Writing",
  "Code",
  "Analysis",
  "Creative",
  "Research",
  "Business",
];

function formatDate(date: Date): string {
  return new Intl.DateTimeFormat("en-US", {
    month: "short",
    day: "numeric",
    year: "numeric",
  }).format(date);
}

function getInitials(name: string): string {
  return name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .toUpperCase()
    .slice(0, 2);
}

export default function TeamPromptLibrary({
  prompts = [],
  currentUserId,
  onCreate,
  onUpdate,
  onDelete,
  onFavorite,
  onUse,
  className,
  showSearch = true,
  showCategories = true,
}: TeamPromptLibraryProps) {
  const [searchQuery, setSearchQuery] = useState("");
  const [selectedCategory, setSelectedCategory] = useState<string>("All");
  const [createDialogOpen, setCreateDialogOpen] = useState(false);
  const [isCreating, setIsCreating] = useState(false);
  const [actionLoading, setActionLoading] = useState<string | null>(null);
  const [promptData, setPromptData] = useState({
    title: "",
    prompt: "",
    description: "",
    category: "",
    tags: [] as string[],
    bestModel: "",
    tone: "",
  });

  const filteredPrompts = prompts.filter((prompt) => {
    const matchesCategory =
      selectedCategory === "All" || prompt.category === selectedCategory;
    const matchesSearch =
      !searchQuery.trim() ||
      prompt.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
      prompt.prompt.toLowerCase().includes(searchQuery.toLowerCase()) ||
      prompt.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
      prompt.tags?.some((tag) =>
        tag.toLowerCase().includes(searchQuery.toLowerCase())
      );
    return matchesCategory && matchesSearch;
  });

  const handleCreate = async () => {
    if (!(promptData.title.trim() && promptData.prompt.trim() && onCreate))
      return;

    setIsCreating(true);
    try {
      await onCreate(promptData);
      setPromptData({
        title: "",
        prompt: "",
        description: "",
        category: "",
        tags: [],
        bestModel: "",
        tone: "",
      });
      setCreateDialogOpen(false);
    } finally {
      setIsCreating(false);
    }
  };

  const handleAction = async (
    action: () => Promise<void>,
    promptId: string
  ) => {
    setActionLoading(promptId);
    try {
      await action();
    } finally {
      setActionLoading(null);
    }
  };

  return (
    <Card className={cn("w-full shadow-xs", className)}>
      <CardHeader>
        <div className="flex flex-col gap-4">
          <div className="flex flex-col flex-wrap gap-3 md:flex-row md:items-start md:justify-between">
            <div className="flex min-w-0 flex-1 flex-col gap-2">
              <CardTitle>Prompt Library</CardTitle>
              <CardDescription>
                {prompts.length} prompt{prompts.length !== 1 ? "s" : ""} shared
                with your team
              </CardDescription>
            </div>
            {onCreate && (
              <Dialog
                onOpenChange={setCreateDialogOpen}
                open={createDialogOpen}
              >
                <DialogTrigger asChild>
                  <Button className="w-full shrink-0 md:w-auto" type="button">
                    <Plus className="size-4" />
                    Add Prompt
                  </Button>
                </DialogTrigger>
                <DialogContent className="md:max-w-2xl">
                  <DialogHeader>
                    <DialogTitle>Add Prompt to Library</DialogTitle>
                    <DialogDescription>
                      Share a prompt with your team
                    </DialogDescription>
                  </DialogHeader>
                  <div className="flex flex-col gap-4">
                    <Field>
                      <FieldLabel htmlFor="prompt-title">Title</FieldLabel>
                      <FieldContent>
                        <InputGroup>
                          <InputGroupInput
                            id="prompt-title"
                            onChange={(
                              e: React.ChangeEvent<HTMLInputElement>
                            ) =>
                              setPromptData((prev) => ({
                                ...prev,
                                title: e.target.value,
                              }))
                            }
                            placeholder="Prompt title…"
                            type="text"
                            value={promptData.title}
                          />
                        </InputGroup>
                      </FieldContent>
                    </Field>
                    <Field>
                      <FieldLabel htmlFor="prompt-content">Prompt</FieldLabel>
                      <FieldContent>
                        <Textarea
                          id="prompt-content"
                          onChange={(
                            e: React.ChangeEvent<HTMLTextAreaElement>
                          ) =>
                            setPromptData((prev) => ({
                              ...prev,
                              prompt: e.target.value,
                            }))
                          }
                          placeholder="Enter the prompt text…"
                          rows={6}
                          value={promptData.prompt}
                        />
                      </FieldContent>
                    </Field>
                    <Field>
                      <FieldLabel htmlFor="prompt-description">
                        Description
                      </FieldLabel>
                      <FieldContent>
                        <Textarea
                          id="prompt-description"
                          onChange={(
                            e: React.ChangeEvent<HTMLTextAreaElement>
                          ) =>
                            setPromptData((prev) => ({
                              ...prev,
                              description: e.target.value,
                            }))
                          }
                          placeholder="What is this prompt best used for?"
                          rows={2}
                          value={promptData.description}
                        />
                      </FieldContent>
                    </Field>
                    <div className="grid gap-4 md:grid-cols-2">
                      <Field>
                        <FieldLabel htmlFor="prompt-category">
                          Category
                        </FieldLabel>
                        <FieldContent>
                          <Select
                            onValueChange={(value: string) =>
                              setPromptData((prev) => ({
                                ...prev,
                                category: value,
                              }))
                            }
                            value={promptData.category}
                          >
                            <SelectTrigger id="prompt-category">
                              <SelectValue placeholder="Select category" />
                            </SelectTrigger>
                            <SelectContent>
                              {defaultCategories
                                .filter((c) => c !== "All")
                                .map((category) => (
                                  <SelectItem key={category} value={category}>
                                    {category}
                                  </SelectItem>
                                ))}
                            </SelectContent>
                          </Select>
                        </FieldContent>
                      </Field>
                      <Field>
                        <FieldLabel htmlFor="prompt-model">
                          Best Model
                        </FieldLabel>
                        <FieldContent>
                          <InputGroup>
                            <InputGroupInput
                              id="prompt-model"
                              onChange={(
                                e: React.ChangeEvent<HTMLInputElement>
                              ) =>
                                setPromptData((prev) => ({
                                  ...prev,
                                  bestModel: e.target.value,
                                }))
                              }
                              placeholder="gpt-4, claude-3, etc."
                              type="text"
                              value={promptData.bestModel}
                            />
                          </InputGroup>
                        </FieldContent>
                      </Field>
                    </div>
                  </div>
                  <DialogFooter>
                    <Button
                      onClick={() => setCreateDialogOpen(false)}
                      type="button"
                      variant="outline"
                    >
                      Cancel
                    </Button>
                    <Button
                      aria-busy={isCreating}
                      data-loading={isCreating}
                      disabled={
                        !(promptData.title.trim() && promptData.prompt.trim())
                      }
                      onClick={handleCreate}
                      type="button"
                    >
                      {isCreating ? (
                        <>
                          <Loader2 className="size-4 animate-spin" />
                          Adding…
                        </>
                      ) : (
                        "Add Prompt"
                      )}
                    </Button>
                  </DialogFooter>
                </DialogContent>
              </Dialog>
            )}
          </div>
          {showSearch && (
            <InputGroup>
              <InputGroupAddon>
                <Search className="size-4" />
              </InputGroupAddon>
              <InputGroupInput
                onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
                  setSearchQuery(e.target.value)
                }
                placeholder="Search prompts…"
                type="search"
                value={searchQuery}
              />
            </InputGroup>
          )}
          {showCategories && (
            <div className="flex flex-wrap gap-2">
              {defaultCategories.map((category) => (
                <Button
                  className={cn(
                    "h-auto gap-2 px-3 py-1.5",
                    selectedCategory === category
                      ? "bg-primary text-primary-foreground"
                      : "bg-muted text-muted-foreground hover:bg-muted/80"
                  )}
                  key={category}
                  onClick={() => setSelectedCategory(category)}
                  type="button"
                  variant="ghost"
                >
                  {category !== "All" && <Sparkles className="size-3.5" />}
                  <span className="text-xs">{category}</span>
                </Button>
              ))}
            </div>
          )}
        </div>
      </CardHeader>
      <CardContent>
        {filteredPrompts.length === 0 ? (
          <Empty>
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <BookOpen className="size-6" />
              </EmptyMedia>
              <EmptyTitle>
                {searchQuery || selectedCategory !== "All"
                  ? "No prompts found"
                  : "No prompts yet"}
              </EmptyTitle>
            </EmptyHeader>
          </Empty>
        ) : (
          <div className="grid gap-4 md:grid-cols-2">
            {filteredPrompts.map((prompt) => (
              <div
                className="group flex flex-col gap-3 rounded-lg border bg-card p-4 transition-colors hover:border-primary hover:shadow-sm"
                key={prompt.id}
              >
                <div className="flex items-start justify-between gap-2">
                  <div className="flex min-w-0 flex-1 flex-col gap-2">
                    <h3 className="wrap-break-word font-semibold text-base leading-tight">
                      {prompt.title}
                    </h3>
                    {prompt.description && (
                      <p className="wrap-break-word line-clamp-2 text-muted-foreground text-sm">
                        {prompt.description}
                      </p>
                    )}
                    <div className="flex flex-wrap items-center gap-2">
                      {prompt.category && (
                        <Badge className="text-xs" variant="outline">
                          {prompt.category}
                        </Badge>
                      )}
                      {prompt.bestModel && (
                        <Badge className="text-xs" variant="secondary">
                          {prompt.bestModel}
                        </Badge>
                      )}
                      {prompt.rating && (
                        <div className="flex items-center gap-1">
                          <Star className="size-3 fill-yellow-500 text-yellow-500" />
                          <span className="text-muted-foreground text-xs">
                            {prompt.rating.toFixed(1)}
                          </span>
                        </div>
                      )}
                      {prompt.usageCount !== undefined && (
                        <span className="text-muted-foreground text-xs">
                          {prompt.usageCount} use
                          {prompt.usageCount !== 1 ? "s" : ""}
                        </span>
                      )}
                    </div>
                  </div>
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button
                        aria-label={`More options for ${prompt.title}`}
                        size="icon-sm"
                        type="button"
                        variant="ghost"
                      >
                        {actionLoading === prompt.id ? (
                          <Loader2 className="size-4 animate-spin" />
                        ) : (
                          <MoreVertical className="size-4" />
                        )}
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent
                      align="end"
                      collisionPadding={8}
                      sideOffset={4}
                    >
                      {onUse && (
                        <DropdownMenuItem onSelect={() => onUse(prompt)}>
                          <Sparkles className="size-4" />
                          Use Prompt
                        </DropdownMenuItem>
                      )}
                      {onFavorite && (
                        <DropdownMenuItem
                          onSelect={() =>
                            handleAction(
                              () => onFavorite(prompt.id, !prompt.isFavorite),
                              prompt.id
                            )
                          }
                        >
                          <Heart
                            className={cn(
                              "size-4",
                              prompt.isFavorite && "fill-red-500 text-red-500"
                            )}
                          />
                          {prompt.isFavorite
                            ? "Remove from favorites"
                            : "Add to favorites"}
                        </DropdownMenuItem>
                      )}
                      {onDelete && (
                        <>
                          <DropdownMenuSeparator />
                          <DropdownMenuItem
                            onSelect={() =>
                              handleAction(() => onDelete(prompt.id), prompt.id)
                            }
                            variant="destructive"
                          >
                            <Trash2 className="size-4" />
                            Delete
                          </DropdownMenuItem>
                        </>
                      )}
                    </DropdownMenuContent>
                  </DropdownMenu>
                </div>
                {prompt.tags && prompt.tags.length > 0 && (
                  <div className="flex flex-wrap gap-1">
                    {prompt.tags.map((tag) => (
                      <Badge className="text-xs" key={tag} variant="outline">
                        {tag}
                      </Badge>
                    ))}
                  </div>
                )}
                <Separator />
                <div className="flex items-center justify-between">
                  <div className="flex items-center gap-2">
                    <Avatar className="size-6">
                      <AvatarImage
                        alt={prompt.author.name}
                        src={prompt.author.avatar}
                      />
                      <AvatarFallback className="text-xs">
                        {getInitials(prompt.author.name)}
                      </AvatarFallback>
                    </Avatar>
                    <span className="text-muted-foreground text-xs">
                      {prompt.author.name}
                    </span>
                  </div>
                  <span className="text-muted-foreground text-xs">
                    {formatDate(prompt.createdAt)}
                  </span>
                </div>
              </div>
            ))}
          </div>
        )}
      </CardContent>
    </Card>
  );
}

Componentes parecidos