refactor: enhance PromptsContent with dropdown menu for actions, update loading states, and improve styling consistency

This commit is contained in:
Anish Sarkar 2026-05-19 12:59:02 +05:30
parent 3b168e987d
commit 78ad19dd6a
5 changed files with 68 additions and 92 deletions

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { AlertTriangle, Globe, Lock, Pencil, Sparkles, Trash2 } from "lucide-react"; import { AlertTriangle, Globe, Lock, MoreHorizontal, Pencil, Sparkles, Trash2 } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@ -10,6 +10,7 @@ import {
updatePromptMutationAtom, updatePromptMutationAtom,
} from "@/atoms/prompts/prompts-mutation.atoms"; } from "@/atoms/prompts/prompts-mutation.atoms";
import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms"; import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -20,7 +21,6 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { import {
@ -31,10 +31,14 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -42,6 +46,8 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import type { PromptRead } from "@/contracts/types/prompts.types"; import type { PromptRead } from "@/contracts/types/prompts.types";
@ -221,8 +227,12 @@ export function PromptsContent() {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="transform">Transform rewrites or modifies your text</SelectItem> <SelectItem value="transform">
<SelectItem value="explore">Explore answers a question about your text</SelectItem> Transform rewrites or modifies your text
</SelectItem>
<SelectItem value="explore">
Explore answers a question about your text
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -333,41 +343,45 @@ export function PromptsContent() {
</Button> </Button>
)} )}
</div> </div>
<div className="flex items-center gap-1 shrink-0 opacity-0 pointer-events-none transition-opacity duration-150 group-hover:opacity-100 group-hover:pointer-events-auto"> <DropdownMenu>
<Button <DropdownMenuTrigger asChild>
type="button" <Button
variant="ghost" type="button"
size="icon" variant="ghost"
title={prompt.is_public ? "Make private" : "Share with community"} size="icon"
onClick={() => handleTogglePublic(prompt)} className="h-7 w-7 shrink-0 self-center rounded-lg text-muted-foreground opacity-100 pointer-events-auto transition-opacity duration-150 hover:text-accent-foreground sm:opacity-0 sm:pointer-events-none sm:group-hover:opacity-100 sm:group-hover:pointer-events-auto"
disabled={togglingPublicIds.has(prompt.id)} >
className="h-7 w-7 rounded-lg text-muted-foreground hover:text-accent-foreground" <MoreHorizontal className="size-3.5" />
> <span className="sr-only">Prompt actions</span>
{togglingPublicIds.has(prompt.id) ? ( </Button>
<Spinner className="size-3.5" /> </DropdownMenuTrigger>
) : prompt.is_public ? ( <DropdownMenuContent align="end">
<Lock className="size-3.5" /> <DropdownMenuItem
) : ( onClick={() => handleTogglePublic(prompt)}
<Globe className="size-3.5" /> disabled={togglingPublicIds.has(prompt.id)}
)} >
</Button> {togglingPublicIds.has(prompt.id) ? (
<Button <Spinner className="size-4" />
variant="ghost" ) : prompt.is_public ? (
size="icon" <Lock className="size-4" />
className="h-7 w-7 rounded-lg text-muted-foreground hover:text-accent-foreground" ) : (
onClick={() => handleEdit(prompt)} <Globe className="size-4" />
> )}
<Pencil className="size-3.5" /> {prompt.is_public ? "Make private" : "Share with community"}
</Button> </DropdownMenuItem>
<Button <DropdownMenuItem onClick={() => handleEdit(prompt)}>
variant="ghost" <Pencil className="size-4" />
size="icon" Edit
className="h-7 w-7 rounded-lg text-muted-foreground hover:text-destructive" </DropdownMenuItem>
onClick={() => setDeleteTarget(prompt.id)} <DropdownMenuItem
> onClick={() => setDeleteTarget(prompt.id)}
<Trash2 className="size-3.5" /> className="text-destructive focus:text-destructive"
</Button> >
</div> <Trash2 className="size-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
))} ))}
</div> </div>

View file

@ -7,7 +7,6 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { materialDark, materialLight } from "react-syntax-highlighter/dist/esm/styles/prism"; import { materialDark, materialLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn, copyToClipboard } from "@/lib/utils"; import { cn, copyToClipboard } from "@/lib/utils";
type MarkdownCodeBlockProps = { type MarkdownCodeBlockProps = {
@ -49,7 +48,7 @@ function MarkdownCodeBlockComponent({
}, [hasCopied]); }, [hasCopied]);
return ( return (
<div className="mt-4 overflow-hidden rounded-2xl" style={{ background: "var(--syntax-bg)" }}> <div className="mt-4 overflow-hidden rounded-md bg-accent">
<div className="flex items-center justify-between gap-4 px-4 py-2 font-semibold text-muted-foreground text-sm"> <div className="flex items-center justify-between gap-4 px-4 py-2 font-semibold text-muted-foreground text-sm">
<span className="lowercase text-xs">{language}</span> <span className="lowercase text-xs">{language}</span>
<Button <Button
@ -82,23 +81,3 @@ function MarkdownCodeBlockComponent({
} }
export const MarkdownCodeBlock = memo(MarkdownCodeBlockComponent); export const MarkdownCodeBlock = memo(MarkdownCodeBlockComponent);
export function MarkdownCodeBlockSkeleton() {
return (
<div
className="mt-4 overflow-hidden rounded-2xl border"
style={{ background: "var(--syntax-bg)" }}
>
<div className="flex items-center justify-between gap-4 border-b px-4 py-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-8 w-8 rounded-md" />
</div>
<div className="space-y-2 p-4">
<Skeleton className="h-4 w-11/12" />
<Skeleton className="h-4 w-10/12" />
<Skeleton className="h-4 w-8/12" />
<Skeleton className="h-4 w-9/12" />
</div>
</div>
);
}

View file

@ -22,7 +22,6 @@ import { MentionChip } from "@/components/assistant-ui/mention-chip";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import { toast } from "sonner"; import { toast } from "sonner";
import { processChildrenWithCitations } from "@/components/citations/citation-renderer"; import { processChildrenWithCitations } from "@/components/citations/citation-renderer";
import { Skeleton } from "@/components/ui/skeleton";
import { tryGetHostname } from "@/lib/url"; import { tryGetHostname } from "@/lib/url";
import { import {
@ -39,30 +38,14 @@ import { getVirtualPathDisplay } from "@/lib/chat/virtual-path-display";
import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser"; import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function MarkdownCodeBlockSkeleton() { function MarkdownCodeBlockLoading() {
return ( return <div className="mt-4 h-32 overflow-hidden rounded-md bg-accent" />;
<div
className="mt-4 overflow-hidden rounded-2xl border"
style={{ background: "var(--syntax-bg)" }}
>
<div className="flex items-center justify-between gap-4 border-b px-4 py-2">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-8 w-8 rounded-md" />
</div>
<div className="space-y-2 p-4">
<Skeleton className="h-4 w-11/12" />
<Skeleton className="h-4 w-10/12" />
<Skeleton className="h-4 w-8/12" />
<Skeleton className="h-4 w-9/12" />
</div>
</div>
);
} }
const LazyMarkdownCodeBlock = dynamic( const LazyMarkdownCodeBlock = dynamic(
() => import("./markdown-code-block").then((mod) => mod.MarkdownCodeBlock), () => import("./markdown-code-block").then((mod) => mod.MarkdownCodeBlock),
{ {
loading: () => <MarkdownCodeBlockSkeleton />, loading: () => <MarkdownCodeBlockLoading />,
} }
); );
@ -443,7 +426,7 @@ const defaultComponents = memoizeMarkdownComponents({
<hr className={cn("aui-md-hr my-5 border-b", className)} {...props} /> <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
), ),
table: ({ className, ...props }) => ( table: ({ className, ...props }) => (
<div className="aui-md-table-wrapper my-5 overflow-hidden rounded-2xl border"> <div className="aui-md-table-wrapper my-5 overflow-hidden rounded-md border">
<Table className={cn("aui-md-table", className)} {...props} /> <Table className={cn("aui-md-table", className)} {...props} />
</div> </div>
), ),
@ -520,7 +503,7 @@ const defaultComponents = memoizeMarkdownComponents({
return ( return (
<code <code
className={cn( className={cn(
"aui-md-inline-code rounded-md border bg-muted px-1.5 py-0.5 font-mono text-[0.9em] font-normal", "aui-md-inline-code rounded-md bg-primary/10 px-1.5 py-0.5 font-mono text-[0.9em] font-normal text-primary/80",
className className
)} )}
{...props} {...props}
@ -533,7 +516,7 @@ const defaultComponents = memoizeMarkdownComponents({
return ( return (
<code <code
className={cn( className={cn(
"aui-md-inline-code rounded-md border bg-muted px-1.5 py-0.5 font-mono text-[0.9em] font-normal", "aui-md-inline-code rounded-md bg-primary/10 px-1.5 py-0.5 font-mono text-[0.9em] font-normal text-primary/80",
className className
)} )}
{...props} {...props}

View file

@ -916,8 +916,8 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-9 rounded-full p-1 font-semibold text-xs text-muted-foreground dark:border-muted-foreground/15 hover:bg-accent hover:text-accent-foreground" className="h-9 w-9 rounded-full p-0 font-semibold text-xs text-muted-foreground transition-colors dark:border-muted-foreground/15 hover:bg-foreground/10 hover:text-foreground"
aria-label="More actions" aria-label="Upload files, connect tools and more"
data-joyride="connector-icon" data-joyride="connector-icon"
> >
<Plus className="size-5" /> <Plus className="size-5" />
@ -1066,13 +1066,13 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
<DropdownMenu onOpenChange={(open) => !open && setToolsPopoverOpen(false)}> <DropdownMenu onOpenChange={(open) => !open && setToolsPopoverOpen(false)}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<TooltipIconButton <TooltipIconButton
tooltip="More actions" tooltip="Upload files, connect tools and more"
side="bottom" side="bottom"
disableTooltip={toolsPopoverOpen} disableTooltip={toolsPopoverOpen}
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-9 rounded-full p-1 font-semibold text-xs text-muted-foreground dark:border-muted-foreground/15 hover:bg-accent hover:text-accent-foreground" className="h-9 w-9 rounded-full p-0 font-semibold text-xs text-muted-foreground transition-colors dark:border-muted-foreground/15 hover:bg-foreground/10 hover:text-foreground"
aria-label="More actions" aria-label="Upload files, connect tools and more"
data-joyride="connector-icon" data-joyride="connector-icon"
> >
<Plus className="size-5" /> <Plus className="size-5" />

View file

@ -71,7 +71,7 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-neutral-800 text-white font-medium shadow-md shadow-neutral-950/40 px-3 py-1.5 dark:bg-neutral-800 dark:text-white border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none", "bg-neutral-950 text-white font-medium px-3 py-1.5 dark:bg-neutral-950 dark:text-white border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none",
className className
)} )}
{...props} {...props}